From 208bdd25dfa4c599e5dda241ff3ea67c398c9ffe Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Tue, 12 May 2026 12:10:23 +0900 Subject: [PATCH] =?UTF-8?q?w6c+selfhost:=20str=20globals=20=E2=80=94=2016B?= =?UTF-8?q?=20DATAW=20+=20(LEAQ,=20MOVQ,=20MOVQ)=20sequences?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extend top-level mutable `let` to cover str. The cgen now: - emits a 16-byte zero DATAW for `let s: str;` (and the trivial `nil` / `""` inits); a non-empty strlit init is skipped because a compile-time .data → .text reloc isn't supported yet, so the user gets a clean undefined-symbol error at link; - loads `s` as `(LEAQ s(SB), CX; MOVQ (CX), AX; MOVQ 8(CX), BX)` so the (AX=ptr, BX=len) pair convention is preserved; - stores via the same `&s` indirection for `s = expr;` and routes the `.ptr` / `.len` pseudo-field N_DOT branch through it; and - tracks the declared type on each LetVar so cgident / cgdot / cgassign pick the right load/store shape. Selfhost cgen mirrors all four paths byte-for-byte; test 990 (cgen-match on err.ww) and tests 994/995 (self-rebuild) stay green. 630_let_global gains two new fixtures (`let msg: str;` + runtime assign, plus reassign from a helper). Slice and struct globals still NYI — same scope deferred. --- cmd/w6c/cgen.c | 164 ++++++++++++++----- selfhost/cmd/w6c/main.combined.ww | 228 ++++++++++++++++++++------- selfhost/cmd/wcc/cgen.ww | 175 +++++++++++++------- selfhost/cmd/wcc/cgenexpr.ww | 53 ++++++- selfhost/cmd/wwdump/main.combined.ww | 228 ++++++++++++++++++++------- test/wcc/630_let_global.c | 22 +++ 6 files changed, 648 insertions(+), 222 deletions(-) diff --git a/cmd/w6c/cgen.c b/cmd/w6c/cgen.c index 008424e1..0fdd1738 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -397,13 +397,13 @@ struct LetVar { }; static LetVar *letvars; -/* Subset of types we know how to store in 8 bytes of .data and load - * back with a plain MOVQ. Floats need MOVSS/MOVSD; str/slice/struct/ - * tagged unions are multi-word; enums route through their storage - * type. Keep this tight — extending it requires the matching load/ - * store code below. */ +/* Slot size for a top-level `let` of type t, or 0 if the type isn't + * supported as a writable global yet. Floats (MOVSS/SD) and slice/ + * struct/tagged unions are deferred. enums route through their + * storage type. Keep this tight — extending it requires the matching + * load/store code below. */ static int -let_scalar_ok(Type *t) +let_emit_size(Type *t) { if (t == NULL) return 0; Type *u = (t->kind == TY_NAMED) ? t->under : t; @@ -414,12 +414,25 @@ let_scalar_ok(Type *t) case TY_U8: case TY_U16: case TY_U32: case TY_U64: case TY_INT: case TY_UINT: case TY_UINTPTR: case TY_PTR: - return 1; + return 8; + case TY_STR: + return 16; /* {ptr, len}; literal-strlit init NYI. */ default: return 0; } } +/* Is the unwrapped type a str? Used by the load/store paths so the + * (AX, BX) pair convention is preserved for str globals, mirroring + * what we already do for str locals. */ +static int +let_isstr(Type *t) +{ + if (t == NULL) return 0; + Type *u = (t->kind == TY_NAMED) ? t->under : t; + return u && u->kind == TY_STR; +} + static int decl_has_ffisym(Node *d) { @@ -476,7 +489,7 @@ let_collect(Cg *c, Node *file) for (Node *d = file->list; d; d = d->next) { if (d->kind != N_LET) continue; if (d->str == NULL || d->str[0] == '\0') continue; - if (!let_scalar_ok(d->type)) continue; + if (let_emit_size(d->type) == 0) continue; LetVar *lv = amalloc(c->a, sizeof *lv); lv->name = d->str; lv->next = letvars; @@ -751,6 +764,15 @@ cgexpr(Cg *c, Node *n, Local *locals) areg(D_BX)); goto ident_done; } + if (let_islet(n->str) && let_isstr(n->type)) { + /* Top-level str global: load both halves via + * its address (the asm has no `name+8(SB)` + * operand form). */ + ins2(c, A_LEAQ, masym(c, n->str), areg(D_CX)); + ins2(c, A_MOVQ, amem(D_CX, 0), areg(D_AX)); + ins2(c, A_MOVQ, amem(D_CX, 8), areg(D_BX)); + goto ident_done; + } ins2(c, A_MOVQ, masym(c, n->str), areg(D_AX)); } ident_done: @@ -1421,17 +1443,29 @@ cgexpr(Cg *c, Node *n, Local *locals) } /* Plain `name = strexpr;` for a str-typed local. cgexpr leaves * (AX=ptr, BX=len); store both halves at off+0 and off+8. - * Mirrors the let-init shape so reassignment doesn't truncate. */ + * Mirrors the let-init shape so reassignment doesn't truncate. + * Top-level str globals follow the same shape but go through + * &name(SB) since the asm has no `name+8(SB)` operand form. */ if (n->lhs && n->lhs->kind == N_IDENT && n->op == TK_ASSIGN && n->lhs->type) { Type *lt = n->lhs->type; Type *lu = (lt && lt->kind == TY_NAMED) ? lt->under : lt; if (lu && lu->kind == TY_STR) { int off = localfind(locals, n->lhs->str); - if (off == 0) break; - cgexpr(c, n->rhs, locals); - ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, off + 0)); - ins2(c, A_MOVQ, areg(D_BX), amem(D_BP, off + 8)); + if (off != 0) { + cgexpr(c, n->rhs, locals); + ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, off + 0)); + ins2(c, A_MOVQ, areg(D_BX), amem(D_BP, off + 8)); + break; + } + if (let_islet(n->lhs->str)) { + cgexpr(c, n->rhs, locals); + ins2(c, A_LEAQ, masym(c, n->lhs->str), + areg(D_CX)); + ins2(c, A_MOVQ, areg(D_AX), amem(D_CX, 0)); + ins2(c, A_MOVQ, areg(D_BX), amem(D_CX, 8)); + break; + } break; } } @@ -2429,6 +2463,20 @@ cgexpr(Cg *c, Node *n, Local *locals) } goto dot_done; } + /* Top-level str/slice `let` — load + * the field through &name(SB). Same + * pattern as the bare N_IDENT load. */ + if (let_islet(n->lhs->str)) { + int delta = ptrfld ? 0 + : (lenfld ? 8 : 16); + ins2(c, A_LEAQ, + masym(c, n->lhs->str), + areg(D_CX)); + ins2(c, A_MOVQ, + amem(D_CX, delta), + areg(D_AX)); + goto dot_done; + } } int delta = ptrfld ? 0 : (lenfld ? 8 : 16); ins2(c, A_MOVQ, amem(D_BP, off + delta), @@ -3570,50 +3618,88 @@ cgfn(Cg *c, FILE *out, Node *fn) txt_emit(out, c->head); } -/* Emit a single 8-byte DATA/DATAW row for a scalar value. Shares - * the escape rules with emit_defs/emit_data so the .o bytes stay - * stable. */ +/* Escape one byte for an asm string literal — the same rules + * emit_data and emit_defs already use. */ +static void +emit_data_byte(FILE *out, u8 b) +{ + if (b == '"' || b == '\\') + fprintf(out, "\\%c", b); + else if (b < 0x20 || b >= 0x7f) + fprintf(out, "\\x%02x", b); + else + fputc(b, out); +} + +/* Emit `DIR NAME(SB),"<8 LE bytes of v>"`. Used for scalar `def` + * constants (DATA) and scalar `let` globals (DATAW). */ static void emit_data_row(FILE *out, const char *dir, const char *name, u64 v) { fprintf(out, "%s %s(SB),\"", dir, name); - for (int i = 0; i < 8; i++) { - unsigned b = (unsigned)((v >> (i * 8)) & 0xff); - if (b == '"' || b == '\\') - fprintf(out, "\\%c", b); - else if (b < 0x20 || b >= 0x7f) - fprintf(out, "\\x%02x", b); - else - fputc(b, out); - } + for (int i = 0; i < 8; i++) + emit_data_byte(out, (u8)((v >> (i * 8)) & 0xff)); fputs("\"\n", out); } -/* Emit DATAW directives for top-level mutable `let` decls. Only scalar - * types in `let_scalar_ok` are supported; the rest are silently - * skipped at cgen and link with an undefined-symbol error if used. - * Initialisers must be integer/rune/bool/nil literals (or a `let` - * with no init, which zero-initialises). */ +/* Emit `DIR NAME(SB),""`. Used for top-level str/ + * slice/struct lets without a baked-in initialiser — the slot is + * pre-zeroed and the program writes the real value at runtime. */ +static void +emit_data_row_zero(FILE *out, const char *dir, const char *name, int sz) +{ + fprintf(out, "%s %s(SB),\"", dir, name); + for (int i = 0; i < sz; i++) + emit_data_byte(out, 0); + fputs("\"\n", out); +} + +/* Emit DATAW directives for top-level mutable `let` decls. + * + * 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. */ static void emit_lets(Cg *c, FILE *out, Node *file) { for (Node *d = file->list; d; d = d->next) { if (d->kind != N_LET) continue; if (d->str == NULL || d->str[0] == '\0') continue; - if (!let_scalar_ok(d->type)) continue; - u64 v = 0; + int sz = let_emit_size(d->type); + if (sz == 0) continue; + if (sz == 8) { + u64 v = 0; + if (d->rhs != NULL) { + Node *r = d->rhs; + while (r != NULL && r->kind == N_CAST) r = r->lhs; + if (r == NULL) continue; + if (r->kind == N_INTLIT) v = r->uval; + else if (r->kind == N_RUNELIT) v = r->uval; + else if (r->kind == N_TRUE) v = 1; + else if (r->kind == N_FALSE) v = 0; + else if (r->kind == N_NIL) v = 0; + else continue; + } + emit_data_row(out, "DATAW", mod_mangle(c, d->str), v); + continue; + } + /* Multi-word (str, 16B). 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 at runtime to use it). */ if (d->rhs != NULL) { Node *r = d->rhs; while (r != NULL && r->kind == N_CAST) r = r->lhs; if (r == NULL) continue; - if (r->kind == N_INTLIT) v = r->uval; - else if (r->kind == N_RUNELIT) v = r->uval; - else if (r->kind == N_TRUE) v = 1; - else if (r->kind == N_FALSE) v = 0; - else if (r->kind == N_NIL) v = 0; - else continue; /* non-literal init: skip */ + int empty_str = (r->kind == N_STRLIT && r->strlen == 0); + if (r->kind != N_NIL && !empty_str) + continue; } - emit_data_row(out, "DATAW", mod_mangle(c, d->str), v); + emit_data_row_zero(out, "DATAW", mod_mangle(c, d->str), sz); } } diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 76b4daa2..6e29a686 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -6129,11 +6129,20 @@ fn cgident(c: *cgen, n: *node) void = { return; }; // Top-level mutable `let` — RIP-relative load from its DATAW - // slot. Mirrors C cgen's catch-all `MOVQ masym(s), AX`. Names - // that aren't lets either (typos, never-defined) flow through - // here too in C; the divergence today is bounded to scalar lets, - // which is what `isletvar` gates on. + // slot. Mirrors C cgen's catch-all `MOVQ masym(s), AX` for + // scalar lets, plus the (LEAQ, MOVQ, MOVQ) sequence for str + // globals so both ptr and len land in (AX, BX). Names that + // aren't lets either (typos, never-defined) drop through to + // the silent return. if (isletvar(c, nm)) { + if (letvarisstr(c, nm)) { + emitline("\tLEAQ\t"); + emitsymname(c, nm); + emitline("(SB), CX\n"); + emitline("\tMOVQ\t(CX), AX\n"); + emitline("\tMOVQ\t8(CX), BX\n"); + return; + }; emitline("\tMOVQ\t"); emitsymname(c, nm); emitline("(SB), AX\n"); @@ -6599,6 +6608,30 @@ fn cgdot(c: *cgen, n: *node) void = { }; }; }; + // Top-level str global field access — load .ptr / .len via + // &name(SB) into CX, then MOVQ delta(CX), AX. Without this + // the module-qualified fallback below would mis-emit + // `MOVQ (SB), AX`. + if (lhs != nil) { + if (lhs.kind == nkind.N_IDENT) { + if (isletvar(c, lhs.str)) { + if (letvarisstr(c, lhs.str)) { + let delta: i32 = -1; + if (streq(fld, "ptr")) { delta = 0; }; + if (streq(fld, "len")) { delta = 8; }; + if (delta >= 0) { + emitline("\tLEAQ\t"); + emitsymname(c, lhs.str); + emitline("(SB), CX\n"); + emitline("\tMOVQ\t"); + emitdispreg(delta: i64, "CX"); + emitline(", AX\n"); + return; + }; + }; + }; + }; + }; // Module-qualified value reference: `mod.name` where `mod` // is nkind.N_IDENT bound as skind.SK_USE and the leaf isn't a local. // Treat as a SB symbol — `MOVQ leaf(SB), AX`. Same fallback @@ -7274,10 +7307,20 @@ fn cgassign(c: *cgen, n: *node) void = { if (off == 0) { // Top-level let target: RIP-relative store // for `=`, or load→combine→store for the - // compound forms. + // compound forms. For a str global, take its + // address into CX and store both halves; the + // asm has no `name+8(SB)` operand form. if (!isletvar(c, nm)) { return; }; cgexpr(c, n.rhs); if (n.op == tkind.TK_ASSIGN) { + if (letvarisstr(c, nm)) { + emitline("\tLEAQ\t"); + emitsymname(c, nm); + emitline("(SB), CX\n"); + emitline("\tMOVQ\tAX, (CX)\n"); + emitline("\tMOVQ\tBX, 8(CX)\n"); + return; + }; emitline("\tMOVQ\tAX, "); emitsymname(c, nm); emitline("(SB)\n"); @@ -8739,11 +8782,14 @@ type cgen = struct { }; // Top-level mutable `let` registry. Mirrors cmd/w6c/cgen.c LetVar. -// Populated alongside modents; consulted by cgassign and the TK_AMP -// path so reads/writes hit a RIP-relative DATAW slot instead of being -// silently dropped. Only scalar (≤8B) types make the list. +// Populated alongside modents; consulted by cgassign, cgdot, cgident +// and the TK_AMP path so reads/writes hit a RIP-relative DATAW slot +// instead of being silently dropped. tnode is the declared type AST +// node — needed to distinguish scalar (8B) from str (16B) globals +// when picking the load/store sequence. type letvar = struct { name: str, + tnode: *node, lvnext: *letvar, }; @@ -8967,8 +9013,7 @@ fn internstrlit(c: *cgen, bytes: str) str = { // letscalarprim — recognise the bare type-name keywords whose values // fit in an 8-byte .data slot and load back with a plain MOVQ. Float // types deliberately excluded; they need MOVSS/MOVSD. Mirrors C -// `let_scalar_ok` for the unwrapped TY_* enumeration. Pointer types -// (N_TPTR) handle separately at the callsite. +// `let_emit_size`'s 8-byte branch. fn letscalarprim(nm: str) bool = { if (streq(nm, "bool")) { return true; }; if (streq(nm, "rune")) { return true; }; @@ -8986,24 +9031,25 @@ fn letscalarprim(nm: str) bool = { return false; }; -// letscalarok — true iff the N_LET decl's declared type lands in the -// scalar set, walking type aliases. Pointer types are always ok. -fn letscalarok(c: *cgen, d: *node) bool = { - if (d == nil) { return false; }; +// letemitsize — slot size in bytes for a top-level `let`, or 0 if +// the type isn't yet supported as a writable global. Walks type +// aliases so byte output matches C cgen, which resolves Type kinds. +// 8 → scalar (literal init supported) +// 16 → str (only zero-init / nil / "" supported) +fn letemitsize(c: *cgen, d: *node) i32 = { + if (d == nil) { return 0; }; let t: *node = d.lhs; for (t != nil) { - if (t.kind == nkind.N_TPTR) { return true; }; - if (t.kind != nkind.N_TNAME) { return false; }; + if (t.kind == nkind.N_TPTR) { return 8; }; + if (t.kind != nkind.N_TNAME) { return 0; }; let nm: str = t.str; - if (letscalarprim(nm)) { return true; }; - // Resolve a `type x = y;` alias and look again. C cgen - // works on the resolved Type, so byte output diverges - // here without the walk. + if (letscalarprim(nm)) { return 8; }; + if (streq(nm, "str")) { return 16; }; let next: *node = aliaslookup(c, nm); - if (next == nil) { return false; }; + if (next == nil) { return 0; }; t = next; }; - return false; + return 0; }; fn collectlets(c: *cgen, file: *node) void = { @@ -9014,9 +9060,10 @@ fn collectlets(c: *cgen, file: *node) void = { if (d.kind == nkind.N_LET) { let nm: str = d.str; if (nm.len > 0) { - if (letscalarok(c, d)) { - let lv: *letvar = amalloc(c.a, 32u64): *letvar; + if (letemitsize(c, d) > 0) { + let lv: *letvar = amalloc(c.a, 48u64): *letvar; lv.name = nm; + lv.tnode = d.lhs; lv.lvnext = c.lets; c.lets = lv; }; @@ -9035,18 +9082,79 @@ fn isletvar(c: *cgen, name: str) bool = { return false; }; -// emitletdataw — DATAW directive per top-level scalar `let`. Same -// 8-byte LE byte encoding as emitdefconstants; only the directive -// keyword differs ("DATAW" vs "DATA"). Mirrors cmd/w6c/cgen.c -// emit_lets — w6a routes DATAW into a writable .data section, and -// w6l covers it with a second R+W PT_LOAD. +// letvarisstr — is the named top-level let a str global? Resolves +// aliases to mirror C cgen's `let_isstr`. Used by cgident/cgdot/ +// cgassign to pick the (LEAQ, MOVQ, MOVQ) sequence over the bare +// MOVQ scalar load. +fn letvarisstr(c: *cgen, name: str) bool = { + let lv: *letvar = c.lets; + for (lv != nil) { + if (streq(lv.name, name)) { + let t: *node = lv.tnode; + for (t != nil) { + if (t.kind != nkind.N_TNAME) { return false; }; + let nm: str = t.str; + if (streq(nm, "str")) { return true; }; + let nx: *node = aliaslookup(c, nm); + if (nx == nil) { return false; }; + t = nx; + }; + return false; + }; + lv = lv.lvnext; + }; + return false; +}; + +// emitdatawbyte — write one byte of an asm string literal using +// the same escape rules as emitdefconstants / emitdatasection. +fn emitdatawbyte(b: u8) void = { + if (b == 34u8) { emitline("\\\""); return; }; + if (b == 92u8) { emitline("\\\\"); return; }; + if (b < 32u8) { + emitline("\\x"); + let hi: u8 = b >> 4u8; + let lo: u8 = b & 15u8; + let bb: [2]u8; + if (hi < 10u8) { bb[0] = hi + 48u8; } + else { bb[0] = (hi - 10u8) + 97u8; }; + if (lo < 10u8) { bb[1] = lo + 48u8; } + else { bb[1] = (lo - 10u8) + 97u8; }; + os.write(1, bb.ptr, 2u64); + return; + }; + if (b >= 127u8) { + emitline("\\x"); + let hi: u8 = b >> 4u8; + let lo: u8 = b & 15u8; + let bb: [2]u8; + if (hi < 10u8) { bb[0] = hi + 48u8; } + else { bb[0] = (hi - 10u8) + 97u8; }; + if (lo < 10u8) { bb[1] = lo + 48u8; } + else { bb[1] = (lo - 10u8) + 97u8; }; + os.write(1, bb.ptr, 2u64); + return; + }; + let bb: [1]u8; + bb[0] = b; + os.write(1, bb.ptr, 1u64); +}; + +// emitletdataw — DATAW directive per top-level `let` global. +// 8B scalar with int/rune/bool/nil literal init (or no init). +// 16B str with no init (or `nil` / `""`) — zero header; the +// program must assign a real strlit at runtime before +// using .ptr / .len. +// Non-literal scalar inits and non-empty strlit inits are skipped +// so the link surfaces an undefined-symbol error. fn emitletdataw(c: *cgen, file: *node) void = { let d: *node = file.list; for (d != nil) { if (d.kind == nkind.N_LET) { let nm: str = d.str; if (nm.len > 0) { - if (letscalarok(c, d)) { + let sz: i32 = letemitsize(c, d); + if (sz == 8) { let v: u64 = 0u64; let ok: bool = true; if (d.rhs != nil) { @@ -9073,37 +9181,35 @@ fn emitletdataw(c: *cgen, file: *node) void = { for (i < 8) { let b: u8 = (n & 255u64): u8; n = n >> 8u64; - if (b == 34u8) { emitline("\\\""); } - else { if (b == 92u8) { emitline("\\\\"); } - else { - if (b < 32u8) { - emitline("\\x"); - let hi: u8 = b >> 4u8; - let lo: u8 = b & 15u8; - let bb: [2]u8; - if (hi < 10u8) { bb[0] = hi + 48u8; } - else { bb[0] = (hi - 10u8) + 97u8; }; - if (lo < 10u8) { bb[1] = lo + 48u8; } - else { bb[1] = (lo - 10u8) + 97u8; }; - os.write(1, bb.ptr, 2u64); - } else { - if (b >= 127u8) { - emitline("\\x"); - let hi: u8 = b >> 4u8; - let lo: u8 = b & 15u8; - let bb: [2]u8; - if (hi < 10u8) { bb[0] = hi + 48u8; } - else { bb[0] = (hi - 10u8) + 97u8; }; - if (lo < 10u8) { bb[1] = lo + 48u8; } - else { bb[1] = (lo - 10u8) + 97u8; }; - os.write(1, bb.ptr, 2u64); - } else { - let bb: [1]u8; - bb[0] = b; - os.write(1, bb.ptr, 1u64); - }; - }; - };}; + emitdatawbyte(b); + i += 1; + }; + emitline("\"\n"); + }; + }; + if (sz == 16) { + let ok: bool = true; + if (d.rhs != nil) { + let r: *node = d.rhs; + for (r != nil) { + if (r.kind != nkind.N_CAST) { break; }; + r = r.lhs; + }; + ok = false; + if (r != nil) { + if (r.kind == nkind.N_NIL) { ok = true; }; + if (r.kind == nkind.N_STRLIT) { + if (r.str.len == 0) { ok = true; }; + }; + }; + }; + if (ok) { + emitline("DATAW "); + emitsymname(c, nm); + emitline("(SB),\""); + let i: i32 = 0; + for (i < 16) { + emitdatawbyte(0u8); i += 1; }; emitline("\"\n"); diff --git a/selfhost/cmd/wcc/cgen.ww b/selfhost/cmd/wcc/cgen.ww index 87d2430b..57b5fecc 100644 --- a/selfhost/cmd/wcc/cgen.ww +++ b/selfhost/cmd/wcc/cgen.ww @@ -332,11 +332,14 @@ type cgen = struct { }; // Top-level mutable `let` registry. Mirrors cmd/w6c/cgen.c LetVar. -// Populated alongside modents; consulted by cgassign and the TK_AMP -// path so reads/writes hit a RIP-relative DATAW slot instead of being -// silently dropped. Only scalar (≤8B) types make the list. +// Populated alongside modents; consulted by cgassign, cgdot, cgident +// and the TK_AMP path so reads/writes hit a RIP-relative DATAW slot +// instead of being silently dropped. tnode is the declared type AST +// node — needed to distinguish scalar (8B) from str (16B) globals +// when picking the load/store sequence. type letvar = struct { name: str, + tnode: *node, lvnext: *letvar, }; @@ -560,8 +563,7 @@ fn internstrlit(c: *cgen, bytes: str) str = { // letscalarprim — recognise the bare type-name keywords whose values // fit in an 8-byte .data slot and load back with a plain MOVQ. Float // types deliberately excluded; they need MOVSS/MOVSD. Mirrors C -// `let_scalar_ok` for the unwrapped TY_* enumeration. Pointer types -// (N_TPTR) handle separately at the callsite. +// `let_emit_size`'s 8-byte branch. fn letscalarprim(nm: str) bool = { if (streq(nm, "bool")) { return true; }; if (streq(nm, "rune")) { return true; }; @@ -579,24 +581,25 @@ fn letscalarprim(nm: str) bool = { return false; }; -// letscalarok — true iff the N_LET decl's declared type lands in the -// scalar set, walking type aliases. Pointer types are always ok. -fn letscalarok(c: *cgen, d: *node) bool = { - if (d == nil) { return false; }; +// letemitsize — slot size in bytes for a top-level `let`, or 0 if +// the type isn't yet supported as a writable global. Walks type +// aliases so byte output matches C cgen, which resolves Type kinds. +// 8 → scalar (literal init supported) +// 16 → str (only zero-init / nil / "" supported) +fn letemitsize(c: *cgen, d: *node) i32 = { + if (d == nil) { return 0; }; let t: *node = d.lhs; for (t != nil) { - if (t.kind == nkind.N_TPTR) { return true; }; - if (t.kind != nkind.N_TNAME) { return false; }; + if (t.kind == nkind.N_TPTR) { return 8; }; + if (t.kind != nkind.N_TNAME) { return 0; }; let nm: str = t.str; - if (letscalarprim(nm)) { return true; }; - // Resolve a `type x = y;` alias and look again. C cgen - // works on the resolved Type, so byte output diverges - // here without the walk. + if (letscalarprim(nm)) { return 8; }; + if (streq(nm, "str")) { return 16; }; let next: *node = aliaslookup(c, nm); - if (next == nil) { return false; }; + if (next == nil) { return 0; }; t = next; }; - return false; + return 0; }; fn collectlets(c: *cgen, file: *node) void = { @@ -607,9 +610,10 @@ fn collectlets(c: *cgen, file: *node) void = { if (d.kind == nkind.N_LET) { let nm: str = d.str; if (nm.len > 0) { - if (letscalarok(c, d)) { - let lv: *letvar = amalloc(c.a, 32u64): *letvar; + if (letemitsize(c, d) > 0) { + let lv: *letvar = amalloc(c.a, 48u64): *letvar; lv.name = nm; + lv.tnode = d.lhs; lv.lvnext = c.lets; c.lets = lv; }; @@ -628,18 +632,79 @@ fn isletvar(c: *cgen, name: str) bool = { return false; }; -// emitletdataw — DATAW directive per top-level scalar `let`. Same -// 8-byte LE byte encoding as emitdefconstants; only the directive -// keyword differs ("DATAW" vs "DATA"). Mirrors cmd/w6c/cgen.c -// emit_lets — w6a routes DATAW into a writable .data section, and -// w6l covers it with a second R+W PT_LOAD. +// letvarisstr — is the named top-level let a str global? Resolves +// aliases to mirror C cgen's `let_isstr`. Used by cgident/cgdot/ +// cgassign to pick the (LEAQ, MOVQ, MOVQ) sequence over the bare +// MOVQ scalar load. +fn letvarisstr(c: *cgen, name: str) bool = { + let lv: *letvar = c.lets; + for (lv != nil) { + if (streq(lv.name, name)) { + let t: *node = lv.tnode; + for (t != nil) { + if (t.kind != nkind.N_TNAME) { return false; }; + let nm: str = t.str; + if (streq(nm, "str")) { return true; }; + let nx: *node = aliaslookup(c, nm); + if (nx == nil) { return false; }; + t = nx; + }; + return false; + }; + lv = lv.lvnext; + }; + return false; +}; + +// emitdatawbyte — write one byte of an asm string literal using +// the same escape rules as emitdefconstants / emitdatasection. +fn emitdatawbyte(b: u8) void = { + if (b == 34u8) { emitline("\\\""); return; }; + if (b == 92u8) { emitline("\\\\"); return; }; + if (b < 32u8) { + emitline("\\x"); + let hi: u8 = b >> 4u8; + let lo: u8 = b & 15u8; + let bb: [2]u8; + if (hi < 10u8) { bb[0] = hi + 48u8; } + else { bb[0] = (hi - 10u8) + 97u8; }; + if (lo < 10u8) { bb[1] = lo + 48u8; } + else { bb[1] = (lo - 10u8) + 97u8; }; + os.write(1, bb.ptr, 2u64); + return; + }; + if (b >= 127u8) { + emitline("\\x"); + let hi: u8 = b >> 4u8; + let lo: u8 = b & 15u8; + let bb: [2]u8; + if (hi < 10u8) { bb[0] = hi + 48u8; } + else { bb[0] = (hi - 10u8) + 97u8; }; + if (lo < 10u8) { bb[1] = lo + 48u8; } + else { bb[1] = (lo - 10u8) + 97u8; }; + os.write(1, bb.ptr, 2u64); + return; + }; + let bb: [1]u8; + bb[0] = b; + os.write(1, bb.ptr, 1u64); +}; + +// emitletdataw — DATAW directive per top-level `let` global. +// 8B scalar with int/rune/bool/nil literal init (or no init). +// 16B str with no init (or `nil` / `""`) — zero header; the +// program must assign a real strlit at runtime before +// using .ptr / .len. +// Non-literal scalar inits and non-empty strlit inits are skipped +// so the link surfaces an undefined-symbol error. fn emitletdataw(c: *cgen, file: *node) void = { let d: *node = file.list; for (d != nil) { if (d.kind == nkind.N_LET) { let nm: str = d.str; if (nm.len > 0) { - if (letscalarok(c, d)) { + let sz: i32 = letemitsize(c, d); + if (sz == 8) { let v: u64 = 0u64; let ok: bool = true; if (d.rhs != nil) { @@ -666,37 +731,35 @@ fn emitletdataw(c: *cgen, file: *node) void = { for (i < 8) { let b: u8 = (n & 255u64): u8; n = n >> 8u64; - if (b == 34u8) { emitline("\\\""); } - else { if (b == 92u8) { emitline("\\\\"); } - else { - if (b < 32u8) { - emitline("\\x"); - let hi: u8 = b >> 4u8; - let lo: u8 = b & 15u8; - let bb: [2]u8; - if (hi < 10u8) { bb[0] = hi + 48u8; } - else { bb[0] = (hi - 10u8) + 97u8; }; - if (lo < 10u8) { bb[1] = lo + 48u8; } - else { bb[1] = (lo - 10u8) + 97u8; }; - os.write(1, bb.ptr, 2u64); - } else { - if (b >= 127u8) { - emitline("\\x"); - let hi: u8 = b >> 4u8; - let lo: u8 = b & 15u8; - let bb: [2]u8; - if (hi < 10u8) { bb[0] = hi + 48u8; } - else { bb[0] = (hi - 10u8) + 97u8; }; - if (lo < 10u8) { bb[1] = lo + 48u8; } - else { bb[1] = (lo - 10u8) + 97u8; }; - os.write(1, bb.ptr, 2u64); - } else { - let bb: [1]u8; - bb[0] = b; - os.write(1, bb.ptr, 1u64); - }; - }; - };}; + emitdatawbyte(b); + i += 1; + }; + emitline("\"\n"); + }; + }; + if (sz == 16) { + let ok: bool = true; + if (d.rhs != nil) { + let r: *node = d.rhs; + for (r != nil) { + if (r.kind != nkind.N_CAST) { break; }; + r = r.lhs; + }; + ok = false; + if (r != nil) { + if (r.kind == nkind.N_NIL) { ok = true; }; + if (r.kind == nkind.N_STRLIT) { + if (r.str.len == 0) { ok = true; }; + }; + }; + }; + if (ok) { + emitline("DATAW "); + emitsymname(c, nm); + emitline("(SB),\""); + let i: i32 = 0; + for (i < 16) { + emitdatawbyte(0u8); i += 1; }; emitline("\"\n"); diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index b1e03a9b..f287fd1e 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -401,11 +401,20 @@ fn cgident(c: *cgen, n: *node) void = { return; }; // Top-level mutable `let` — RIP-relative load from its DATAW - // slot. Mirrors C cgen's catch-all `MOVQ masym(s), AX`. Names - // that aren't lets either (typos, never-defined) flow through - // here too in C; the divergence today is bounded to scalar lets, - // which is what `isletvar` gates on. + // slot. Mirrors C cgen's catch-all `MOVQ masym(s), AX` for + // scalar lets, plus the (LEAQ, MOVQ, MOVQ) sequence for str + // globals so both ptr and len land in (AX, BX). Names that + // aren't lets either (typos, never-defined) drop through to + // the silent return. if (isletvar(c, nm)) { + if (letvarisstr(c, nm)) { + emitline("\tLEAQ\t"); + emitsymname(c, nm); + emitline("(SB), CX\n"); + emitline("\tMOVQ\t(CX), AX\n"); + emitline("\tMOVQ\t8(CX), BX\n"); + return; + }; emitline("\tMOVQ\t"); emitsymname(c, nm); emitline("(SB), AX\n"); @@ -871,6 +880,30 @@ fn cgdot(c: *cgen, n: *node) void = { }; }; }; + // Top-level str global field access — load .ptr / .len via + // &name(SB) into CX, then MOVQ delta(CX), AX. Without this + // the module-qualified fallback below would mis-emit + // `MOVQ (SB), AX`. + if (lhs != nil) { + if (lhs.kind == nkind.N_IDENT) { + if (isletvar(c, lhs.str)) { + if (letvarisstr(c, lhs.str)) { + let delta: i32 = -1; + if (streq(fld, "ptr")) { delta = 0; }; + if (streq(fld, "len")) { delta = 8; }; + if (delta >= 0) { + emitline("\tLEAQ\t"); + emitsymname(c, lhs.str); + emitline("(SB), CX\n"); + emitline("\tMOVQ\t"); + emitdispreg(delta: i64, "CX"); + emitline(", AX\n"); + return; + }; + }; + }; + }; + }; // Module-qualified value reference: `mod.name` where `mod` // is nkind.N_IDENT bound as skind.SK_USE and the leaf isn't a local. // Treat as a SB symbol — `MOVQ leaf(SB), AX`. Same fallback @@ -1546,10 +1579,20 @@ fn cgassign(c: *cgen, n: *node) void = { if (off == 0) { // Top-level let target: RIP-relative store // for `=`, or load→combine→store for the - // compound forms. + // compound forms. For a str global, take its + // address into CX and store both halves; the + // asm has no `name+8(SB)` operand form. if (!isletvar(c, nm)) { return; }; cgexpr(c, n.rhs); if (n.op == tkind.TK_ASSIGN) { + if (letvarisstr(c, nm)) { + emitline("\tLEAQ\t"); + emitsymname(c, nm); + emitline("(SB), CX\n"); + emitline("\tMOVQ\tAX, (CX)\n"); + emitline("\tMOVQ\tBX, 8(CX)\n"); + return; + }; emitline("\tMOVQ\tAX, "); emitsymname(c, nm); emitline("(SB)\n"); diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 218b5589..b84d1e49 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -6129,11 +6129,20 @@ fn cgident(c: *cgen, n: *node) void = { return; }; // Top-level mutable `let` — RIP-relative load from its DATAW - // slot. Mirrors C cgen's catch-all `MOVQ masym(s), AX`. Names - // that aren't lets either (typos, never-defined) flow through - // here too in C; the divergence today is bounded to scalar lets, - // which is what `isletvar` gates on. + // slot. Mirrors C cgen's catch-all `MOVQ masym(s), AX` for + // scalar lets, plus the (LEAQ, MOVQ, MOVQ) sequence for str + // globals so both ptr and len land in (AX, BX). Names that + // aren't lets either (typos, never-defined) drop through to + // the silent return. if (isletvar(c, nm)) { + if (letvarisstr(c, nm)) { + emitline("\tLEAQ\t"); + emitsymname(c, nm); + emitline("(SB), CX\n"); + emitline("\tMOVQ\t(CX), AX\n"); + emitline("\tMOVQ\t8(CX), BX\n"); + return; + }; emitline("\tMOVQ\t"); emitsymname(c, nm); emitline("(SB), AX\n"); @@ -6599,6 +6608,30 @@ fn cgdot(c: *cgen, n: *node) void = { }; }; }; + // Top-level str global field access — load .ptr / .len via + // &name(SB) into CX, then MOVQ delta(CX), AX. Without this + // the module-qualified fallback below would mis-emit + // `MOVQ (SB), AX`. + if (lhs != nil) { + if (lhs.kind == nkind.N_IDENT) { + if (isletvar(c, lhs.str)) { + if (letvarisstr(c, lhs.str)) { + let delta: i32 = -1; + if (streq(fld, "ptr")) { delta = 0; }; + if (streq(fld, "len")) { delta = 8; }; + if (delta >= 0) { + emitline("\tLEAQ\t"); + emitsymname(c, lhs.str); + emitline("(SB), CX\n"); + emitline("\tMOVQ\t"); + emitdispreg(delta: i64, "CX"); + emitline(", AX\n"); + return; + }; + }; + }; + }; + }; // Module-qualified value reference: `mod.name` where `mod` // is nkind.N_IDENT bound as skind.SK_USE and the leaf isn't a local. // Treat as a SB symbol — `MOVQ leaf(SB), AX`. Same fallback @@ -7274,10 +7307,20 @@ fn cgassign(c: *cgen, n: *node) void = { if (off == 0) { // Top-level let target: RIP-relative store // for `=`, or load→combine→store for the - // compound forms. + // compound forms. For a str global, take its + // address into CX and store both halves; the + // asm has no `name+8(SB)` operand form. if (!isletvar(c, nm)) { return; }; cgexpr(c, n.rhs); if (n.op == tkind.TK_ASSIGN) { + if (letvarisstr(c, nm)) { + emitline("\tLEAQ\t"); + emitsymname(c, nm); + emitline("(SB), CX\n"); + emitline("\tMOVQ\tAX, (CX)\n"); + emitline("\tMOVQ\tBX, 8(CX)\n"); + return; + }; emitline("\tMOVQ\tAX, "); emitsymname(c, nm); emitline("(SB)\n"); @@ -8739,11 +8782,14 @@ type cgen = struct { }; // Top-level mutable `let` registry. Mirrors cmd/w6c/cgen.c LetVar. -// Populated alongside modents; consulted by cgassign and the TK_AMP -// path so reads/writes hit a RIP-relative DATAW slot instead of being -// silently dropped. Only scalar (≤8B) types make the list. +// Populated alongside modents; consulted by cgassign, cgdot, cgident +// and the TK_AMP path so reads/writes hit a RIP-relative DATAW slot +// instead of being silently dropped. tnode is the declared type AST +// node — needed to distinguish scalar (8B) from str (16B) globals +// when picking the load/store sequence. type letvar = struct { name: str, + tnode: *node, lvnext: *letvar, }; @@ -8967,8 +9013,7 @@ fn internstrlit(c: *cgen, bytes: str) str = { // letscalarprim — recognise the bare type-name keywords whose values // fit in an 8-byte .data slot and load back with a plain MOVQ. Float // types deliberately excluded; they need MOVSS/MOVSD. Mirrors C -// `let_scalar_ok` for the unwrapped TY_* enumeration. Pointer types -// (N_TPTR) handle separately at the callsite. +// `let_emit_size`'s 8-byte branch. fn letscalarprim(nm: str) bool = { if (streq(nm, "bool")) { return true; }; if (streq(nm, "rune")) { return true; }; @@ -8986,24 +9031,25 @@ fn letscalarprim(nm: str) bool = { return false; }; -// letscalarok — true iff the N_LET decl's declared type lands in the -// scalar set, walking type aliases. Pointer types are always ok. -fn letscalarok(c: *cgen, d: *node) bool = { - if (d == nil) { return false; }; +// letemitsize — slot size in bytes for a top-level `let`, or 0 if +// the type isn't yet supported as a writable global. Walks type +// aliases so byte output matches C cgen, which resolves Type kinds. +// 8 → scalar (literal init supported) +// 16 → str (only zero-init / nil / "" supported) +fn letemitsize(c: *cgen, d: *node) i32 = { + if (d == nil) { return 0; }; let t: *node = d.lhs; for (t != nil) { - if (t.kind == nkind.N_TPTR) { return true; }; - if (t.kind != nkind.N_TNAME) { return false; }; + if (t.kind == nkind.N_TPTR) { return 8; }; + if (t.kind != nkind.N_TNAME) { return 0; }; let nm: str = t.str; - if (letscalarprim(nm)) { return true; }; - // Resolve a `type x = y;` alias and look again. C cgen - // works on the resolved Type, so byte output diverges - // here without the walk. + if (letscalarprim(nm)) { return 8; }; + if (streq(nm, "str")) { return 16; }; let next: *node = aliaslookup(c, nm); - if (next == nil) { return false; }; + if (next == nil) { return 0; }; t = next; }; - return false; + return 0; }; fn collectlets(c: *cgen, file: *node) void = { @@ -9014,9 +9060,10 @@ fn collectlets(c: *cgen, file: *node) void = { if (d.kind == nkind.N_LET) { let nm: str = d.str; if (nm.len > 0) { - if (letscalarok(c, d)) { - let lv: *letvar = amalloc(c.a, 32u64): *letvar; + if (letemitsize(c, d) > 0) { + let lv: *letvar = amalloc(c.a, 48u64): *letvar; lv.name = nm; + lv.tnode = d.lhs; lv.lvnext = c.lets; c.lets = lv; }; @@ -9035,18 +9082,79 @@ fn isletvar(c: *cgen, name: str) bool = { return false; }; -// emitletdataw — DATAW directive per top-level scalar `let`. Same -// 8-byte LE byte encoding as emitdefconstants; only the directive -// keyword differs ("DATAW" vs "DATA"). Mirrors cmd/w6c/cgen.c -// emit_lets — w6a routes DATAW into a writable .data section, and -// w6l covers it with a second R+W PT_LOAD. +// letvarisstr — is the named top-level let a str global? Resolves +// aliases to mirror C cgen's `let_isstr`. Used by cgident/cgdot/ +// cgassign to pick the (LEAQ, MOVQ, MOVQ) sequence over the bare +// MOVQ scalar load. +fn letvarisstr(c: *cgen, name: str) bool = { + let lv: *letvar = c.lets; + for (lv != nil) { + if (streq(lv.name, name)) { + let t: *node = lv.tnode; + for (t != nil) { + if (t.kind != nkind.N_TNAME) { return false; }; + let nm: str = t.str; + if (streq(nm, "str")) { return true; }; + let nx: *node = aliaslookup(c, nm); + if (nx == nil) { return false; }; + t = nx; + }; + return false; + }; + lv = lv.lvnext; + }; + return false; +}; + +// emitdatawbyte — write one byte of an asm string literal using +// the same escape rules as emitdefconstants / emitdatasection. +fn emitdatawbyte(b: u8) void = { + if (b == 34u8) { emitline("\\\""); return; }; + if (b == 92u8) { emitline("\\\\"); return; }; + if (b < 32u8) { + emitline("\\x"); + let hi: u8 = b >> 4u8; + let lo: u8 = b & 15u8; + let bb: [2]u8; + if (hi < 10u8) { bb[0] = hi + 48u8; } + else { bb[0] = (hi - 10u8) + 97u8; }; + if (lo < 10u8) { bb[1] = lo + 48u8; } + else { bb[1] = (lo - 10u8) + 97u8; }; + os.write(1, bb.ptr, 2u64); + return; + }; + if (b >= 127u8) { + emitline("\\x"); + let hi: u8 = b >> 4u8; + let lo: u8 = b & 15u8; + let bb: [2]u8; + if (hi < 10u8) { bb[0] = hi + 48u8; } + else { bb[0] = (hi - 10u8) + 97u8; }; + if (lo < 10u8) { bb[1] = lo + 48u8; } + else { bb[1] = (lo - 10u8) + 97u8; }; + os.write(1, bb.ptr, 2u64); + return; + }; + let bb: [1]u8; + bb[0] = b; + os.write(1, bb.ptr, 1u64); +}; + +// emitletdataw — DATAW directive per top-level `let` global. +// 8B scalar with int/rune/bool/nil literal init (or no init). +// 16B str with no init (or `nil` / `""`) — zero header; the +// program must assign a real strlit at runtime before +// using .ptr / .len. +// Non-literal scalar inits and non-empty strlit inits are skipped +// so the link surfaces an undefined-symbol error. fn emitletdataw(c: *cgen, file: *node) void = { let d: *node = file.list; for (d != nil) { if (d.kind == nkind.N_LET) { let nm: str = d.str; if (nm.len > 0) { - if (letscalarok(c, d)) { + let sz: i32 = letemitsize(c, d); + if (sz == 8) { let v: u64 = 0u64; let ok: bool = true; if (d.rhs != nil) { @@ -9073,37 +9181,35 @@ fn emitletdataw(c: *cgen, file: *node) void = { for (i < 8) { let b: u8 = (n & 255u64): u8; n = n >> 8u64; - if (b == 34u8) { emitline("\\\""); } - else { if (b == 92u8) { emitline("\\\\"); } - else { - if (b < 32u8) { - emitline("\\x"); - let hi: u8 = b >> 4u8; - let lo: u8 = b & 15u8; - let bb: [2]u8; - if (hi < 10u8) { bb[0] = hi + 48u8; } - else { bb[0] = (hi - 10u8) + 97u8; }; - if (lo < 10u8) { bb[1] = lo + 48u8; } - else { bb[1] = (lo - 10u8) + 97u8; }; - os.write(1, bb.ptr, 2u64); - } else { - if (b >= 127u8) { - emitline("\\x"); - let hi: u8 = b >> 4u8; - let lo: u8 = b & 15u8; - let bb: [2]u8; - if (hi < 10u8) { bb[0] = hi + 48u8; } - else { bb[0] = (hi - 10u8) + 97u8; }; - if (lo < 10u8) { bb[1] = lo + 48u8; } - else { bb[1] = (lo - 10u8) + 97u8; }; - os.write(1, bb.ptr, 2u64); - } else { - let bb: [1]u8; - bb[0] = b; - os.write(1, bb.ptr, 1u64); - }; - }; - };}; + emitdatawbyte(b); + i += 1; + }; + emitline("\"\n"); + }; + }; + if (sz == 16) { + let ok: bool = true; + if (d.rhs != nil) { + let r: *node = d.rhs; + for (r != nil) { + if (r.kind != nkind.N_CAST) { break; }; + r = r.lhs; + }; + ok = false; + if (r != nil) { + if (r.kind == nkind.N_NIL) { ok = true; }; + if (r.kind == nkind.N_STRLIT) { + if (r.str.len == 0) { ok = true; }; + }; + }; + }; + if (ok) { + emitline("DATAW "); + emitsymname(c, nm); + emitline("(SB),\""); + let i: i32 = 0; + for (i < 16) { + emitdatawbyte(0u8); i += 1; }; emitline("\"\n"); diff --git a/test/wcc/630_let_global.c b/test/wcc/630_let_global.c index 09451a3e..80aee2f2 100644 --- a/test/wcc/630_let_global.c +++ b/test/wcc/630_let_global.c @@ -91,6 +91,28 @@ static const struct fixture fixtures[] = { "fn main() i32 = { return val: i32; };\n", 123, }, + { + "str-zeroinit", + /* `let msg: str;` zero-inits the {ptr,len} slot. Assigning + * a strlit at runtime updates both halves; .len then reads + * the stored length. */ + "let msg: str;\n" + "fn main() i32 = {\n" + "\tmsg = \"hello\";\n" + "\treturn msg.len: i32;\n" + "};\n", + 5, + }, + { + "str-reassign", + "let msg: str;\n" + "fn set(s: str) void = { msg = s; };\n" + "fn main() i32 = {\n" + "\tset(\"abcdefg\");\n" + "\treturn msg.len: i32;\n" + "};\n", + 7, + }, { NULL, NULL, 0 } };