wcc/cgen: #87 plain tagged-union module-global DATA + match SB-resolution (both-stage)

A PLAIN (non-alias) module-level tagged-union global SEGV'd on BOTH
stages: no static DATA was emitted (let_emit_size/letemitsize returned 0
for TY_TAGGED) so the global was never registered, and the match
scrutinee resolved it as a frame-local at offset 0 — reading saved BP as
the tag. Two sub-sites, one route (neither half ships alone — DATA
without SB-resolution still SEGVs; SB-resolution without DATA reads
nothing):

(a) DATA-emitter — a non-nullable TY_TAGGED arm emits the box that
    byte-MIRRORS a runtime LOCAL of the same type: tag word at +0 (the
    const-selected variant index via cg_tag_for_variant / taggedvariant-
    index), payload at +8, zero-padded to the union box size. int and
    str/slice literal variants are wired (str carries a DATAR ptr patch
    at +8); any other variant payload loud-stops (rule 7). emit_tagged_
    data + emittaggeddata are the per-stage twins; let_pre_intern/
    letpreintern gain the matching str-variant intern. Nullable stays 0
    so the (*T|void) one-word fold keeps the 8B scalar arm.

(b) match-scrutinee global resolution — the PLAIN-tagged twin of #78:
    a global tagged ident scrutinee LEAQs name(SB) and copies the box
    into an @match_spill slot the dispatch indexes off BP.

DATA target (mirror of the local box, verified byte-for-byte): for
(i32|str)=42 the 32B box is tag0 | 42@8 | zero-pad; for ="x" it is
tag1 | ptr0@8(DATAR _S_n) | len@16 | cap@24. cs and ww emit byte-
identical asm.

Pins (rob §3, dual-stage 910 cstage + 997 wwstage, attest_pass.ww): the
tagged global-vs-local byte-identity pin (match over the GLOBAL gives the
same arm/value as over a LOCAL — was SEGV both stages) and the str-
variant tag-1 pin, plus the #86 tuple global-vs-local lock-pin guarding
the already-correct emitter path.

929 fail_global_src graduates: a >48B tagged GLOBAL by-value arg now
resolves through the cgplaceaddr MEMORY-class arm (LEAQ g(SB) + blit)
instead of the #38b loud-stop, and runs correctly (uninit zero box ->
first variant); the row becomes a positive run pin. The struct-variant
>48B init still loud-stops via the data emitter.

The first-class-VALUE copy of a tagged ident (`let q = g`) stays a
pre-existing silent #49/#46 sibling (local and global identically),
filed separately — out of this fold's two sub-sites.
This commit is contained in:
2026-06-06 11:44:28 +09:00
parent 3546673756
commit 4459a49d3a
7 changed files with 656 additions and 28 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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