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),<strlit_label>(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.
This commit is contained in:
2026-05-12 12:49:01 +09:00
parent d998425391
commit 003f707618
2 changed files with 82 additions and 18 deletions

View File

@@ -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);

View File

@@ -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