From 8578ad0533e46efea3853290493b4fca9d8cc6c9 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Thu, 4 Jun 2026 19:02:41 +0900 Subject: [PATCH] =?UTF-8?q?wcc+w6c+w6c=5Fww:=20global=20tuple=20lets=20?= =?UTF-8?q?=E2=80=94=20DATA=20emit=20+=20element=20reads=20+=20len(g.N)=20?= =?UTF-8?q?(C-t3,=20#48)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Global tuple lets were WHOLLY unwired, silently: let_emit_size returned 0 so emit_lets SKIPPED the definition (no DATA, no diagnostic), then cstage's t.N read and #235 len arm read BP-frame garbage (localfind→0) while wwstage — with the tuple never in collectlets — mis-emitted the field index as a symbol (`MOVQ 0(SB), AX`). ken's #48 was the len() facet of this. Now: let_emit_size/letemitsize admit TY_TUPLE (slot-sum size, rides C-t0); emit_tuple_data/emittupledata lay the slot-format DATAW row — a scalar element one 8B LE word, a str element its 24B header slot with a DATAR ptr patch at the element's slot offset (the #18 [N]str per-element pattern; strlits pre-interned in element order) — and any element that doesn't reduce to an int/str literal dies LOUD instead of skipped. The t.N read and len arms gain the global base (LEAQ sym(SB) into CX, the struct-field-global pattern; wwstage's C5 len loud-stop graduates to the working path). A GLOBAL tuple as a first-class VALUE (`let q = g;`) loud-stops on both stages — pre-fix it byte-identically loaded word0 only and read a stale cursor for words 1+ (element reads are the supported surface). 941 grows the t3 rows: global element reads (str+i64 and packed u32,u32 incl. len(g.0)) + rejects (float-element init, whole-value use, pre-existing element-write anchor). 7/82 checks fail at the C-t2 parent (cs silent-garbage runtime, ww C5 build-fail, both rejects vacuous-or-absent). --- cmd/w6c/cgen.c | 203 +++++++++++++++- selfhost/cmd/w6c/main.combined.ww | 343 ++++++++++++++++++++++++++- selfhost/cmd/wcc/cgen.ww | 213 ++++++++++++++++- selfhost/cmd/wcc/cgenexpr.ww | 130 ++++++++++ selfhost/cmd/wwdump/main.combined.ww | 343 ++++++++++++++++++++++++++- test/wcc/941_tuple_slot_layout_run.c | 46 ++++ 6 files changed, 1262 insertions(+), 16 deletions(-) diff --git a/cmd/w6c/cgen.c b/cmd/w6c/cgen.c index 0d24aa0e..c8f8ec38 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -1080,6 +1080,14 @@ let_emit_size(Type *t) * loads/stores via cgindex. Mirror * of selfhost letemitsize's * N_TARRAY branch. */ + case TY_TUPLE: + return (int)u->size; /* C-t3 (#48): slot-sum size (C-t0). + * int/str-literal element init via + * emit_tuple_data; element reads via + * the N_DOT t.N global arm. Pre-C-t3 + * the 0 here SILENTLY skipped the + * definition and every read saw + * BP-frame garbage. */ default: return 0; } @@ -3505,6 +3513,20 @@ cgexpr(Cg *c, Node *n, Local *locals) goto ident_done; } } + /* C-t3 (#48, rule 7): a GLOBAL tuple as a first-class + * VALUE (`let q = g;` / `return g;` / `f(g)`) has no + * slot-to-cursor path (cg_tuple_slot_to_cursor is + * BP-relative) — pre-fix it fell to the scalar MOVQ + * below, loading word0 only, and the receive read a + * STALE cursor for words 1+. Element reads (g.N) + * are the supported surface. */ + if (let_islet(n->str)) { + Type *gu = type_chase_named(n->type); + if (gu && gu->kind == TY_TUPLE) + fatal("#48: global tuple as a " + "first-class value unwired " + "(element reads only; rule 7)"); + } if (let_islet(n->str) && (let_isstr(n->type) || let_isslice(n->type))) { /* Top-level str/slice global: load each word @@ -6681,9 +6703,24 @@ cgexpr(Cg *c, Node *n, Local *locals) idx--; } int off = localfind(locals, a->lhs->str); - ins2(c, A_MOVQ, - amem(D_BP, off + foff + 8), - areg(D_AX)); + /* C-t3 (#48): GLOBAL tuple base — + * pre-fix localfind's 0 read the .len + * word at foff+8(BP), stack garbage, + * SILENT. Twin of the N_DOT TY_TUPLE + * global arm. */ + if (off == 0 + && let_islet(a->lhs->str)) { + ins2(c, A_LEAQ, + masym(c, a->lhs->str), + areg(D_CX)); + ins2(c, A_MOVQ, + amem(D_CX, foff + 8), + areg(D_AX)); + } else { + ins2(c, A_MOVQ, + amem(D_BP, off + foff + 8), + areg(D_AX)); + } lendone = 1; } /* struct-field N_DOT (`len(s.field)`): the old @@ -9582,6 +9619,20 @@ cgexpr(Cg *c, Node *n, Local *locals) ? tp->type->under : tp->type; int op = fldloadop(tp->type, fsz); int off = localfind(locals, n->lhs->str); + /* C-t3 (#48): GLOBAL tuple base — LEAQ the + * mangled symbol into CX and read at CX+foff, + * the struct-field global pattern below. + * Pre-C-t3 localfind's 0 silently read the + * stack frame. */ + int base_reg = D_BP; + int base_disp = off; + if (off == 0 && let_islet(n->lhs->str)) { + ins2(c, A_LEAQ, + masym(c, n->lhs->str), + areg(D_CX)); + base_reg = D_CX; + base_disp = 0; + } /* f64/f32 tuple field must ride X0 via MOVSD/MOVSS; * the integer fldloadop left it in AX (#103 FACE Z). * Mirrors the struct-field float load at cgen.c:1462, @@ -9589,7 +9640,8 @@ cgexpr(Cg *c, Node *n, Local *locals) int tup_isf32 = 0; if (fld_isfloat(tp->type, &tup_isf32)) { int mov = tup_isf32 ? A_MOVSS : A_MOVSD; - ins2(c, mov, amem(D_BP, off + foff), + ins2(c, mov, + amem(base_reg, base_disp + foff), areg(D_X0)); break; } @@ -9599,15 +9651,22 @@ cgexpr(Cg *c, Node *n, Local *locals) * slice-rhs convention (#1/Phase 3 collapse). * UNLIKE the field arms there is no slice-element * sibling here, so the triple is hand-authored; - * base is BP (frame, not a target reg) so the - * canonical ptr/len/cap order has no clobber risk. */ + * base is BP (frame) or CX (global base — CX is + * written LAST so it survives the +0/+8 reads). */ if (fu && fu->kind == TY_STR) { - ins2(c, A_MOVQ, amem(D_BP, off + foff + 0), areg(D_AX)); - ins2(c, A_MOVQ, amem(D_BP, off + foff + 8), areg(D_BX)); - ins2(c, A_MOVQ, amem(D_BP, off + foff + 16), areg(D_CX)); + ins2(c, A_MOVQ, + amem(base_reg, base_disp + foff + 0), + areg(D_AX)); + ins2(c, A_MOVQ, + amem(base_reg, base_disp + foff + 8), + areg(D_BX)); + ins2(c, A_MOVQ, + amem(base_reg, base_disp + foff + 16), + areg(D_CX)); break; } - ins2(c, op, amem(D_BP, off + foff), areg(D_AX)); + ins2(c, op, amem(base_reg, base_disp + foff), + areg(D_AX)); } break; } @@ -13333,6 +13392,88 @@ emit_strarray_data(FILE *out, Cg *c, const char *directive, return 1; } +/* emit_tuple_data — module-level `let g: (T0, T1, ...) = (v0, v1, ...);` + * static init (C-t3, #48). Slot layout (C-t0): a scalar element is one + * 8B LE word, a str element its 24B header slot (8 zero ptr placeholder + * + LE len + 8 zero cap) with a DATAR patching the ptr word at the + * element's slot offset — the per-element twin of the scalar-str-global + * arm, offset like emit_strarray_data's rows. Elements must reduce to + * int (fold_int_literal) or str literals; anything else returns 0 and + * the caller loud-stops (rule 7 — pre-C-t3 the whole definition was + * SILENTLY skipped and reads saw BP-frame garbage). rhs == NULL + * zero-inits the slot. */ +static int +emit_tuple_data(FILE *out, Cg *c, const char *name, const char *module, + Type *t, Node *rhs) +{ + Type *u = type_unwrap(t); + if (u == NULL || u->kind != TY_TUPLE) return 0; + const char *sym = mod_mangle_value(c, name, module); + if (rhs == NULL) { + fprintf(out, "DATAW %s(SB),\"", sym); + for (int i = 0; i < (int)u->size; i++) + emit_data_byte(out, 0); + fputs("\"\n", out); + return 1; + } + if (rhs->kind != N_TUPLE) return 0; + /* Validate: every cast-peeled element folds (int) or is a strlit + * in a str slot. Two-pass so a partial row never reaches the + * output (emit_array_data precedent). */ + Tparam *tp = u->params; + for (Node *e = rhs->list; e; e = e->next, tp = tp ? tp->next : NULL) { + Node *ev = e; + while (ev && ev->kind == N_CAST) ev = ev->lhs; + if (ev == NULL) return 0; + int wide = tp && (type_isstr(tp->type) + || type_isslice(tp->type)); + if (wide) { + if (ev->kind != N_STRLIT) return 0; + continue; + } + u64 v; + if (!fold_int_literal(ev, &v)) return 0; + } + fprintf(out, "DATAW %s(SB),\"", sym); + tp = u->params; + for (Node *e = rhs->list; e; e = e->next, tp = tp ? tp->next : NULL) { + Node *ev = e; + while (ev && ev->kind == N_CAST) ev = ev->lhs; + int wide = tp && (type_isstr(tp->type) + || type_isslice(tp->type)); + if (wide) { + u64 v = ev->strlen; + 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)); + for (int i = 16; i < (int)ty_str->size; i++) + emit_data_byte(out, 0); + continue; + } + u64 v = 0; + (void)fold_int_literal(ev, &v); + for (int i = 0; i < 8; i++) + emit_data_byte(out, (u8)((v >> (i * 8)) & 0xff)); + } + fputs("\"\n", out); + int foff = 0; + tp = u->params; + for (Node *e = rhs->list; e; e = e->next, tp = tp ? tp->next : NULL) { + Node *ev = e; + while (ev && ev->kind == N_CAST) ev = ev->lhs; + int wide = tp && (type_isstr(tp->type) + || type_isslice(tp->type)); + if (wide && ev->strlen > 0) { + const char *lab = intern_strlit(c, ev->str, + ev->strlen); + fprintf(out, "DATAR %s+%d(SB),%s(SB)\n", + sym, foff, lab); + } + foff += wide ? (int)ty_str->size : 8; + } + return 1; +} + /* emit_array_data — opens DATA/DATAW prefix on validate success, then * emits payload. Two-pass keeps emit-on-failure from emitting partial * bytes (would corrupt the asm if rhs reduces partway through). */ @@ -13441,6 +13582,26 @@ emit_lets(Cg *c, FILE *out, Node *file) d->str, d->module, d->type, d->rhs); continue; } + /* C-t3 (#48): tuple global — slot-laid DATAW row (+ DATAR + * ptr patches for str elements). Unsupported element inits + * die LOUD; pre-C-t3 the whole definition was silently + * skipped (no DATA, no diagnostic) and reads saw BP-frame + * garbage. */ + { + Type *tu = type_unwrap(d->type); + if (tu != NULL && tu->kind == TY_TUPLE) { + Node *tr = d->rhs; + while (tr != NULL && tr->kind == N_CAST) + tr = tr->lhs; + if (!emit_tuple_data(out, c, d->str, + d->module, d->type, tr)) + fatal("global tuple let `%s`: " + "unsupported element init " + "(int/str literals only; rule 7)", + d->str); + continue; + } + } /* #129 A.2: gate `!let_isstruct` so an 8B struct lit * (`struct { i32, i32 }`, `struct { f32, f32 }`, …) does * NOT short-circuit through the scalar 8B `fold_int_literal` @@ -13696,6 +13857,28 @@ let_pre_intern(Cg *c, Node *file) continue; } } + /* C-t3 (#48): tuple global — pre-intern str-element literals + * in element order so emit_tuple_data's DATAR rows find + * their _S_ rodata rows (the #18 array-arm pattern). */ + if (u != NULL && u->kind == TY_TUPLE + && r != NULL && r->kind == N_TUPLE) { + Tparam *tp = u->params; + for (Node *e = r->list; e; + e = e->next, tp = tp ? tp->next : NULL) { + Node *ev = e; + while (ev && ev->kind == N_CAST) + ev = ev->lhs; + if (ev == NULL || ev->kind != N_STRLIT) + continue; + if (!(tp && (type_isstr(tp->type) + || type_isslice(tp->type)))) + continue; + if (ev->strlen > 0) + (void)intern_strlit(c, ev->str, + ev->strlen); + } + continue; + } if (let_emit_size(d->type) != (int)ty_str->size) continue; if (r == NULL || r->kind != N_STRLIT) continue; if (r->strlen == 0) continue; diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index d3f00c03..f54336ab 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -21180,6 +21180,24 @@ fn cgident(c: *cgen, n: *node) void = { // (AX, BX[, CX]). Names that aren't lets either (typos, // never-defined) drop through to the silent return. if (isletvar(c, nm)) { + // C-t3 (#48, rule 7): a GLOBAL tuple as a first-class VALUE + // (`let q = g;` / `return g;` / `f(g)`) has no slot-to-cursor + // path (cgtupleslottocursor is BP-relative) — pre-fix it fell + // to the scalar MOVQ below, loading word0 only, and the + // receive read a STALE cursor for words 1+. Element reads + // (g.N) are the supported surface. Mirrors the cstage cgexpr + // non-local ident guard. + let gtt: *node = letvartnode(c, nm); + for (gtt != nil && gtt.kind == nkind.N_TNAME) { + gtt = aliaslookup(c, gtt.str); + }; + if (gtt != nil) { + if (gtt.kind == nkind.N_TTUPLE) { + let mgt: str = "#48: global tuple as a first-class value unwired (element reads only; rule 7)\n"; + os.write(2, mgt.ptr, mgt.len: u64); + os.exit(1); + }; + }; let isstr: bool = letvarisstr(c, nm); let issl: bool = letvarisslice(c, nm); if (isstr || issl) { @@ -23082,6 +23100,79 @@ fn cgdot(c: *cgen, n: *node) void = { }; }; }; + // Top-level TUPLE global positional read (C-t3, #48): `g.N` — + // LEAQ name(SB) into CX, then load at the element's SLOT offset + // (C-t0 layout), the element's natural width. Mirrors the local + // N_TTUPLE arm above and cstage's N_DOT TY_TUPLE global base. + // Pre-C-t3 the tuple global wasn't in collectlets at all (no + // DATA) and the module-leaf fallback mis-emitted the field index + // as a symbol (`MOVQ 0(SB), AX`). + if (lhs != nil) { + if (lhs.kind == nkind.N_IDENT) { + let gtt: *node = letvartnode(c, lhs.str); + for (gtt != nil && gtt.kind == nkind.N_TNAME) { + gtt = aliaslookup(c, gtt.str); + }; + if (gtt != nil) { + if (gtt.kind == nkind.N_TTUPLE) { + let gidx: i32 = fldnumidx(fld); + if (gidx >= 0) { + let gtp: *node = gtt.list; + let gfoff: i32 = 0; + let gi: i32 = 0; + for (gi < gidx) { + if (gtp == nil) { gi = gidx; } + else { + gfoff += slotsize(c, gtp.lhs); + gtp = gtp.next; + gi += 1; + }; + }; + if (gtp != nil) { + let gpt: *node = gtp.lhs; + emitline("\tLEAQ\t"); + emitsymname(c, lhs.str); + emitline("(SB), CX\n"); + if (isstrtype(c, gpt)) { + // CX (the base) is written LAST so + // it survives the +0/+8 reads. + emitline("\tMOVQ\t"); + emitdispreg((gfoff + 0): i64, "CX"); + emitline(", AX\n"); + emitline("\tMOVQ\t"); + emitdispreg((gfoff + 8): i64, "CX"); + emitline(", BX\n"); + emitline("\tMOVQ\t"); + emitdispreg((gfoff + 16): i64, "CX"); + emitline(", CX\n"); + return; + }; + if (isfloattype(c, gpt)) { + let mov: str = "MOVSD"; + if (isf32type(c, gpt)) { mov = "MOVSS"; }; + emitline("\t"); + emitline(mov); + emitline("\t"); + emitdispreg(gfoff: i64, "CX"); + emitline(", X0\n"); + return; + }; + let gnsz: i32 = 8; + let gpti: *tinfo = gpt.type_: *tinfo; + if (gpti != nil) { gnsz = gpti.size: i32; }; + let gop: str = tnodeloadop(c, gpt, gnsz); + emitline("\t"); + emitline(gop); + emitline("\t"); + emitdispreg(gfoff: i64, "CX"); + emitline(", AX\n"); + return; + }; + }; + }; + }; + }; + }; // Top-level struct global field read — LEAQ name(SB), CX then // load at fi.foff(CX). Mirrors the local "Direct struct local" // branch above, swapping the BP frame slot for the global VA. @@ -25938,6 +26029,45 @@ fn cgcall(c: *cgen, n: *node) void = { }; }; }; + // C-t3 (#48): GLOBAL tuple len(g.N) — + // LEAQ name(SB) into CX, .len word at + // the SLOT offset + 8. Graduates the + // C5 loud-stop this shape previously + // hit; twin of the cgdot global-tuple + // arm and cstage's #235 global base. + if (lc == nil) { + let gtn: *node = letvartnode(c, a.lhs.str); + for (gtn != nil && gtn.kind == nkind.N_TNAME) { + gtn = aliaslookup(c, gtn.str); + }; + if (gtn != nil) { + if (gtn.kind == nkind.N_TTUPLE) { + let gidx: i32 = fldnumidx(a.str); + if (gidx >= 0) { + let gtp: *node = gtn.list; + let gfoff: i32 = 0; + let gi: i32 = 0; + for (gi < gidx) { + if (gtp == nil) { gi = gidx; } + else { + gfoff += slotsize(c, gtp.lhs); + gtp = gtp.next; + gi += 1; + }; + }; + if (gtp != nil) { + emitline("\tLEAQ\t"); + emitsymname(c, a.lhs.str); + emitline("(SB), CX\n"); + emitline("\tMOVQ\t"); + emitdispreg((gfoff + 8): i64, "CX"); + emitline(", AX\n"); + return; + }; + }; + }; + }; + }; // struct-field N_DOT (`len(s.field)`): the // old fallback returned .ptr as the length. // Falls to the resolver route below. @@ -35519,6 +35649,27 @@ fn letemitsize(c: *cgen, d: *node) i32 = { }; return alen * esz; }; + // C-t3 (#48): tuple global — per-element slot sum (C-t0 + // layout: a str/slice its header, everything else one 8B + // eightbyte). Mirrors cstage let_emit_size TY_TUPLE (u->size, + // the checker slot sum). Pre-C-t3 the 0 here kept tuple + // globals out of collectlets entirely — no DATA emitted, and + // the module-leaf fallback mis-emitted the field index as a + // symbol (`MOVQ 0(SB), AX`). + if (t.kind == nkind.N_TTUPLE) { + let tsum: i32 = 0; + let p: *node = t.list; + for (p != nil) { + let et: *node = p.lhs; + if (isstrtype(c, et) || isslicetype(c, et)) { + tsum += (tyslicesize(): i32); + } else { + tsum += 8; + }; + p = p.next; + }; + return tsum; + }; if (t.kind != nkind.N_TNAME) { return 0; }; let nm: str = t.str; if (letscalarprim(nm)) { return 8; }; @@ -35853,6 +36004,42 @@ export fn letpreintern(c: *cgen, file: *node) void = { }; }; }; + // C-t3 (#48): tuple global — pre-intern str-element + // literals in element order so emitletdataw's tuple + // arm's DATAR rows find their _S_ rodata rows (the + // #18 array-arm pattern; cstage let_pre_intern twin). + if (!handled && r != nil && d.lhs != nil) { + if (r.kind == nkind.N_TUPLE) { + let tlt: *node = d.lhs; + for (tlt != nil && tlt.kind == nkind.N_TNAME) { + tlt = aliaslookup(c, tlt.str); + }; + if (tlt != nil) { + if (tlt.kind == nkind.N_TTUPLE) { + handled = true; + let tp: *node = tlt.list; + let e: *node = r.list; + for (e != nil && tp != nil) { + let et: *node = tp.lhs; + let ev: *node = e; + for (ev != nil && ev.kind == nkind.N_CAST) { + ev = ev.lhs; + }; + if (ev != nil) { + if (ev.kind == nkind.N_STRLIT + && (isstrtype(c, et) || isslicetype(c, et))) { + if (ev.str.len > 0) { + internstrlit(c, ev.str); + }; + }; + }; + e = e.next; + tp = tp.next; + }; + }; + }; + }; + }; if (!handled) { let sz: i32 = letemitsize(c, d); // #43: route the str-let gate through primtypesize so @@ -36661,6 +36848,128 @@ fn emitslicedata(c: *cgen, name: str, module: str, slt: *tinfo, emitline(".d(SB)\n"); }; +// emittupledata — module-level `let g: (T0, T1, ...) = (v0, ...);` +// static init (C-t3, #48). Slot layout (C-t0): a scalar element is one +// 8B LE word, a str element its 24B header slot (8 zero ptr placeholder +// + LE len + 8 zero cap) with a DATAR patching the ptr word at the +// element's slot offset — the per-element twin of the scalar-str-global +// arm, offset like emitstrarraydata's rows. Elements must reduce to int +// (foldintliteral) or str literals; anything else returns false and the +// caller loud-stops (rule 7 — pre-C-t3 the whole definition was +// SILENTLY skipped and reads saw garbage). rhs == nil zero-inits the +// slot. Mirror of cstage emit_tuple_data. +fn emittupledata(c: *cgen, name: str, module: str, tt: *node, rhs: *node) bool = { + if (tt == nil) { return false; }; + if (rhs == nil) { + let zsz: i32 = 0; + let p0: *node = tt.list; + for (p0 != nil) { + let et0: *node = p0.lhs; + if (isstrtype(c, et0) || isslicetype(c, et0)) { + zsz += (tyslicesize(): i32); + } else { + zsz += 8; + }; + p0 = p0.next; + }; + emitline("DATAW "); + emitsymnamehint(c, name, module); + emitline("(SB),\""); + let zi: i32 = 0; + for (zi < zsz) { emitdatawbyte(0u8); zi += 1; }; + emitline("\"\n"); + return true; + }; + if (rhs.kind != nkind.N_TUPLE) { return false; }; + // Validate first (two-pass, emitarraydata precedent) so a partial + // row never reaches the output. + let tp: *node = tt.list; + let e: *node = rhs.list; + for (e != nil) { + let et: *node = nil; + if (tp != nil) { et = tp.lhs; }; + let ev: *node = e; + for (ev != nil && ev.kind == nkind.N_CAST) { ev = ev.lhs; }; + if (ev == nil) { return false; }; + let wide: bool = isstrtype(c, et) || isslicetype(c, et); + if (wide) { + if (ev.kind != nkind.N_STRLIT) { return false; }; + } else { + let v: u64 = 0u64; + if (!foldintliteral(ev, &v)) { return false; }; + }; + e = e.next; + if (tp != nil) { tp = tp.next; }; + }; + emitline("DATAW "); + emitsymnamehint(c, name, module); + emitline("(SB),\""); + tp = tt.list; + e = rhs.list; + for (e != nil) { + let et: *node = nil; + if (tp != nil) { et = tp.lhs; }; + let ev: *node = e; + for (ev != nil && ev.kind == nkind.N_CAST) { ev = ev.lhs; }; + let wide: bool = isstrtype(c, et) || isslicetype(c, et); + if (wide) { + let i: i32 = 0; + for (i < 8) { emitdatawbyte(0u8); i += 1; }; + let lv: u64 = ev.str.len: u64; + i = 0; + for (i < 8) { + emitdatawbyte((lv & 255u64): u8); + lv = lv >> 8u64; + i += 1; + }; + i = 16; + let ssz: i32 = primtypesize("str"): i32; + for (i < ssz) { emitdatawbyte(0u8); i += 1; }; + } else { + let v: u64 = 0u64; + foldintliteral(ev, &v); + let i: i32 = 0; + let nv: u64 = v; + for (i < 8) { + emitdatawbyte((nv & 255u64): u8); + nv = nv >> 8u64; + i += 1; + }; + }; + e = e.next; + if (tp != nil) { tp = tp.next; }; + }; + emitline("\"\n"); + let foff: i32 = 0; + tp = tt.list; + e = rhs.list; + for (e != nil) { + let et: *node = nil; + if (tp != nil) { et = tp.lhs; }; + let ev: *node = e; + for (ev != nil && ev.kind == nkind.N_CAST) { ev = ev.lhs; }; + let wide: bool = isstrtype(c, et) || isslicetype(c, et); + if (wide) { + if (ev.str.len > 0) { + let lab: str = internstrlit(c, ev.str); + emitline("DATAR "); + emitsymnamehint(c, name, module); + emitline("+"); + emitint(foff: i64); + emitline("(SB),"); + emitbytes( lab.ptr, lab.len: u64); + emitline("(SB)\n"); + }; + foff += (tyslicesize(): i32); + } else { + foff += 8; + }; + e = e.next; + if (tp != nil) { tp = tp.next; }; + }; + return true; +}; + fn emitletdataw(c: *cgen, file: *node) void = { let d: *node = file.list; for (d != nil) { @@ -36670,6 +36979,36 @@ fn emitletdataw(c: *cgen, file: *node) void = { let sz: i32 = letemitsize(c, d); let issg: bool = letvarisstruct(c, nm); let fsz: i32 = letvarisfloat(c, nm); + // C-t3 (#48): tuple global — slot-laid DATAW + // row (+ DATAR ptr patches for str elements) + // via emittupledata. Unsupported element + // inits die LOUD; pre-C-t3 the definition was + // silently skipped (no DATA, no diagnostic) + // and reads saw garbage. The istup gate also + // keeps a tuple out of the sz==8 / str-size + // arms below (a 24B tuple == str size). + let tlt: *node = d.lhs; + for (tlt != nil && tlt.kind == nkind.N_TNAME) { + tlt = aliaslookup(c, tlt.str); + }; + let istup: bool = false; + if (tlt != nil) { + if (tlt.kind == nkind.N_TTUPLE) { + istup = true; + }; + }; + if (istup) { + let tr: *node = d.rhs; + for (tr != nil) { + if (tr.kind != nkind.N_CAST) { break; }; + tr = tr.lhs; + }; + if (!emittupledata(c, nm, d.nmod, tlt, tr)) { + let mtg: str = "global tuple let: unsupported element init (int/str literals only; rule 7)\n"; + os.write(2, mtg.ptr, mtg.len: u64); + os.exit(1); + }; + }; if (fsz > 0) { // Float global: routes through the // emitfloatlitdata SSoT helper, shared @@ -36704,7 +37043,7 @@ fn emitletdataw(c: *cgen, file: *node) void = { if (d.lhs != nil) { if (d.lhs.kind == nkind.N_TARRAY) { isarr8 = true; }; }; - if (sz == 8 && !issg && fsz == 0 && !isarr8) { + if (sz == 8 && !issg && fsz == 0 && !isarr8 && !istup) { let v: u64 = 0u64; let ok: bool = true; if (d.rhs != nil) { @@ -36735,7 +37074,7 @@ fn emitletdataw(c: *cgen, file: *node) void = { emitline("\"\n"); }; }; - if (sz == primtypesize("str"): i32 && !issg && !letvarisslice(c, nm)) { + if (sz == primtypesize("str"): i32 && !issg && !istup && !letvarisslice(c, nm)) { let r: *node = d.rhs; for (r != nil) { if (r.kind != nkind.N_CAST) { break; }; diff --git a/selfhost/cmd/wcc/cgen.ww b/selfhost/cmd/wcc/cgen.ww index 1f14194c..44861287 100644 --- a/selfhost/cmd/wcc/cgen.ww +++ b/selfhost/cmd/wcc/cgen.ww @@ -996,6 +996,27 @@ fn letemitsize(c: *cgen, d: *node) i32 = { }; return alen * esz; }; + // C-t3 (#48): tuple global — per-element slot sum (C-t0 + // layout: a str/slice its header, everything else one 8B + // eightbyte). Mirrors cstage let_emit_size TY_TUPLE (u->size, + // the checker slot sum). Pre-C-t3 the 0 here kept tuple + // globals out of collectlets entirely — no DATA emitted, and + // the module-leaf fallback mis-emitted the field index as a + // symbol (`MOVQ 0(SB), AX`). + if (t.kind == nkind.N_TTUPLE) { + let tsum: i32 = 0; + let p: *node = t.list; + for (p != nil) { + let et: *node = p.lhs; + if (isstrtype(c, et) || isslicetype(c, et)) { + tsum += (tyslicesize(): i32); + } else { + tsum += 8; + }; + p = p.next; + }; + return tsum; + }; if (t.kind != nkind.N_TNAME) { return 0; }; let nm: str = t.str; if (letscalarprim(nm)) { return 8; }; @@ -1330,6 +1351,42 @@ export fn letpreintern(c: *cgen, file: *node) void = { }; }; }; + // C-t3 (#48): tuple global — pre-intern str-element + // literals in element order so emitletdataw's tuple + // arm's DATAR rows find their _S_ rodata rows (the + // #18 array-arm pattern; cstage let_pre_intern twin). + if (!handled && r != nil && d.lhs != nil) { + if (r.kind == nkind.N_TUPLE) { + let tlt: *node = d.lhs; + for (tlt != nil && tlt.kind == nkind.N_TNAME) { + tlt = aliaslookup(c, tlt.str); + }; + if (tlt != nil) { + if (tlt.kind == nkind.N_TTUPLE) { + handled = true; + let tp: *node = tlt.list; + let e: *node = r.list; + for (e != nil && tp != nil) { + let et: *node = tp.lhs; + let ev: *node = e; + for (ev != nil && ev.kind == nkind.N_CAST) { + ev = ev.lhs; + }; + if (ev != nil) { + if (ev.kind == nkind.N_STRLIT + && (isstrtype(c, et) || isslicetype(c, et))) { + if (ev.str.len > 0) { + internstrlit(c, ev.str); + }; + }; + }; + e = e.next; + tp = tp.next; + }; + }; + }; + }; + }; if (!handled) { let sz: i32 = letemitsize(c, d); // #43: route the str-let gate through primtypesize so @@ -2138,6 +2195,128 @@ fn emitslicedata(c: *cgen, name: str, module: str, slt: *tinfo, emitline(".d(SB)\n"); }; +// emittupledata — module-level `let g: (T0, T1, ...) = (v0, ...);` +// static init (C-t3, #48). Slot layout (C-t0): a scalar element is one +// 8B LE word, a str element its 24B header slot (8 zero ptr placeholder +// + LE len + 8 zero cap) with a DATAR patching the ptr word at the +// element's slot offset — the per-element twin of the scalar-str-global +// arm, offset like emitstrarraydata's rows. Elements must reduce to int +// (foldintliteral) or str literals; anything else returns false and the +// caller loud-stops (rule 7 — pre-C-t3 the whole definition was +// SILENTLY skipped and reads saw garbage). rhs == nil zero-inits the +// slot. Mirror of cstage emit_tuple_data. +fn emittupledata(c: *cgen, name: str, module: str, tt: *node, rhs: *node) bool = { + if (tt == nil) { return false; }; + if (rhs == nil) { + let zsz: i32 = 0; + let p0: *node = tt.list; + for (p0 != nil) { + let et0: *node = p0.lhs; + if (isstrtype(c, et0) || isslicetype(c, et0)) { + zsz += (tyslicesize(): i32); + } else { + zsz += 8; + }; + p0 = p0.next; + }; + emitline("DATAW "); + emitsymnamehint(c, name, module); + emitline("(SB),\""); + let zi: i32 = 0; + for (zi < zsz) { emitdatawbyte(0u8); zi += 1; }; + emitline("\"\n"); + return true; + }; + if (rhs.kind != nkind.N_TUPLE) { return false; }; + // Validate first (two-pass, emitarraydata precedent) so a partial + // row never reaches the output. + let tp: *node = tt.list; + let e: *node = rhs.list; + for (e != nil) { + let et: *node = nil; + if (tp != nil) { et = tp.lhs; }; + let ev: *node = e; + for (ev != nil && ev.kind == nkind.N_CAST) { ev = ev.lhs; }; + if (ev == nil) { return false; }; + let wide: bool = isstrtype(c, et) || isslicetype(c, et); + if (wide) { + if (ev.kind != nkind.N_STRLIT) { return false; }; + } else { + let v: u64 = 0u64; + if (!foldintliteral(ev, &v)) { return false; }; + }; + e = e.next; + if (tp != nil) { tp = tp.next; }; + }; + emitline("DATAW "); + emitsymnamehint(c, name, module); + emitline("(SB),\""); + tp = tt.list; + e = rhs.list; + for (e != nil) { + let et: *node = nil; + if (tp != nil) { et = tp.lhs; }; + let ev: *node = e; + for (ev != nil && ev.kind == nkind.N_CAST) { ev = ev.lhs; }; + let wide: bool = isstrtype(c, et) || isslicetype(c, et); + if (wide) { + let i: i32 = 0; + for (i < 8) { emitdatawbyte(0u8); i += 1; }; + let lv: u64 = ev.str.len: u64; + i = 0; + for (i < 8) { + emitdatawbyte((lv & 255u64): u8); + lv = lv >> 8u64; + i += 1; + }; + i = 16; + let ssz: i32 = primtypesize("str"): i32; + for (i < ssz) { emitdatawbyte(0u8); i += 1; }; + } else { + let v: u64 = 0u64; + foldintliteral(ev, &v); + let i: i32 = 0; + let nv: u64 = v; + for (i < 8) { + emitdatawbyte((nv & 255u64): u8); + nv = nv >> 8u64; + i += 1; + }; + }; + e = e.next; + if (tp != nil) { tp = tp.next; }; + }; + emitline("\"\n"); + let foff: i32 = 0; + tp = tt.list; + e = rhs.list; + for (e != nil) { + let et: *node = nil; + if (tp != nil) { et = tp.lhs; }; + let ev: *node = e; + for (ev != nil && ev.kind == nkind.N_CAST) { ev = ev.lhs; }; + let wide: bool = isstrtype(c, et) || isslicetype(c, et); + if (wide) { + if (ev.str.len > 0) { + let lab: str = internstrlit(c, ev.str); + emitline("DATAR "); + emitsymnamehint(c, name, module); + emitline("+"); + emitint(foff: i64); + emitline("(SB),"); + emitbytes( lab.ptr, lab.len: u64); + emitline("(SB)\n"); + }; + foff += (tyslicesize(): i32); + } else { + foff += 8; + }; + e = e.next; + if (tp != nil) { tp = tp.next; }; + }; + return true; +}; + fn emitletdataw(c: *cgen, file: *node) void = { let d: *node = file.list; for (d != nil) { @@ -2147,6 +2326,36 @@ fn emitletdataw(c: *cgen, file: *node) void = { let sz: i32 = letemitsize(c, d); let issg: bool = letvarisstruct(c, nm); let fsz: i32 = letvarisfloat(c, nm); + // C-t3 (#48): tuple global — slot-laid DATAW + // row (+ DATAR ptr patches for str elements) + // via emittupledata. Unsupported element + // inits die LOUD; pre-C-t3 the definition was + // silently skipped (no DATA, no diagnostic) + // and reads saw garbage. The istup gate also + // keeps a tuple out of the sz==8 / str-size + // arms below (a 24B tuple == str size). + let tlt: *node = d.lhs; + for (tlt != nil && tlt.kind == nkind.N_TNAME) { + tlt = aliaslookup(c, tlt.str); + }; + let istup: bool = false; + if (tlt != nil) { + if (tlt.kind == nkind.N_TTUPLE) { + istup = true; + }; + }; + if (istup) { + let tr: *node = d.rhs; + for (tr != nil) { + if (tr.kind != nkind.N_CAST) { break; }; + tr = tr.lhs; + }; + if (!emittupledata(c, nm, d.nmod, tlt, tr)) { + let mtg: str = "global tuple let: unsupported element init (int/str literals only; rule 7)\n"; + os.write(2, mtg.ptr, mtg.len: u64); + os.exit(1); + }; + }; if (fsz > 0) { // Float global: routes through the // emitfloatlitdata SSoT helper, shared @@ -2181,7 +2390,7 @@ fn emitletdataw(c: *cgen, file: *node) void = { if (d.lhs != nil) { if (d.lhs.kind == nkind.N_TARRAY) { isarr8 = true; }; }; - if (sz == 8 && !issg && fsz == 0 && !isarr8) { + if (sz == 8 && !issg && fsz == 0 && !isarr8 && !istup) { let v: u64 = 0u64; let ok: bool = true; if (d.rhs != nil) { @@ -2212,7 +2421,7 @@ fn emitletdataw(c: *cgen, file: *node) void = { emitline("\"\n"); }; }; - if (sz == primtypesize("str"): i32 && !issg && !letvarisslice(c, nm)) { + if (sz == primtypesize("str"): i32 && !issg && !istup && !letvarisslice(c, nm)) { let r: *node = d.rhs; for (r != nil) { if (r.kind != nkind.N_CAST) { break; }; diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index f9ac5a42..6f24af03 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -903,6 +903,24 @@ fn cgident(c: *cgen, n: *node) void = { // (AX, BX[, CX]). Names that aren't lets either (typos, // never-defined) drop through to the silent return. if (isletvar(c, nm)) { + // C-t3 (#48, rule 7): a GLOBAL tuple as a first-class VALUE + // (`let q = g;` / `return g;` / `f(g)`) has no slot-to-cursor + // path (cgtupleslottocursor is BP-relative) — pre-fix it fell + // to the scalar MOVQ below, loading word0 only, and the + // receive read a STALE cursor for words 1+. Element reads + // (g.N) are the supported surface. Mirrors the cstage cgexpr + // non-local ident guard. + let gtt: *node = letvartnode(c, nm); + for (gtt != nil && gtt.kind == nkind.N_TNAME) { + gtt = aliaslookup(c, gtt.str); + }; + if (gtt != nil) { + if (gtt.kind == nkind.N_TTUPLE) { + let mgt: str = "#48: global tuple as a first-class value unwired (element reads only; rule 7)\n"; + os.write(2, mgt.ptr, mgt.len: u64); + os.exit(1); + }; + }; let isstr: bool = letvarisstr(c, nm); let issl: bool = letvarisslice(c, nm); if (isstr || issl) { @@ -2805,6 +2823,79 @@ fn cgdot(c: *cgen, n: *node) void = { }; }; }; + // Top-level TUPLE global positional read (C-t3, #48): `g.N` — + // LEAQ name(SB) into CX, then load at the element's SLOT offset + // (C-t0 layout), the element's natural width. Mirrors the local + // N_TTUPLE arm above and cstage's N_DOT TY_TUPLE global base. + // Pre-C-t3 the tuple global wasn't in collectlets at all (no + // DATA) and the module-leaf fallback mis-emitted the field index + // as a symbol (`MOVQ 0(SB), AX`). + if (lhs != nil) { + if (lhs.kind == nkind.N_IDENT) { + let gtt: *node = letvartnode(c, lhs.str); + for (gtt != nil && gtt.kind == nkind.N_TNAME) { + gtt = aliaslookup(c, gtt.str); + }; + if (gtt != nil) { + if (gtt.kind == nkind.N_TTUPLE) { + let gidx: i32 = fldnumidx(fld); + if (gidx >= 0) { + let gtp: *node = gtt.list; + let gfoff: i32 = 0; + let gi: i32 = 0; + for (gi < gidx) { + if (gtp == nil) { gi = gidx; } + else { + gfoff += slotsize(c, gtp.lhs); + gtp = gtp.next; + gi += 1; + }; + }; + if (gtp != nil) { + let gpt: *node = gtp.lhs; + emitline("\tLEAQ\t"); + emitsymname(c, lhs.str); + emitline("(SB), CX\n"); + if (isstrtype(c, gpt)) { + // CX (the base) is written LAST so + // it survives the +0/+8 reads. + emitline("\tMOVQ\t"); + emitdispreg((gfoff + 0): i64, "CX"); + emitline(", AX\n"); + emitline("\tMOVQ\t"); + emitdispreg((gfoff + 8): i64, "CX"); + emitline(", BX\n"); + emitline("\tMOVQ\t"); + emitdispreg((gfoff + 16): i64, "CX"); + emitline(", CX\n"); + return; + }; + if (isfloattype(c, gpt)) { + let mov: str = "MOVSD"; + if (isf32type(c, gpt)) { mov = "MOVSS"; }; + emitline("\t"); + emitline(mov); + emitline("\t"); + emitdispreg(gfoff: i64, "CX"); + emitline(", X0\n"); + return; + }; + let gnsz: i32 = 8; + let gpti: *tinfo = gpt.type_: *tinfo; + if (gpti != nil) { gnsz = gpti.size: i32; }; + let gop: str = tnodeloadop(c, gpt, gnsz); + emitline("\t"); + emitline(gop); + emitline("\t"); + emitdispreg(gfoff: i64, "CX"); + emitline(", AX\n"); + return; + }; + }; + }; + }; + }; + }; // Top-level struct global field read — LEAQ name(SB), CX then // load at fi.foff(CX). Mirrors the local "Direct struct local" // branch above, swapping the BP frame slot for the global VA. @@ -5661,6 +5752,45 @@ fn cgcall(c: *cgen, n: *node) void = { }; }; }; + // C-t3 (#48): GLOBAL tuple len(g.N) — + // LEAQ name(SB) into CX, .len word at + // the SLOT offset + 8. Graduates the + // C5 loud-stop this shape previously + // hit; twin of the cgdot global-tuple + // arm and cstage's #235 global base. + if (lc == nil) { + let gtn: *node = letvartnode(c, a.lhs.str); + for (gtn != nil && gtn.kind == nkind.N_TNAME) { + gtn = aliaslookup(c, gtn.str); + }; + if (gtn != nil) { + if (gtn.kind == nkind.N_TTUPLE) { + let gidx: i32 = fldnumidx(a.str); + if (gidx >= 0) { + let gtp: *node = gtn.list; + let gfoff: i32 = 0; + let gi: i32 = 0; + for (gi < gidx) { + if (gtp == nil) { gi = gidx; } + else { + gfoff += slotsize(c, gtp.lhs); + gtp = gtp.next; + gi += 1; + }; + }; + if (gtp != nil) { + emitline("\tLEAQ\t"); + emitsymname(c, a.lhs.str); + emitline("(SB), CX\n"); + emitline("\tMOVQ\t"); + emitdispreg((gfoff + 8): i64, "CX"); + emitline(", AX\n"); + return; + }; + }; + }; + }; + }; // struct-field N_DOT (`len(s.field)`): the // old fallback returned .ptr as the length. // Falls to the resolver route below. diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 52cab30d..0ba2fed1 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -21180,6 +21180,24 @@ fn cgident(c: *cgen, n: *node) void = { // (AX, BX[, CX]). Names that aren't lets either (typos, // never-defined) drop through to the silent return. if (isletvar(c, nm)) { + // C-t3 (#48, rule 7): a GLOBAL tuple as a first-class VALUE + // (`let q = g;` / `return g;` / `f(g)`) has no slot-to-cursor + // path (cgtupleslottocursor is BP-relative) — pre-fix it fell + // to the scalar MOVQ below, loading word0 only, and the + // receive read a STALE cursor for words 1+. Element reads + // (g.N) are the supported surface. Mirrors the cstage cgexpr + // non-local ident guard. + let gtt: *node = letvartnode(c, nm); + for (gtt != nil && gtt.kind == nkind.N_TNAME) { + gtt = aliaslookup(c, gtt.str); + }; + if (gtt != nil) { + if (gtt.kind == nkind.N_TTUPLE) { + let mgt: str = "#48: global tuple as a first-class value unwired (element reads only; rule 7)\n"; + os.write(2, mgt.ptr, mgt.len: u64); + os.exit(1); + }; + }; let isstr: bool = letvarisstr(c, nm); let issl: bool = letvarisslice(c, nm); if (isstr || issl) { @@ -23082,6 +23100,79 @@ fn cgdot(c: *cgen, n: *node) void = { }; }; }; + // Top-level TUPLE global positional read (C-t3, #48): `g.N` — + // LEAQ name(SB) into CX, then load at the element's SLOT offset + // (C-t0 layout), the element's natural width. Mirrors the local + // N_TTUPLE arm above and cstage's N_DOT TY_TUPLE global base. + // Pre-C-t3 the tuple global wasn't in collectlets at all (no + // DATA) and the module-leaf fallback mis-emitted the field index + // as a symbol (`MOVQ 0(SB), AX`). + if (lhs != nil) { + if (lhs.kind == nkind.N_IDENT) { + let gtt: *node = letvartnode(c, lhs.str); + for (gtt != nil && gtt.kind == nkind.N_TNAME) { + gtt = aliaslookup(c, gtt.str); + }; + if (gtt != nil) { + if (gtt.kind == nkind.N_TTUPLE) { + let gidx: i32 = fldnumidx(fld); + if (gidx >= 0) { + let gtp: *node = gtt.list; + let gfoff: i32 = 0; + let gi: i32 = 0; + for (gi < gidx) { + if (gtp == nil) { gi = gidx; } + else { + gfoff += slotsize(c, gtp.lhs); + gtp = gtp.next; + gi += 1; + }; + }; + if (gtp != nil) { + let gpt: *node = gtp.lhs; + emitline("\tLEAQ\t"); + emitsymname(c, lhs.str); + emitline("(SB), CX\n"); + if (isstrtype(c, gpt)) { + // CX (the base) is written LAST so + // it survives the +0/+8 reads. + emitline("\tMOVQ\t"); + emitdispreg((gfoff + 0): i64, "CX"); + emitline(", AX\n"); + emitline("\tMOVQ\t"); + emitdispreg((gfoff + 8): i64, "CX"); + emitline(", BX\n"); + emitline("\tMOVQ\t"); + emitdispreg((gfoff + 16): i64, "CX"); + emitline(", CX\n"); + return; + }; + if (isfloattype(c, gpt)) { + let mov: str = "MOVSD"; + if (isf32type(c, gpt)) { mov = "MOVSS"; }; + emitline("\t"); + emitline(mov); + emitline("\t"); + emitdispreg(gfoff: i64, "CX"); + emitline(", X0\n"); + return; + }; + let gnsz: i32 = 8; + let gpti: *tinfo = gpt.type_: *tinfo; + if (gpti != nil) { gnsz = gpti.size: i32; }; + let gop: str = tnodeloadop(c, gpt, gnsz); + emitline("\t"); + emitline(gop); + emitline("\t"); + emitdispreg(gfoff: i64, "CX"); + emitline(", AX\n"); + return; + }; + }; + }; + }; + }; + }; // Top-level struct global field read — LEAQ name(SB), CX then // load at fi.foff(CX). Mirrors the local "Direct struct local" // branch above, swapping the BP frame slot for the global VA. @@ -25938,6 +26029,45 @@ fn cgcall(c: *cgen, n: *node) void = { }; }; }; + // C-t3 (#48): GLOBAL tuple len(g.N) — + // LEAQ name(SB) into CX, .len word at + // the SLOT offset + 8. Graduates the + // C5 loud-stop this shape previously + // hit; twin of the cgdot global-tuple + // arm and cstage's #235 global base. + if (lc == nil) { + let gtn: *node = letvartnode(c, a.lhs.str); + for (gtn != nil && gtn.kind == nkind.N_TNAME) { + gtn = aliaslookup(c, gtn.str); + }; + if (gtn != nil) { + if (gtn.kind == nkind.N_TTUPLE) { + let gidx: i32 = fldnumidx(a.str); + if (gidx >= 0) { + let gtp: *node = gtn.list; + let gfoff: i32 = 0; + let gi: i32 = 0; + for (gi < gidx) { + if (gtp == nil) { gi = gidx; } + else { + gfoff += slotsize(c, gtp.lhs); + gtp = gtp.next; + gi += 1; + }; + }; + if (gtp != nil) { + emitline("\tLEAQ\t"); + emitsymname(c, a.lhs.str); + emitline("(SB), CX\n"); + emitline("\tMOVQ\t"); + emitdispreg((gfoff + 8): i64, "CX"); + emitline(", AX\n"); + return; + }; + }; + }; + }; + }; // struct-field N_DOT (`len(s.field)`): the // old fallback returned .ptr as the length. // Falls to the resolver route below. @@ -35519,6 +35649,27 @@ fn letemitsize(c: *cgen, d: *node) i32 = { }; return alen * esz; }; + // C-t3 (#48): tuple global — per-element slot sum (C-t0 + // layout: a str/slice its header, everything else one 8B + // eightbyte). Mirrors cstage let_emit_size TY_TUPLE (u->size, + // the checker slot sum). Pre-C-t3 the 0 here kept tuple + // globals out of collectlets entirely — no DATA emitted, and + // the module-leaf fallback mis-emitted the field index as a + // symbol (`MOVQ 0(SB), AX`). + if (t.kind == nkind.N_TTUPLE) { + let tsum: i32 = 0; + let p: *node = t.list; + for (p != nil) { + let et: *node = p.lhs; + if (isstrtype(c, et) || isslicetype(c, et)) { + tsum += (tyslicesize(): i32); + } else { + tsum += 8; + }; + p = p.next; + }; + return tsum; + }; if (t.kind != nkind.N_TNAME) { return 0; }; let nm: str = t.str; if (letscalarprim(nm)) { return 8; }; @@ -35853,6 +36004,42 @@ export fn letpreintern(c: *cgen, file: *node) void = { }; }; }; + // C-t3 (#48): tuple global — pre-intern str-element + // literals in element order so emitletdataw's tuple + // arm's DATAR rows find their _S_ rodata rows (the + // #18 array-arm pattern; cstage let_pre_intern twin). + if (!handled && r != nil && d.lhs != nil) { + if (r.kind == nkind.N_TUPLE) { + let tlt: *node = d.lhs; + for (tlt != nil && tlt.kind == nkind.N_TNAME) { + tlt = aliaslookup(c, tlt.str); + }; + if (tlt != nil) { + if (tlt.kind == nkind.N_TTUPLE) { + handled = true; + let tp: *node = tlt.list; + let e: *node = r.list; + for (e != nil && tp != nil) { + let et: *node = tp.lhs; + let ev: *node = e; + for (ev != nil && ev.kind == nkind.N_CAST) { + ev = ev.lhs; + }; + if (ev != nil) { + if (ev.kind == nkind.N_STRLIT + && (isstrtype(c, et) || isslicetype(c, et))) { + if (ev.str.len > 0) { + internstrlit(c, ev.str); + }; + }; + }; + e = e.next; + tp = tp.next; + }; + }; + }; + }; + }; if (!handled) { let sz: i32 = letemitsize(c, d); // #43: route the str-let gate through primtypesize so @@ -36661,6 +36848,128 @@ fn emitslicedata(c: *cgen, name: str, module: str, slt: *tinfo, emitline(".d(SB)\n"); }; +// emittupledata — module-level `let g: (T0, T1, ...) = (v0, ...);` +// static init (C-t3, #48). Slot layout (C-t0): a scalar element is one +// 8B LE word, a str element its 24B header slot (8 zero ptr placeholder +// + LE len + 8 zero cap) with a DATAR patching the ptr word at the +// element's slot offset — the per-element twin of the scalar-str-global +// arm, offset like emitstrarraydata's rows. Elements must reduce to int +// (foldintliteral) or str literals; anything else returns false and the +// caller loud-stops (rule 7 — pre-C-t3 the whole definition was +// SILENTLY skipped and reads saw garbage). rhs == nil zero-inits the +// slot. Mirror of cstage emit_tuple_data. +fn emittupledata(c: *cgen, name: str, module: str, tt: *node, rhs: *node) bool = { + if (tt == nil) { return false; }; + if (rhs == nil) { + let zsz: i32 = 0; + let p0: *node = tt.list; + for (p0 != nil) { + let et0: *node = p0.lhs; + if (isstrtype(c, et0) || isslicetype(c, et0)) { + zsz += (tyslicesize(): i32); + } else { + zsz += 8; + }; + p0 = p0.next; + }; + emitline("DATAW "); + emitsymnamehint(c, name, module); + emitline("(SB),\""); + let zi: i32 = 0; + for (zi < zsz) { emitdatawbyte(0u8); zi += 1; }; + emitline("\"\n"); + return true; + }; + if (rhs.kind != nkind.N_TUPLE) { return false; }; + // Validate first (two-pass, emitarraydata precedent) so a partial + // row never reaches the output. + let tp: *node = tt.list; + let e: *node = rhs.list; + for (e != nil) { + let et: *node = nil; + if (tp != nil) { et = tp.lhs; }; + let ev: *node = e; + for (ev != nil && ev.kind == nkind.N_CAST) { ev = ev.lhs; }; + if (ev == nil) { return false; }; + let wide: bool = isstrtype(c, et) || isslicetype(c, et); + if (wide) { + if (ev.kind != nkind.N_STRLIT) { return false; }; + } else { + let v: u64 = 0u64; + if (!foldintliteral(ev, &v)) { return false; }; + }; + e = e.next; + if (tp != nil) { tp = tp.next; }; + }; + emitline("DATAW "); + emitsymnamehint(c, name, module); + emitline("(SB),\""); + tp = tt.list; + e = rhs.list; + for (e != nil) { + let et: *node = nil; + if (tp != nil) { et = tp.lhs; }; + let ev: *node = e; + for (ev != nil && ev.kind == nkind.N_CAST) { ev = ev.lhs; }; + let wide: bool = isstrtype(c, et) || isslicetype(c, et); + if (wide) { + let i: i32 = 0; + for (i < 8) { emitdatawbyte(0u8); i += 1; }; + let lv: u64 = ev.str.len: u64; + i = 0; + for (i < 8) { + emitdatawbyte((lv & 255u64): u8); + lv = lv >> 8u64; + i += 1; + }; + i = 16; + let ssz: i32 = primtypesize("str"): i32; + for (i < ssz) { emitdatawbyte(0u8); i += 1; }; + } else { + let v: u64 = 0u64; + foldintliteral(ev, &v); + let i: i32 = 0; + let nv: u64 = v; + for (i < 8) { + emitdatawbyte((nv & 255u64): u8); + nv = nv >> 8u64; + i += 1; + }; + }; + e = e.next; + if (tp != nil) { tp = tp.next; }; + }; + emitline("\"\n"); + let foff: i32 = 0; + tp = tt.list; + e = rhs.list; + for (e != nil) { + let et: *node = nil; + if (tp != nil) { et = tp.lhs; }; + let ev: *node = e; + for (ev != nil && ev.kind == nkind.N_CAST) { ev = ev.lhs; }; + let wide: bool = isstrtype(c, et) || isslicetype(c, et); + if (wide) { + if (ev.str.len > 0) { + let lab: str = internstrlit(c, ev.str); + emitline("DATAR "); + emitsymnamehint(c, name, module); + emitline("+"); + emitint(foff: i64); + emitline("(SB),"); + emitbytes( lab.ptr, lab.len: u64); + emitline("(SB)\n"); + }; + foff += (tyslicesize(): i32); + } else { + foff += 8; + }; + e = e.next; + if (tp != nil) { tp = tp.next; }; + }; + return true; +}; + fn emitletdataw(c: *cgen, file: *node) void = { let d: *node = file.list; for (d != nil) { @@ -36670,6 +36979,36 @@ fn emitletdataw(c: *cgen, file: *node) void = { let sz: i32 = letemitsize(c, d); let issg: bool = letvarisstruct(c, nm); let fsz: i32 = letvarisfloat(c, nm); + // C-t3 (#48): tuple global — slot-laid DATAW + // row (+ DATAR ptr patches for str elements) + // via emittupledata. Unsupported element + // inits die LOUD; pre-C-t3 the definition was + // silently skipped (no DATA, no diagnostic) + // and reads saw garbage. The istup gate also + // keeps a tuple out of the sz==8 / str-size + // arms below (a 24B tuple == str size). + let tlt: *node = d.lhs; + for (tlt != nil && tlt.kind == nkind.N_TNAME) { + tlt = aliaslookup(c, tlt.str); + }; + let istup: bool = false; + if (tlt != nil) { + if (tlt.kind == nkind.N_TTUPLE) { + istup = true; + }; + }; + if (istup) { + let tr: *node = d.rhs; + for (tr != nil) { + if (tr.kind != nkind.N_CAST) { break; }; + tr = tr.lhs; + }; + if (!emittupledata(c, nm, d.nmod, tlt, tr)) { + let mtg: str = "global tuple let: unsupported element init (int/str literals only; rule 7)\n"; + os.write(2, mtg.ptr, mtg.len: u64); + os.exit(1); + }; + }; if (fsz > 0) { // Float global: routes through the // emitfloatlitdata SSoT helper, shared @@ -36704,7 +37043,7 @@ fn emitletdataw(c: *cgen, file: *node) void = { if (d.lhs != nil) { if (d.lhs.kind == nkind.N_TARRAY) { isarr8 = true; }; }; - if (sz == 8 && !issg && fsz == 0 && !isarr8) { + if (sz == 8 && !issg && fsz == 0 && !isarr8 && !istup) { let v: u64 = 0u64; let ok: bool = true; if (d.rhs != nil) { @@ -36735,7 +37074,7 @@ fn emitletdataw(c: *cgen, file: *node) void = { emitline("\"\n"); }; }; - if (sz == primtypesize("str"): i32 && !issg && !letvarisslice(c, nm)) { + if (sz == primtypesize("str"): i32 && !issg && !istup && !letvarisslice(c, nm)) { let r: *node = d.rhs; for (r != nil) { if (r.kind != nkind.N_CAST) { break; }; diff --git a/test/wcc/941_tuple_slot_layout_run.c b/test/wcc/941_tuple_slot_layout_run.c index 9bd10a7a..41de90e7 100644 --- a/test/wcc/941_tuple_slot_layout_run.c +++ b/test/wcc/941_tuple_slot_layout_run.c @@ -436,6 +436,52 @@ static const struct row rows[] = { " return 0;\n" "};\n", 0, K_BUILDERR, "element kind unsupported" }, + + /* ---- C-t3 (#48): GLOBAL tuples. Pre-C-t3 the definition was + * SILENTLY skipped (let_emit_size 0 → no DATA, no diagnostic); + * cstage element reads then read BP-frame garbage (localfind→0) + * and wwstage mis-emitted the field index as a symbol + * (`MOVQ 0(SB), AX`). Now: slot-laid DATAW (+ DATAR str-element + * ptr patches), element reads + len(g.N) via LEAQ sym(SB); + * unsupported element inits and the whole-tuple-as-value shape + * die LOUD. ---- */ + { "t3_global_elem", + "package main;\n" + "let g: (str, i64) = (\"hello\", 9);\n" + "let h: (u32, u32) = (5, 6);\n" + "export fn main() i32 = {\n" + " if (len(g.0) != 5) { return 1; };\n" + " if (g.1 != 9) { return 2; };\n" + " if (h.0 != 5) { return 3; };\n" + " if (h.1 != 6) { return 4; };\n" + " return 0;\n" + "};\n", 0, K_RUN, NULL }, + { "t3_reject_float_global", + "package main;\n" + "let g: (f64, i64) = (2.5, 4);\n" + "export fn main() i32 = {\n" + " if (g.1 != 4) { return 1; };\n" + " return 0;\n" + "};\n", 0, + K_BUILDERR, "unsupported element init (int/str literals only; rule 7)" }, + { "t3_reject_whole_value", + "package main;\n" + "let g: (i64, i64) = (3, 4);\n" + "export fn main() i32 = {\n" + " let q: (i64, i64) = g;\n" + " if (q.0 != 3) { return 1; };\n" + " return 0;\n" + "};\n", 0, + K_BUILDERR, "global tuple as a first-class value unwired" }, + /* pre-existing loud (no-regress pin): global tuple element WRITE. */ + { "t3_reject_global_write", + "package main;\n" + "let g: (i64, i64) = (3, 4);\n" + "export fn main() i32 = {\n" + " g.0 = 7;\n" + " return 0;\n" + "};\n", 0, + K_BUILDERR, "unsupported assign target shape" }, }; /* build+run via a driver (ww / ww_ww); returns 0 pass, nonzero fail. */