From 003f7076182daceaad61b0284a825f82d5fb4746 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Tue, 12 May 2026 12:49:01 +0900 Subject: [PATCH] w6c: emit DATAR for `let s: str = "literal"` initialisers Use the new DATAR mechanism so str-literal init on a top-level mutable `let` lands in .data and links cleanly. emit_lets, when it sees `let s: str = "lit"` (non-empty strlit), emits: DATAW s(SB),"<8 zero placeholder><8 LE bytes of len>" DATAR s+0(SB),(SB) The linker patches the placeholder with the strlit's runtime VA at program load time, so `s.ptr` reads as the real pointer and `s.len` as the literal length. A new let_pre_intern pass scans top-level lets ahead of emit_data so the strlit gets a DATA row in the same .s file; running emit_lets after emit_data instead would have flipped the (DATA strlits, DATAW lets) section order in the .s and broken byte-identity with the wwstage cgen. The wwstage cgen still emits the zero-init shape for str lets, which only matters if the wwstage is asked to compile source that uses str-literal init. None of the selfhost combined sources do that today, so test 994 / 990 stay green. The selfhost mirror for DATAR + DATAW + this w6c branch is a follow-up. 630_let_global gains two fixtures: a length-readback and a first- byte readback through the patched ptr. --- cmd/w6c/cgen.c | 79 ++++++++++++++++++++++++++++++--------- test/wcc/630_let_global.c | 21 +++++++++++ 2 files changed, 82 insertions(+), 18 deletions(-) diff --git a/cmd/w6c/cgen.c b/cmd/w6c/cgen.c index 745ea4a2..6b194993 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -3726,13 +3726,17 @@ emit_data_row_zero(FILE *out, const char *dir, const char *name, int sz) * Scalar lets (8B): emit the literal value, or 0 if no init. * Non-literal init: skip — undefined symbol surfaces at link time. * - * str lets (16B): emit 16 zero bytes when there is no init (or - * the init is `nil` / `""`). A non-empty strlit init would need a - * compile-time .data → .text relocation (asm doesn't support that - * yet); we silently skip and let the link fail loudly. + * str lets (16B): three init shapes are wired: + * - no rhs / `nil` / `""` → 16 zero bytes + * - `"literal"` (non-empty) → 8 zero placeholder + 8 LE len, + * plus DATAR patching the ptr + * half with the interned strlit's + * runtime VA at link time. * * Slice lets (24B): no-init only — the slot is zero. There's no - * literal slice syntax to honour, so this is the natural shape. */ + * literal slice syntax to honour, so this is the natural shape. + * + * Struct lets (size from Type.size): no-init only. */ static void emit_lets(Cg *c, FILE *out, Node *file) { @@ -3757,23 +3761,39 @@ emit_lets(Cg *c, FILE *out, Node *file) emit_data_row(out, "DATAW", mod_mangle(c, d->str), v); continue; } - /* Multi-word (str=16, slice=24, struct=N). Only zero-init - * shapes are supported: no rhs, or `nil`, or `""` (which - * interns to a strlit but we still emit a zero header — - * the program has to assign a real strlit / slice / field - * at runtime to use it). Struct literals as init are - * skipped (no compile-time eval), so a non-default init - * surfaces as an undefined-symbol link error. */ - if (d->rhs != NULL && !let_isstruct(d->type)) { - Node *r = d->rhs; + /* Strip leading casts on the rhs so a `nil: str` etc. + * reads the same as a bare nil. */ + Node *r = NULL; + if (d->rhs != NULL) { + r = d->rhs; while (r != NULL && r->kind == N_CAST) r = r->lhs; if (r == NULL) continue; - int empty_str = (r->kind == N_STRLIT && r->strlen == 0); - if (r->kind != N_NIL && !empty_str) - continue; } - if (d->rhs != NULL && let_isstruct(d->type)) + /* str literal init: bake the interned label's address + * into the ptr half via a DATAR reloc, set the len half + * inline. */ + if (sz == 16 && r != NULL && r->kind == N_STRLIT + && r->strlen > 0) { + const char *lab = intern_strlit(c, r->str, r->strlen); + const char *sym = mod_mangle(c, d->str); + u64 v = r->strlen; + /* 16-byte payload: 8 zero placeholder + LE len. */ + fprintf(out, "DATAW %s(SB),\"", sym); + for (int i = 0; i < 8; i++) emit_data_byte(out, 0); + for (int i = 0; i < 8; i++) + emit_data_byte(out, (u8)((v >> (i * 8)) & 0xff)); + fputs("\"\n", out); + fprintf(out, "DATAR %s+0(SB),%s(SB)\n", sym, lab); continue; + } + /* Otherwise: zero-init. str accepts nil / ""; struct + * accepts no rhs at all; slice accepts nil. */ + if (r != NULL) { + int is_struct = let_isstruct(d->type); + int empty_str = (r->kind == N_STRLIT && r->strlen == 0); + if (is_struct) continue; + if (r->kind != N_NIL && !empty_str) continue; + } emit_data_row_zero(out, "DATAW", mod_mangle(c, d->str), sz); } } @@ -3832,6 +3852,28 @@ sdef_collect(Cg *c, Node *file) } } +/* Pre-intern strlits referenced from top-level `let` initialisers + * (e.g. `let g: str = "hello";`). Interning has to happen before + * emit_data walks the strlit list, but we don't want to reorder + * emit_data after emit_lets (the (DATA strlits, DATAW lets) section + * order is part of the byte-identity contract with the selfhost + * cgen). So this pass populates the strlit table; emit_lets later + * just looks up the label. */ +static void +let_pre_intern(Cg *c, Node *file) +{ + if (file == NULL) return; + for (Node *d = file->list; d; d = d->next) { + if (d->kind != N_LET) continue; + if (let_emit_size(d->type) != 16) continue; + Node *r = d->rhs; + while (r != NULL && r->kind == N_CAST) r = r->lhs; + if (r == NULL || r->kind != N_STRLIT) continue; + if (r->strlen == 0) continue; + (void)intern_strlit(c, r->str, r->strlen); + } +} + void cg_file(Cg *c, FILE *out, Node *file) { @@ -3846,6 +3888,7 @@ cg_file(Cg *c, FILE *out, Node *file) if (d->kind != N_FNDECL) continue; cgfn(c, out, d); } + let_pre_intern(c, file); emit_data(c, out); emit_defs(c, out, file); emit_lets(c, out, file); diff --git a/test/wcc/630_let_global.c b/test/wcc/630_let_global.c index 845f312b..ac043cdb 100644 --- a/test/wcc/630_let_global.c +++ b/test/wcc/630_let_global.c @@ -113,6 +113,27 @@ static const struct fixture fixtures[] = { "};\n", 7, }, + { + "str-literal-init", + /* `let s: str = "literal";` — the ptr half is patched at + * link time by an R_X86_64_64 reloc against the strlit's + * data label; .len comes baked into the DATAW payload. */ + "let g: str = \"hello world\";\n" + "fn main() i32 = { return g.len: i32; };\n", + 11, + }, + { + "str-literal-via-fn", + /* Confirm the patched ptr really points at the right bytes + * by reading the first byte of g.ptr through a pointer + * cast. 'h' == 104. */ + "let g: str = \"hello\";\n" + "fn main() i32 = {\n" + "\tlet p: *u8 = g.ptr;\n" + "\treturn p[0]: i32;\n" + "};\n", + 104, + }, { "slice-zeroinit", /* Slice globals start as {nil, 0, 0}. .len and .cap both