kit

kit
git clone https://git.ryansepassi.com/git/kit.git
Log | Files | Refs | README

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:
Msrc/cg/cg.c | 16++++++++++++++++
Msrc/decl/decl.c | 1+
Msrc/parse/parse.c | 48+++++++++++++++++++++++++++++++++++++++++++++++-
Mtest/parse/CORPUS.md | 1+
Atest/parse/cases/6_7_1_03_thread_local_basic.c | 7+++++++
Atest/parse/cases/6_7_1_03_thread_local_basic.expected | 1+
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