commit fa5bef9f094356eb6e7054031eff88aa0a4c98b9
parent e863650bbfd2bcbfd4ee858301ff16f73eb674ae
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Mon, 11 May 2026 14:42:56 -0700
parse: accept _Thread_local; route TLS objects through tls_addr_of
The KW_THREAD_LOCAL token was tokenized but never consumed by
parse_decl_specs, so every `_Thread_local` source failed at parse time
even though codegen (tls_addr_of), the linker (PT_TLS / TLV), and the
JIT already supported TLS. Wire DF_THREAD from the decl-specs through
to Decl.flags, mint SK_TLS symbols, emit storage into .tdata / .tbss
with SF_TLS, and have cg_push_global materialize the per-thread address
via target->tls_addr_of for SK_TLS symbols so existing load/store paths
emit the normal OPK_INDIRECT sequence.
Diffstat:
6 files changed, 73 insertions(+), 1 deletion(-)
diff --git a/src/cg/cg.c b/src/cg/cg.c
@@ -729,6 +729,22 @@ void cg_retag_top(CG* g, const Type* ty) {
}
void cg_push_global(CG* g, ObjSymId sym, const Type* ty) {
+ /* TLS storage isn't reachable via a single (ADRP+ADD)-style addressing
+ * mode: the access sequence is multi-instruction (LE: tpidr_el0 + tprel
+ * relocs; macho: TLV descriptor call). Materialize the per-thread
+ * address eagerly through target->tls_addr_of and push it as an
+ * OPK_INDIRECT lvalue so subsequent load/store/addr_of paths emit the
+ * normal indirect sequence rather than the OPK_GLOBAL path. */
+ CGTarget* T = g->target;
+ const ObjSym* os = obj_symbol_get(T->obj, sym);
+ if (os && os->kind == SK_TLS) {
+ const Type* pty = type_ptr(g->pool, ty);
+ Reg r = alloc_reg_or_spill(g, RC_INT, pty);
+ Operand dst = op_reg(r, pty);
+ T->tls_addr_of(T, dst, sym, 0);
+ push(g, make_sv(op_indirect(r, 0, ty), ty));
+ return;
+ }
push(g, make_sv(op_global(sym, 0, ty), ty));
}
diff --git a/src/decl/decl.c b/src/decl/decl.c
@@ -89,6 +89,7 @@ DeclId decl_declare(DeclTable* t, const Decl* in) {
slot->storage != DS_REGISTER) {
SymBind bind = (slot->linkage == DL_EXTERNAL) ? SB_GLOBAL : SB_LOCAL;
SymKind k = (slot->type && slot->type->kind == TY_FUNC) ? SK_FUNC : SK_OBJ;
+ if (slot->flags & DF_THREAD) k = SK_TLS;
if (slot->flags & DF_WEAK) {
if (slot->linkage != DL_EXTERNAL)
compiler_panic(t->c, slot->loc,
diff --git a/src/parse/parse.c b/src/parse/parse.c
@@ -1034,6 +1034,8 @@ static int parse_decl_specs(Parser* p, DeclSpecs* out) {
seen = 1;
} else if (is_kw(p, &t, KW_INLINE)) {
out->flags |= DF_INLINE; advance(p); seen = 1;
+ } else if (is_kw(p, &t, KW_THREAD_LOCAL)) {
+ out->flags |= DF_THREAD; advance(p); seen = 1;
} else if (is_kw(p, &t, KW_NORETURN) || is_kw(p, &t, KW_REGISTER) ||
is_kw(p, &t, KW_AUTO)) {
/* Recognized but currently no-op at this slice. */
@@ -1931,6 +1933,7 @@ static int starts_type_name(const Parser* p, const Tok* t) {
case KW_AUTO:
case KW_TYPEDEF:
case KW_ALIGNAS:
+ case KW_THREAD_LOCAL:
return 1;
case KW_NONE: {
/* `__builtin_va_list` is a target-defined type-name (the va_list
@@ -5057,6 +5060,12 @@ static void define_static_object(Parser* p, ObjSymId sym, const Type* var_ty,
u8* buf = NULL;
int has_nonzero = 0;
ObjSecId override_sec;
+ /* TLS objects route to .tdata / .tbss with SF_TLS; decl_declare marked
+ * the symbol SK_TLS when the source carried `_Thread_local`. The
+ * .rodata override path is skipped — TLS storage is per-thread mutable
+ * even when declared `const`. */
+ const ObjSym* os = obj_symbol_get(ob, sym);
+ int is_tls = (os && os->kind == SK_TLS);
if (has_init) {
buf = (u8*)arena_array(p->c->tu, u8, size ? size : 1u);
@@ -5075,6 +5084,41 @@ static void define_static_object(Parser* p, ObjSymId sym, const Type* var_ty,
if (p->static_relocs_len) has_nonzero = 1;
}
+ if (is_tls) {
+ /* TLS path: .tbss for zero-init, .tdata otherwise. The section flags
+ * mirror what clang emits for `_Thread_local` globals so the linker's
+ * existing PT_TLS / TLV layout code applies unchanged. */
+ Sym sname;
+ ObjSecId sec;
+ u32 a = align ? align : 1u;
+ u32 base;
+ if (!has_init || !has_nonzero) {
+ sname = obj_secname_tbss(p->c);
+ sec = obj_section_ex(ob, sname, SEC_BSS, SSEM_NOBITS,
+ SF_ALLOC | SF_WRITE | SF_TLS, a, 0, OBJ_SEC_NONE, 0);
+ base = obj_align_to(ob, sec, a);
+ obj_reserve_bss(ob, sec, base + size, a);
+ obj_symbol_define(ob, sym, sec, base, size);
+ return;
+ }
+ sname = obj_secname_tdata(p->c);
+ sec = obj_section(ob, sname, SEC_DATA, SF_ALLOC | SF_WRITE | SF_TLS, a);
+ base = obj_align_to(ob, sec, a);
+ {
+ u8* dst = obj_reserve(ob, sec, size);
+ if (dst) memcpy(dst, buf, size);
+ }
+ obj_symbol_define(ob, sym, sec, base, size);
+ for (u32 i = 0; i < p->static_relocs_len; ++i) {
+ RelocKind rk = (p->static_relocs[i].size == 8) ? R_ABS64 : R_ABS32;
+ obj_reloc(ob, sec, base + p->static_relocs[i].offset, rk,
+ p->static_relocs[i].target, p->static_relocs[i].addend);
+ }
+ p->static_relocs_len = 0;
+ (void)loc;
+ return;
+ }
+
override_sec = pick_object_section(p, quals, has_nonzero);
if (override_sec != OBJ_SEC_NONE) {
/* .rodata path: emit bytes directly here so we can pin the section.
@@ -5275,7 +5319,7 @@ static void parse_init_declarator(Parser* p, const DeclSpecs* specs) {
decl_in.storage = DS_STATIC;
decl_in.linkage = DL_INTERNAL;
decl_in.visibility = SV_DEFAULT;
- decl_in.flags = DF_STATIC_LOCAL;
+ decl_in.flags = DF_STATIC_LOCAL | (specs->flags & DF_THREAD);
attr_list_to_decl(p->c, p->decls, specs->attrs, &decl_in);
did = decl_declare(p->decls, &decl_in);
sym = decl_obj_sym(p->decls, did);
@@ -5326,6 +5370,7 @@ static void parse_init_declarator(Parser* p, const DeclSpecs* specs) {
decl_in.storage = DS_EXTERN;
decl_in.linkage = DL_EXTERNAL;
decl_in.visibility = SV_DEFAULT;
+ decl_in.flags = specs->flags & DF_THREAD;
attr_list_to_decl(p->c, p->decls, specs->attrs, &decl_in);
did = decl_declare(p->decls, &decl_in);
sym = decl_obj_sym(p->decls, did);
@@ -6640,6 +6685,7 @@ static void parse_external_decl(Parser* p) {
decl_in.linkage = DL_EXTERNAL;
}
decl_in.visibility = SV_DEFAULT;
+ decl_in.flags = specs.flags & DF_THREAD;
attr_list_to_decl(p->c, p->decls, specs.attrs, &decl_in);
attr_list_to_decl(p->c, p->decls, dattrs, &decl_in);
did = decl_declare(p->decls, &decl_in);
diff --git a/test/parse/CORPUS.md b/test/parse/CORPUS.md
@@ -236,6 +236,7 @@ cover `register` and other specifier interactions.
|---|---|---|---|
| `6_7_1_01_register_sizeof` | ★ | `register int x=42; return (int)sizeof(x)>0?x:0;` — `sizeof` is legal on a register variable | 42 |
| `6_7_1_02_register_multi_decl` | ★ | `register int x=40, y=2; return x+y;` — multi-declarator with `register` | 42 |
+| `6_7_1_03_thread_local_basic` | ★ | `_Thread_local int x=42; return x;` — `_Thread_local` accepted as a storage-class specifier on a file-scope object | 42 |
## §6.7.2 Type specifiers
diff --git a/test/parse/cases/6_7_1_03_thread_local_basic.c b/test/parse/cases/6_7_1_03_thread_local_basic.c
@@ -0,0 +1,7 @@
+/* `_Thread_local int x = 42; return x;` — the C frontend must accept
+ * `_Thread_local` as a storage-class specifier on a file-scope object.
+ * Lower layers (codegen tls_addr_of, SK_TLS section emission, linker TLS
+ * image, JIT TLV setup) are already exercised by test/cg group N and
+ * test/link/cases/36_tls_basic; this case is the missing parser hook. */
+_Thread_local int tls_x = 42;
+int test_main(void) { return tls_x; }
diff --git a/test/parse/cases/6_7_1_03_thread_local_basic.expected b/test/parse/cases/6_7_1_03_thread_local_basic.expected
@@ -0,0 +1 @@
+42