diff --git a/cmd/w6c/cgen.c b/cmd/w6c/cgen.c index 1b6c31e6..c806c868 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -1246,6 +1246,16 @@ let_emit_size(Type *t) * the 0 here SILENTLY skipped the * definition and every read saw * BP-frame garbage. */ + case TY_TAGGED: + /* #87: non-nullable tagged-union global — box size (tag word + * + payload, mirror of the runtime local box). int/str-literal + * variant init via emit_tagged_data; match-scrutinee reads + * resolve the box at name(SB). Pre-#87 the missing arm sized 0 + * → no DATA, no letvar registration, and match read saved BP + * as the tag (SEGV). The nullable `(*T | void)` one-word fold + * stays 0 (handled, when const, by the 8B scalar arm in + * emit_lets — adding it here would re-route that path). */ + return u->nullable ? 0 : (int)u->size; default: return 0; } @@ -9460,7 +9470,25 @@ cgexpr(Cg *c, Node *n, Local *locals) int is_nullable = type_isnullable(st); int slot_size = (su && su->kind == TY_TAGGED) ? (int)su->size : 16; int sl_off; - if (s->kind == N_IDENT) { + if (s->kind == N_IDENT && let_islet(s->str) + && !is_nullable && su && su->kind == TY_TAGGED) { + /* #87: a tagged-union GLOBAL scrutinee — the box lives + * in static DATA at name(SB), not the BP frame. + * localfind would return 0 and the dispatch would read + * saved BP as the tag (SEGV). The PLAIN-tagged twin of + * #78's SB-resolution: LEAQ the address and copy the + * box into a scratch slot the dispatch indexes off BP. + * `is`/case-let binds read the same scratch. */ + sl_off = localoff(c, &locals, "@match_spill", + slot_size, cg_frame); + ins2(c, A_LEAQ, mahint(c, s->str, c->cur_mod), + areg(D_AX)); + for (int k = 0; k < slot_size; k += 8) { + ins2(c, A_MOVQ, amem(D_AX, k), areg(D_DX)); + ins2(c, A_MOVQ, areg(D_DX), + amem(D_BP, sl_off + k)); + } + } else if (s->kind == N_IDENT) { sl_off = localfind(locals, s->str); } else if (s->kind == N_DOT && s->lhs && s->lhs->kind == N_IDENT && s->lhs->type) { @@ -14702,6 +14730,70 @@ emit_strarray_data(FILE *out, Cg *c, const char *directive, return 1; } +/* emit_tagged_data — module-level `let g: (T0 | T1 | ...) = v;` static + * init (#87). The static DATA must byte-MIRROR a runtime LOCAL tagged box + * of the same type (the SSoT pin, rob §3): tag word at +0 (the const- + * selected variant's index, cg_tag_for_variant — the same routine the + * runtime widen + match dispatch key on), the payload at +8, zero-padded + * to the union's box size. So `let g: (i32|str) = 42` and a local + * `let l: (i32|str) = 42` read byte-identically. int and str-literal + * variants are wired (the shapes Hare stdlib uses, ref/hare/time/chrono/ + * utc.ha:44); any other variant payload returns 0 and the caller loud- + * stops (rule 7) — never the pre-#87 silent no-DATA + saved-BP read. */ +static int +emit_tagged_data(FILE *out, Cg *c, const char *name, const char *module, + Type *t, Node *rhs) +{ + Type *u = type_chase_named(t); + if (u == NULL || u->kind != TY_TAGGED || u->nullable) return 0; + int sz = (int)u->size; + const char *sym = mod_mangle_value(c, name, module); + if (rhs == NULL) { + fprintf(out, "DATAW %s(SB),\"", sym); + for (int i = 0; i < sz; i++) emit_data_byte(out, 0); + fputs("\"\n", out); + return 1; + } + Node *r = rhs; + while (r != NULL && r->kind == N_CAST) r = r->lhs; + if (r == NULL) return 0; + int tag = cg_tag_for_variant(u, r->type); + if (tag < 0) return 0; + int wide = type_isstr(r->type) || type_isslice(r->type); + if (wide) { + /* str/slice variant: {ptr placeholder, len, cap} at +8 with a + * DATAR patching the ptr word, mirroring the runtime box + * (LEAQ _S_n into ptr@+8, len@+16, cap@+24). */ + if (r->kind != N_STRLIT) return 0; + u64 len = r->strlen; + fprintf(out, "DATAW %s(SB),\"", sym); + for (int i = 0; i < 8; i++) + emit_data_byte(out, (u8)(((u64)tag >> (i * 8)) & 0xff)); + for (int i = 0; i < 8; i++) emit_data_byte(out, 0); + for (int i = 0; i < 8; i++) + emit_data_byte(out, (u8)((len >> (i * 8)) & 0xff)); + for (int i = 0; i < 8; i++) + emit_data_byte(out, (u8)((len >> (i * 8)) & 0xff)); + for (int i = 32; i < sz; i++) emit_data_byte(out, 0); + fputs("\"\n", out); + if (len > 0) { + const char *lab = intern_strlit(c, r->str, r->strlen); + fprintf(out, "DATAR %s+8(SB),%s(SB)\n", sym, lab); + } + return 1; + } + u64 v; + if (!fold_int_literal(r, &v)) return 0; + fprintf(out, "DATAW %s(SB),\"", sym); + for (int i = 0; i < 8; i++) + emit_data_byte(out, (u8)(((u64)tag >> (i * 8)) & 0xff)); + for (int i = 0; i < 8; i++) + emit_data_byte(out, (u8)((v >> (i * 8)) & 0xff)); + for (int i = 16; i < sz; i++) emit_data_byte(out, 0); + fputs("\"\n", out); + 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 @@ -14921,6 +15013,23 @@ emit_lets(Cg *c, FILE *out, Node *file) continue; } } + /* #87: non-nullable tagged-union global — emit the box that + * mirrors the runtime local (tag + payload). let_emit_size + * keeps nullable at 0 so the (*T|void) one-word fold stays on + * the 8B scalar arm below. */ + { + Type *gu = type_chase_named(d->type); + if (gu != NULL && gu->kind == TY_TAGGED + && !gu->nullable) { + if (!emit_tagged_data(out, c, d->str, + d->module, d->type, d->rhs)) + fatal("global tagged let `%s`: " + "unsupported variant init " + "(int/str literal 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` @@ -15200,6 +15309,15 @@ let_pre_intern(Cg *c, Node *file) } continue; } + /* #87: tagged global with a str/slice-variant literal init — + * pre-intern so emit_tagged_data's DATAR (ptr@+8) finds its + * _S_ rodata row (the #48 tuple-arm pattern). */ + if (u != NULL && u->kind == TY_TAGGED && !u->nullable + && r != NULL && r->kind == N_STRLIT && r->strlen > 0 + && (type_isstr(r->type) || type_isslice(r->type))) { + (void)intern_strlit(c, r->str, r->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 976da51e..125a0d99 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -23730,7 +23730,33 @@ fn cgmatch(c: *cgen, n: *node) void = { if (lc != nil) { scrutoff = lc.off; scrutt = resolvetagged(c, lc.tnode); - }; + } else { if (isletvar(c, scrut.str)) { + // #87: a tagged-union GLOBAL scrutinee — the box lives + // in static DATA at name(SB), not the BP frame. + // localfindnode returns nil and the dispatch would read + // saved BP as the tag (garbage). The PLAIN-tagged twin + // of cstage #78 SB-resolution: LEAQ the address, copy + // the box into an @match_spill slot indexed off BP. + let gtt: *node = resolvetagged(c, letvartnode(c, scrut.str)); + if (gtt != nil && !isnullabletype(gtt)) { + scrutt = gtt; + let gspill: i32 = matchspillsz(c, gtt); + scrutoff = localalloc(c, "@match_spill", gspill, nil); + emitline("\tLEAQ\t"); + emitsymnamehint(c, scrut.str, c.curmod); + emitline("(SB), AX\n"); + let gk: i32 = 0; + for (gk < gspill) { + emitline("\tMOVQ\t"); + emitdispreg(gk: i64, "AX"); + emitline(", DX\n"); + emitline("\tMOVQ\tDX, "); + emitoff((scrutoff + gk): i64); + emitline("(BP)\n"); + gk += 8; + }; + }; + }; }; } else { // Non-ident scrutinee (call result, arr[i], p.field, // ?, etc.). Spill into an `@match_spill` scratch slot @@ -38229,6 +38255,14 @@ fn letemitsize(c: *cgen, d: *node) i32 = { }; return tsum; }; + // #87: non-nullable tagged-union global — box size (tag word + + // max payload, mirror of the runtime local). Nullable stays 0 so + // the (*T|void) one-word fold keeps the 8B scalar arm in + // emitletdataw. Mirrors cstage let_emit_size TY_TAGGED. + if (t.kind == nkind.N_TTAGGED) { + if (isnullabletype(t)) { return 0; }; + return slotsize(c, t); + }; if (t.kind != nkind.N_TNAME) { return 0; }; let nm: str = t.str; if (letscalarprim(nm)) { return 8; }; @@ -38591,6 +38625,24 @@ export fn letpreintern(c: *cgen, file: *node) void = { }; }; }; + // #87: tagged global with a str/slice-variant literal init — + // pre-intern so emittaggeddata's DATAR (ptr@+8) finds its _S_ + // rodata row (the #48 tuple-arm pattern; cstage letpreintern twin). + if (!handled && r != nil && d.lhs != nil) { + 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_TTAGGED && !isnullabletype(tlt)) { + if (r.kind == nkind.N_STRLIT && r.str.len > 0 + && (nodeisstr(c, r) || nodeisslice(c, r))) { + handled = true; + internstrlit(c, r.str); + }; + }; + }; + }; if (!handled) { let sz: i32 = letemitsize(c, d); // #43: route the str-let gate through primtypesize so @@ -39399,6 +39451,89 @@ fn emitslicedata(c: *cgen, name: str, module: str, slt: *tinfo, emitline(".d(SB)\n"); }; +// emittaggeddata — module-level `let g: (T0 | T1 | ...) = v;` static +// init (#87). Byte-MIRRORS a runtime LOCAL tagged box (rob §3 SSoT pin): +// tag word at +0 (the const-selected variant index, taggedvariantindex — +// the routine the runtime widen + match dispatch key on), payload at +8, +// zero-padded to the union box size `sz`. int and str-literal variants +// are wired (the Hare-stdlib shapes, ref/hare/time/chrono/utc.ha:44); any +// other variant payload returns false and the caller loud-stops (rule 7) — +// never the pre-#87 silent no-DATA + garbage read. Mirror of cstage +// emit_tagged_data. +fn emittaggeddata(c: *cgen, name: str, module: str, tt: *node, rhs: *node, sz: i32) bool = { + if (tt == nil) { return false; }; + if (rhs == nil) { + emitline("DATAW "); + emitsymnamehint(c, name, module); + emitline("(SB),\""); + let zi: i32 = 0; + for (zi < sz) { emitdatawbyte(0u8); zi += 1; }; + emitline("\"\n"); + return true; + }; + let r: *node = rhs; + for (r != nil && r.kind == nkind.N_CAST) { r = r.lhs; }; + if (r == nil) { return false; }; + let tag: i32 = taggedvariantindex(c, tt, r); + if (tag < 0) { return false; }; + let wide: bool = nodeisstr(c, r) || nodeisslice(c, r); + let i: i32 = 0; + let acc: u64 = 0u64; + if (wide) { + if (r.kind != nkind.N_STRLIT) { return false; }; + let lv: u64 = r.str.len: u64; + emitline("DATAW "); + emitsymnamehint(c, name, module); + emitline("(SB),\""); + // tag@0 + acc = tag: u64; + i = 0; + for (i < 8) { emitdatawbyte((acc & 255u64): u8); acc = acc >> 8u64; i += 1; }; + // ptr placeholder@8 + i = 0; + for (i < 8) { emitdatawbyte(0u8); i += 1; }; + // len@16 + acc = lv; + i = 0; + for (i < 8) { emitdatawbyte((acc & 255u64): u8); acc = acc >> 8u64; i += 1; }; + // cap@24 (= len for a static str literal, mirroring the box) + acc = lv; + i = 0; + for (i < 8) { emitdatawbyte((acc & 255u64): u8); acc = acc >> 8u64; i += 1; }; + // pad to sz + i = 32; + for (i < sz) { emitdatawbyte(0u8); i += 1; }; + emitline("\"\n"); + if (r.str.len > 0) { + let lab: str = internstrlit(c, r.str); + emitline("DATAR "); + emitsymnamehint(c, name, module); + emitline("+8(SB),"); + emitbytes( lab.ptr, lab.len: u64); + emitline("(SB)\n"); + }; + return true; + }; + let v: u64 = 0u64; + if (!foldintliteral(r, &v)) { return false; }; + emitline("DATAW "); + emitsymnamehint(c, name, module); + emitline("(SB),\""); + // tag@0 + acc = tag: u64; + i = 0; + for (i < 8) { emitdatawbyte((acc & 255u64): u8); acc = acc >> 8u64; i += 1; }; + // payload@8 + acc = v; + i = 0; + for (i < 8) { emitdatawbyte((acc & 255u64): u8); acc = acc >> 8u64; i += 1; }; + // pad to sz + i = 16; + for (i < sz) { emitdatawbyte(0u8); i += 1; }; + emitline("\"\n"); + return true; +}; + // 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 @@ -39582,6 +39717,24 @@ fn emitletdataw(c: *cgen, file: *node) void = { os.exit(1); }; }; + // #87: non-nullable tagged-union global — emit the box + // mirroring the runtime local (tag + payload). letemitsize + // keeps nullable at 0 so the (*T|void) one-word fold stays + // on the 8B scalar arm below. The istagged gate also keeps + // a tagged box (size can equal str/slice size) out of those. + let istagged: bool = false; + if (tlt != nil) { + if (tlt.kind == nkind.N_TTAGGED) { + if (!isnullabletype(tlt)) { istagged = true; }; + }; + }; + if (istagged) { + if (!emittaggeddata(c, nm, d.nmod, tlt, d.rhs, sz)) { + let mtg: str = "global tagged let: unsupported variant init (int/str literal 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 @@ -39616,7 +39769,7 @@ fn emitletdataw(c: *cgen, file: *node) void = { if (dti != nil) { if (dti.kind == tykind.TY_ARRAY) { isarr8 = true; }; }; - if (sz == 8 && !issg && fsz == 0 && !isarr8 && !istup) { + if (sz == 8 && !issg && fsz == 0 && !isarr8 && !istup && !istagged) { let v: u64 = 0u64; let ok: bool = true; if (d.rhs != nil) { @@ -39647,7 +39800,7 @@ fn emitletdataw(c: *cgen, file: *node) void = { emitline("\"\n"); }; }; - if (sz == primtypesize("str"): i32 && !issg && !istup && !letvarisslice(c, nm)) { + if (sz == primtypesize("str"): i32 && !issg && !istup && !istagged && !letvarisslice(c, nm)) { let r: *node = d.rhs; for (r != nil) { if (r.kind != nkind.N_CAST) { break; }; @@ -39712,7 +39865,7 @@ fn emitletdataw(c: *cgen, file: *node) void = { }; }; }; - if (sz == tyslicesize(): i32 && !issg && letvarisslice(c, nm)) { + if (sz == tyslicesize(): i32 && !issg && !istagged && 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 b25dba12..b83663bf 100644 --- a/selfhost/cmd/wcc/cgen.ww +++ b/selfhost/cmd/wcc/cgen.ww @@ -1017,6 +1017,14 @@ fn letemitsize(c: *cgen, d: *node) i32 = { }; return tsum; }; + // #87: non-nullable tagged-union global — box size (tag word + + // max payload, mirror of the runtime local). Nullable stays 0 so + // the (*T|void) one-word fold keeps the 8B scalar arm in + // emitletdataw. Mirrors cstage let_emit_size TY_TAGGED. + if (t.kind == nkind.N_TTAGGED) { + if (isnullabletype(t)) { return 0; }; + return slotsize(c, t); + }; if (t.kind != nkind.N_TNAME) { return 0; }; let nm: str = t.str; if (letscalarprim(nm)) { return 8; }; @@ -1379,6 +1387,24 @@ export fn letpreintern(c: *cgen, file: *node) void = { }; }; }; + // #87: tagged global with a str/slice-variant literal init — + // pre-intern so emittaggeddata's DATAR (ptr@+8) finds its _S_ + // rodata row (the #48 tuple-arm pattern; cstage letpreintern twin). + if (!handled && r != nil && d.lhs != nil) { + 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_TTAGGED && !isnullabletype(tlt)) { + if (r.kind == nkind.N_STRLIT && r.str.len > 0 + && (nodeisstr(c, r) || nodeisslice(c, r))) { + handled = true; + internstrlit(c, r.str); + }; + }; + }; + }; if (!handled) { let sz: i32 = letemitsize(c, d); // #43: route the str-let gate through primtypesize so @@ -2187,6 +2213,89 @@ fn emitslicedata(c: *cgen, name: str, module: str, slt: *tinfo, emitline(".d(SB)\n"); }; +// emittaggeddata — module-level `let g: (T0 | T1 | ...) = v;` static +// init (#87). Byte-MIRRORS a runtime LOCAL tagged box (rob §3 SSoT pin): +// tag word at +0 (the const-selected variant index, taggedvariantindex — +// the routine the runtime widen + match dispatch key on), payload at +8, +// zero-padded to the union box size `sz`. int and str-literal variants +// are wired (the Hare-stdlib shapes, ref/hare/time/chrono/utc.ha:44); any +// other variant payload returns false and the caller loud-stops (rule 7) — +// never the pre-#87 silent no-DATA + garbage read. Mirror of cstage +// emit_tagged_data. +fn emittaggeddata(c: *cgen, name: str, module: str, tt: *node, rhs: *node, sz: i32) bool = { + if (tt == nil) { return false; }; + if (rhs == nil) { + emitline("DATAW "); + emitsymnamehint(c, name, module); + emitline("(SB),\""); + let zi: i32 = 0; + for (zi < sz) { emitdatawbyte(0u8); zi += 1; }; + emitline("\"\n"); + return true; + }; + let r: *node = rhs; + for (r != nil && r.kind == nkind.N_CAST) { r = r.lhs; }; + if (r == nil) { return false; }; + let tag: i32 = taggedvariantindex(c, tt, r); + if (tag < 0) { return false; }; + let wide: bool = nodeisstr(c, r) || nodeisslice(c, r); + let i: i32 = 0; + let acc: u64 = 0u64; + if (wide) { + if (r.kind != nkind.N_STRLIT) { return false; }; + let lv: u64 = r.str.len: u64; + emitline("DATAW "); + emitsymnamehint(c, name, module); + emitline("(SB),\""); + // tag@0 + acc = tag: u64; + i = 0; + for (i < 8) { emitdatawbyte((acc & 255u64): u8); acc = acc >> 8u64; i += 1; }; + // ptr placeholder@8 + i = 0; + for (i < 8) { emitdatawbyte(0u8); i += 1; }; + // len@16 + acc = lv; + i = 0; + for (i < 8) { emitdatawbyte((acc & 255u64): u8); acc = acc >> 8u64; i += 1; }; + // cap@24 (= len for a static str literal, mirroring the box) + acc = lv; + i = 0; + for (i < 8) { emitdatawbyte((acc & 255u64): u8); acc = acc >> 8u64; i += 1; }; + // pad to sz + i = 32; + for (i < sz) { emitdatawbyte(0u8); i += 1; }; + emitline("\"\n"); + if (r.str.len > 0) { + let lab: str = internstrlit(c, r.str); + emitline("DATAR "); + emitsymnamehint(c, name, module); + emitline("+8(SB),"); + emitbytes( lab.ptr, lab.len: u64); + emitline("(SB)\n"); + }; + return true; + }; + let v: u64 = 0u64; + if (!foldintliteral(r, &v)) { return false; }; + emitline("DATAW "); + emitsymnamehint(c, name, module); + emitline("(SB),\""); + // tag@0 + acc = tag: u64; + i = 0; + for (i < 8) { emitdatawbyte((acc & 255u64): u8); acc = acc >> 8u64; i += 1; }; + // payload@8 + acc = v; + i = 0; + for (i < 8) { emitdatawbyte((acc & 255u64): u8); acc = acc >> 8u64; i += 1; }; + // pad to sz + i = 16; + for (i < sz) { emitdatawbyte(0u8); i += 1; }; + emitline("\"\n"); + return true; +}; + // 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 @@ -2370,6 +2479,24 @@ fn emitletdataw(c: *cgen, file: *node) void = { os.exit(1); }; }; + // #87: non-nullable tagged-union global — emit the box + // mirroring the runtime local (tag + payload). letemitsize + // keeps nullable at 0 so the (*T|void) one-word fold stays + // on the 8B scalar arm below. The istagged gate also keeps + // a tagged box (size can equal str/slice size) out of those. + let istagged: bool = false; + if (tlt != nil) { + if (tlt.kind == nkind.N_TTAGGED) { + if (!isnullabletype(tlt)) { istagged = true; }; + }; + }; + if (istagged) { + if (!emittaggeddata(c, nm, d.nmod, tlt, d.rhs, sz)) { + let mtg: str = "global tagged let: unsupported variant init (int/str literal 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 @@ -2404,7 +2531,7 @@ fn emitletdataw(c: *cgen, file: *node) void = { if (dti != nil) { if (dti.kind == tykind.TY_ARRAY) { isarr8 = true; }; }; - if (sz == 8 && !issg && fsz == 0 && !isarr8 && !istup) { + if (sz == 8 && !issg && fsz == 0 && !isarr8 && !istup && !istagged) { let v: u64 = 0u64; let ok: bool = true; if (d.rhs != nil) { @@ -2435,7 +2562,7 @@ fn emitletdataw(c: *cgen, file: *node) void = { emitline("\"\n"); }; }; - if (sz == primtypesize("str"): i32 && !issg && !istup && !letvarisslice(c, nm)) { + if (sz == primtypesize("str"): i32 && !issg && !istup && !istagged && !letvarisslice(c, nm)) { let r: *node = d.rhs; for (r != nil) { if (r.kind != nkind.N_CAST) { break; }; @@ -2500,7 +2627,7 @@ fn emitletdataw(c: *cgen, file: *node) void = { }; }; }; - if (sz == tyslicesize(): i32 && !issg && letvarisslice(c, nm)) { + if (sz == tyslicesize(): i32 && !issg && !istagged && 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 a4534103..173b47c7 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -2554,7 +2554,33 @@ fn cgmatch(c: *cgen, n: *node) void = { if (lc != nil) { scrutoff = lc.off; scrutt = resolvetagged(c, lc.tnode); - }; + } else { if (isletvar(c, scrut.str)) { + // #87: a tagged-union GLOBAL scrutinee — the box lives + // in static DATA at name(SB), not the BP frame. + // localfindnode returns nil and the dispatch would read + // saved BP as the tag (garbage). The PLAIN-tagged twin + // of cstage #78 SB-resolution: LEAQ the address, copy + // the box into an @match_spill slot indexed off BP. + let gtt: *node = resolvetagged(c, letvartnode(c, scrut.str)); + if (gtt != nil && !isnullabletype(gtt)) { + scrutt = gtt; + let gspill: i32 = matchspillsz(c, gtt); + scrutoff = localalloc(c, "@match_spill", gspill, nil); + emitline("\tLEAQ\t"); + emitsymnamehint(c, scrut.str, c.curmod); + emitline("(SB), AX\n"); + let gk: i32 = 0; + for (gk < gspill) { + emitline("\tMOVQ\t"); + emitdispreg(gk: i64, "AX"); + emitline(", DX\n"); + emitline("\tMOVQ\tDX, "); + emitoff((scrutoff + gk): i64); + emitline("(BP)\n"); + gk += 8; + }; + }; + }; }; } else { // Non-ident scrutinee (call result, arr[i], p.field, // ?, etc.). Spill into an `@match_spill` scratch slot diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 41d8a514..45bee84c 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -23730,7 +23730,33 @@ fn cgmatch(c: *cgen, n: *node) void = { if (lc != nil) { scrutoff = lc.off; scrutt = resolvetagged(c, lc.tnode); - }; + } else { if (isletvar(c, scrut.str)) { + // #87: a tagged-union GLOBAL scrutinee — the box lives + // in static DATA at name(SB), not the BP frame. + // localfindnode returns nil and the dispatch would read + // saved BP as the tag (garbage). The PLAIN-tagged twin + // of cstage #78 SB-resolution: LEAQ the address, copy + // the box into an @match_spill slot indexed off BP. + let gtt: *node = resolvetagged(c, letvartnode(c, scrut.str)); + if (gtt != nil && !isnullabletype(gtt)) { + scrutt = gtt; + let gspill: i32 = matchspillsz(c, gtt); + scrutoff = localalloc(c, "@match_spill", gspill, nil); + emitline("\tLEAQ\t"); + emitsymnamehint(c, scrut.str, c.curmod); + emitline("(SB), AX\n"); + let gk: i32 = 0; + for (gk < gspill) { + emitline("\tMOVQ\t"); + emitdispreg(gk: i64, "AX"); + emitline(", DX\n"); + emitline("\tMOVQ\tDX, "); + emitoff((scrutoff + gk): i64); + emitline("(BP)\n"); + gk += 8; + }; + }; + }; }; } else { // Non-ident scrutinee (call result, arr[i], p.field, // ?, etc.). Spill into an `@match_spill` scratch slot @@ -38229,6 +38255,14 @@ fn letemitsize(c: *cgen, d: *node) i32 = { }; return tsum; }; + // #87: non-nullable tagged-union global — box size (tag word + + // max payload, mirror of the runtime local). Nullable stays 0 so + // the (*T|void) one-word fold keeps the 8B scalar arm in + // emitletdataw. Mirrors cstage let_emit_size TY_TAGGED. + if (t.kind == nkind.N_TTAGGED) { + if (isnullabletype(t)) { return 0; }; + return slotsize(c, t); + }; if (t.kind != nkind.N_TNAME) { return 0; }; let nm: str = t.str; if (letscalarprim(nm)) { return 8; }; @@ -38591,6 +38625,24 @@ export fn letpreintern(c: *cgen, file: *node) void = { }; }; }; + // #87: tagged global with a str/slice-variant literal init — + // pre-intern so emittaggeddata's DATAR (ptr@+8) finds its _S_ + // rodata row (the #48 tuple-arm pattern; cstage letpreintern twin). + if (!handled && r != nil && d.lhs != nil) { + 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_TTAGGED && !isnullabletype(tlt)) { + if (r.kind == nkind.N_STRLIT && r.str.len > 0 + && (nodeisstr(c, r) || nodeisslice(c, r))) { + handled = true; + internstrlit(c, r.str); + }; + }; + }; + }; if (!handled) { let sz: i32 = letemitsize(c, d); // #43: route the str-let gate through primtypesize so @@ -39399,6 +39451,89 @@ fn emitslicedata(c: *cgen, name: str, module: str, slt: *tinfo, emitline(".d(SB)\n"); }; +// emittaggeddata — module-level `let g: (T0 | T1 | ...) = v;` static +// init (#87). Byte-MIRRORS a runtime LOCAL tagged box (rob §3 SSoT pin): +// tag word at +0 (the const-selected variant index, taggedvariantindex — +// the routine the runtime widen + match dispatch key on), payload at +8, +// zero-padded to the union box size `sz`. int and str-literal variants +// are wired (the Hare-stdlib shapes, ref/hare/time/chrono/utc.ha:44); any +// other variant payload returns false and the caller loud-stops (rule 7) — +// never the pre-#87 silent no-DATA + garbage read. Mirror of cstage +// emit_tagged_data. +fn emittaggeddata(c: *cgen, name: str, module: str, tt: *node, rhs: *node, sz: i32) bool = { + if (tt == nil) { return false; }; + if (rhs == nil) { + emitline("DATAW "); + emitsymnamehint(c, name, module); + emitline("(SB),\""); + let zi: i32 = 0; + for (zi < sz) { emitdatawbyte(0u8); zi += 1; }; + emitline("\"\n"); + return true; + }; + let r: *node = rhs; + for (r != nil && r.kind == nkind.N_CAST) { r = r.lhs; }; + if (r == nil) { return false; }; + let tag: i32 = taggedvariantindex(c, tt, r); + if (tag < 0) { return false; }; + let wide: bool = nodeisstr(c, r) || nodeisslice(c, r); + let i: i32 = 0; + let acc: u64 = 0u64; + if (wide) { + if (r.kind != nkind.N_STRLIT) { return false; }; + let lv: u64 = r.str.len: u64; + emitline("DATAW "); + emitsymnamehint(c, name, module); + emitline("(SB),\""); + // tag@0 + acc = tag: u64; + i = 0; + for (i < 8) { emitdatawbyte((acc & 255u64): u8); acc = acc >> 8u64; i += 1; }; + // ptr placeholder@8 + i = 0; + for (i < 8) { emitdatawbyte(0u8); i += 1; }; + // len@16 + acc = lv; + i = 0; + for (i < 8) { emitdatawbyte((acc & 255u64): u8); acc = acc >> 8u64; i += 1; }; + // cap@24 (= len for a static str literal, mirroring the box) + acc = lv; + i = 0; + for (i < 8) { emitdatawbyte((acc & 255u64): u8); acc = acc >> 8u64; i += 1; }; + // pad to sz + i = 32; + for (i < sz) { emitdatawbyte(0u8); i += 1; }; + emitline("\"\n"); + if (r.str.len > 0) { + let lab: str = internstrlit(c, r.str); + emitline("DATAR "); + emitsymnamehint(c, name, module); + emitline("+8(SB),"); + emitbytes( lab.ptr, lab.len: u64); + emitline("(SB)\n"); + }; + return true; + }; + let v: u64 = 0u64; + if (!foldintliteral(r, &v)) { return false; }; + emitline("DATAW "); + emitsymnamehint(c, name, module); + emitline("(SB),\""); + // tag@0 + acc = tag: u64; + i = 0; + for (i < 8) { emitdatawbyte((acc & 255u64): u8); acc = acc >> 8u64; i += 1; }; + // payload@8 + acc = v; + i = 0; + for (i < 8) { emitdatawbyte((acc & 255u64): u8); acc = acc >> 8u64; i += 1; }; + // pad to sz + i = 16; + for (i < sz) { emitdatawbyte(0u8); i += 1; }; + emitline("\"\n"); + return true; +}; + // 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 @@ -39582,6 +39717,24 @@ fn emitletdataw(c: *cgen, file: *node) void = { os.exit(1); }; }; + // #87: non-nullable tagged-union global — emit the box + // mirroring the runtime local (tag + payload). letemitsize + // keeps nullable at 0 so the (*T|void) one-word fold stays + // on the 8B scalar arm below. The istagged gate also keeps + // a tagged box (size can equal str/slice size) out of those. + let istagged: bool = false; + if (tlt != nil) { + if (tlt.kind == nkind.N_TTAGGED) { + if (!isnullabletype(tlt)) { istagged = true; }; + }; + }; + if (istagged) { + if (!emittaggeddata(c, nm, d.nmod, tlt, d.rhs, sz)) { + let mtg: str = "global tagged let: unsupported variant init (int/str literal 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 @@ -39616,7 +39769,7 @@ fn emitletdataw(c: *cgen, file: *node) void = { if (dti != nil) { if (dti.kind == tykind.TY_ARRAY) { isarr8 = true; }; }; - if (sz == 8 && !issg && fsz == 0 && !isarr8 && !istup) { + if (sz == 8 && !issg && fsz == 0 && !isarr8 && !istup && !istagged) { let v: u64 = 0u64; let ok: bool = true; if (d.rhs != nil) { @@ -39647,7 +39800,7 @@ fn emitletdataw(c: *cgen, file: *node) void = { emitline("\"\n"); }; }; - if (sz == primtypesize("str"): i32 && !issg && !istup && !letvarisslice(c, nm)) { + if (sz == primtypesize("str"): i32 && !issg && !istup && !istagged && !letvarisslice(c, nm)) { let r: *node = d.rhs; for (r != nil) { if (r.kind != nkind.N_CAST) { break; }; @@ -39712,7 +39865,7 @@ fn emitletdataw(c: *cgen, file: *node) void = { }; }; }; - if (sz == tyslicesize(): i32 && !issg && letvarisslice(c, nm)) { + if (sz == tyslicesize(): i32 && !issg && !istagged && letvarisslice(c, nm)) { let r: *node = d.rhs; for (r != nil) { if (r.kind != nkind.N_CAST) { break; }; diff --git a/test/wcc/929_tagged_memarg_run.c b/test/wcc/929_tagged_memarg_run.c index 41f3fd23..d9286ffb 100644 --- a/test/wcc/929_tagged_memarg_run.c +++ b/test/wcc/929_tagged_memarg_run.c @@ -489,26 +489,28 @@ static const struct row rows[] = { 0, NULL, NULL, 1, "#38b: sret-class tagged call result as a >48B by-value arg " "unwired (#40-family follow-up)" }, - /* LOUD-STOP: global-let source. Global tagged lets have no - * .data emission at ANY size (let_emit_size returns 0 for - * TY_TAGGED; the ≤48B arg path silently reads stack garbage at - * master — independent latent, filed as its own task). The >48B - * ARG path stops loud instead. No assign to g here — the global - * tagged ASSIGN is the other half of that independent bug and - * fails with its own (non-#38b) resolver diagnostic on wwstage - * post-cgplaceaddr. */ - { "fail_global_src", + /* GRADUATED by #87: tagged-union globals now emit static DATA and + * register as letvars, so a >48B tagged GLOBAL by-value arg resolves + * through the MEMORY-class pre-pass's cgplaceaddr arm (LEAQ g(SB) + + * 56B blit) instead of the #38b loud-stop. Pre-#87 let_emit_size + * returned 0 for TY_TAGGED, so g was not a letvar and the arg path + * stopped loud. An UNINIT tagged global is a zero box = the FIRST + * variant (t_lit rune 0); probe reads tag 0 → the rune arm → 0 != 'x' + * → 91. The arg-path blit is value-agnostic (any box at g(SB) copies + * the same way); a NON-zero >48B init needs a struct-variant literal, + * which #87's int/str-only DATA emit loud-stops — that data-emit + * boundary is pinned in attest_pass.ww (910/997). The first-class- + * VALUE copy of a tagged global (`let q = g`) stays a pre-existing + * silent residual (task #111), independent of this read-into-arg + * path. */ + { "global_uninit_memarg_src", MEM56_TYPES "let g: t_u;\n" MEM56_PROBE "export fn main() i32 = {\n" - " if (probe(g) != 1) { return 1; };\n" - " return 0;\n" + " return probe(g);\n" /* zero box → rune-0 arm → 91 */ "};\n", - /* marker stops before the source-kind number — cstage prints - * the numeric node kind, wwstage doesn't. */ - 0, NULL, NULL, 1, - "#38b: >48B tagged arg from unsupported source kind" }, + 91, NULL, NULL, 0, NULL }, /* LOUD-STOP: >48B tagged VARIADIC element (memory convention * inside the vararg gather buffer — unwired). */ { "fail_variadic_elem", diff --git a/test/wcc/data/attest_pass.ww b/test/wcc/data/attest_pass.ww index 2541c1e4..496183c0 100644 --- a/test/wcc/data/attest_pass.ww +++ b/test/wcc/data/attest_pass.ww @@ -6,6 +6,17 @@ import rt; // #3/B': check_empty_alloc_annotated calls alloc → rt_malloc. type point = struct { x: i32, y: i32 }; +// #87: a PLAIN (non-alias) module-level tagged-union global SEGV'd BOTH +// stages — no static DATA emitted (read as a frame-local at BP) + the +// match scrutinee read saved BP as the tag. Fix: emit the 32B box that +// byte-MIRRORS a runtime LOCAL (tag@0 | payload@8 | zero-pad) + resolve +// the match scrutinee via LEAQ name(SB). These globals back the rob §3 +// global-vs-local byte-identity PIN below; the tuple global locks the +// already-correct #86 path against any emitter regression. +let g87_int: (i32 | str) = 42; +let g87_str: (i32 | str) = "x"; +let g86_tup: (u32, u32) = (1u32, 2u32); + @test fn check_add() void = { let a: i32 = 2; let b: i32 = 3; @@ -127,3 +138,41 @@ type point = struct { x: i32, y: i32 }; let _: i32 = 1 / 0; }; }; + +// #87 PIN (rob §3, tagged kind): a module-GLOBAL tagged scrutinee must +// match to the SAME arm/value as a LOCAL of the same type — both stages. +// Pre-fix the global match SEGV'd (saved-BP-as-tag) on cs AND ww. +@test fn check_tagged_global_int() void = { + let l: (i32 | str) = 42; // the known-correct runtime box + let gv: i32 = match (g87_int) { + case let n: i32 => yield n; + case let s: str => yield 0; + }; + let lv: i32 = match (l) { + case let n: i32 => yield n; + case let s: str => yield 0; + }; + if (gv != 42) { let _: i32 = 1 / 0; }; + if (gv != lv) { let _: i32 = 1 / 0; }; // global == local +}; + +// #87 PIN: the str variant (tag 1) exercises the DATAR ptr patch + the +// 24B {ptr,len,cap} payload at +8. +@test fn check_tagged_global_str() void = { + let n: i32 = match (g87_str) { + case let v: i32 => yield v; + case let s: str => yield s.len: i32; + }; + if (n != 1) { let _: i32 = 1 / 0; }; // "x".len, via tag-1 arm +}; + +// #86 LOCK PIN (tuple kind): the already-correct tuple-global emitter +// must keep byte-mirroring a local — guards the working path against any +// regression from the #87 tagged arm sharing emitletdataw. +@test fn check_tuple_global_lock() void = { + let l: (u32, u32) = (1u32, 2u32); + if (g86_tup.0 != 1) { let _: i32 = 1 / 0; }; + if (g86_tup.1 != 2) { let _: i32 = 1 / 0; }; + if (g86_tup.0 != l.0) { let _: i32 = 1 / 0; }; + if (g86_tup.1 != l.1) { let _: i32 = 1 / 0; }; +};