wcc/ww: emit correct (tag,payload) for a tagged value in static-init (#19)

A tagged-union value nested in module-level array/struct static-init
mis-emitted in both stages: the lit-bytes emitters had no TY_TAGGED
arm, so a tagged element/field fell to the int path and the payload
landed in the TAG word -- match then read the wrong variant. The
zero-placeholder idiom (today the only way to declare a tagged global:
zero-init in static, write at runtime) was correct only by accident
(int-variant zero folds to (0,0), which equals the right (tag0,0)).

Extract a raw-byte core emittaggedbytes/emit_tagged_bytes -- variant
tag@+0, int payload@+8, zero-pad to the slot size; no directive, no
offset, no reloc -- and refactor the scalar tagged emitter to delegate
to it (byte-id-neutral). Add a TY_TAGGED member branch to the array
and struct lit-bytes emitters (both stages) that calls the core at the
existing full-slot stride, before the int fallthrough. Zero stays
(0,0) byte-identical; a non-zero element/field now emits (tag,payload)
correctly.

A wide (str/slice) or struct/>8B payload nested in an aggregate needs
reloc-at-member-offset machinery the aggregate byte-emitters don't
have, so it is loud-rejected (rule 7), deferred to #30; the existing
slice-of-tagged static-init reject is unchanged.

Regenerates the w6c and wwdump combined.ww. Table-driven 843 test:
non-zero array/struct (pre-fix returned the wrong variant), the
non-tag-0 bool-variant edge, byte-id-neutral zero-placeholder rows,
and wide-payload reject rows; each run row also pins cs-vs-ww asm.
This commit is contained in:
2026-06-14 18:11:28 +09:00
parent 34c1051a63
commit 9767ff8fff
6 changed files with 831 additions and 53 deletions

View File

@@ -1757,6 +1757,21 @@ fn emitstructlitbytes(c: *cgen, structt: *tinfo, rhs: *node,
for (vr != nil && vr.kind == nkind.N_CAST) { vr = vr.lhs; };
let fu: *tinfo = f.type_;
fu = tichase(fu);
// #19 option A: a non-nullable tagged-union field rides the shared
// (tag,payload) core at the field slot size, mirroring the scalar
// tagged global — NOT the int emitter. Wide/struct/non-foldable
// payload loud-rejects (task #30 sub-item, rule 7). v is non-nil
// here (the absent-field zero-fill is handled above).
if (fu != nil && fu.kind == tykind.TY_TAGGED && !typeisnullable(fu)) {
if (!emittaggedbytes(c, f.type_, v, fsz, 1)) {
let m: str = "emitstructlitbytes: tagged-union struct-field static-init needs a zero/int payload; wide (str/slice) or struct payload is deferred (task #30, rule 7)\n";
os.write(2, m.ptr, m.len: u64);
os.exit(1);
};
pos = fstart + fsz: u64;
f = f.tnext;
continue;
};
if (fu != nil && fu.kind == tykind.TY_STRUCT) {
if (vr == nil) {
let m: str = "emitstructlitbytes: nested struct field rhs nil (#129 A.2)\n";
@@ -1914,6 +1929,54 @@ fn emitarraylitbytes(c: *cgen, arrt: *tinfo, rhs: *node,
let eu: *tinfo = au.sub;
eu = tichase(eu);
// #19 option A: a non-nullable tagged-union element rides the shared
// (tag,payload) core (emittaggedbytes) at the full slot stride esz,
// mirroring the scalar tagged global — NOT the int emitter, which
// mis-folds the payload into the tag word. A wide/struct/non-foldable
// payload element loud-rejects (task #30 sub-item, rule 7). A nullable
// `(*T|void)` element is a 1-word fold, not a tag box — left to the
// existing int path (task #15).
if (eu != nil && eu.kind == tykind.TY_TAGGED && !typeisnullable(eu)) {
let idx: i32 = 0;
let last_ev: *node = nil;
let e: *node = rhs.list;
for (e != nil && idx < alen) {
if (e.kind == nkind.N_FIELD) {
if (streq(e.str, "...")) { break; };
};
if (!emittaggedbytes(c, au.sub, e, esz, 0)) {
let m: str = "emitarraylitbytes: tagged-union array element static-init needs a zero/int payload; wide (str/slice) or struct payload is deferred (task #30, rule 7)\n";
os.write(2, m.ptr, m.len: u64);
os.exit(1);
};
last_ev = e;
idx += 1;
e = e.next;
};
if (emit_phase == 0) { return true; };
idx = 0;
let repeat: bool = false;
e = rhs.list;
for (e != nil && idx < alen) {
if (e.kind == nkind.N_FIELD) {
if (streq(e.str, "...")) { repeat = true; break; };
};
emittaggedbytes(c, au.sub, e, esz, 1);
idx += 1;
e = e.next;
};
for (idx < alen) {
if (repeat && last_ev != nil) {
emittaggedbytes(c, au.sub, last_ev, esz, 1);
} else {
let bb: i32 = 0;
for (bb < esz) { emitdatawbyte(0u8); bb += 1; };
};
idx += 1;
};
return true;
};
if (eu != nil && eu.kind == tykind.TY_STRUCT) {
// Validate: every element must be N_STRUCTLIT (after N_CAST).
let idx: i32 = 0;
@@ -2481,6 +2544,52 @@ fn emitslicedata(c: *cgen, name: str, module: str, slt: *tinfo,
emitline(".d(SB)\n");
};
// emittaggedbytes — raw sz-byte static-init payload for a tagged-union
// value: variant tag@+0 (8B), int payload@+8 (8B), zero-pad to sz. NO
// directive open/close, NO reloc — emits exactly sz bytes via
// emitdatawbyte at the current cursor. Handles zero (nil rhs) + a
// foldable int payload only; a wide (str/slice) payload needs a DATAR the
// raw core cannot place inside an already-open aggregate directive, and a
// struct/non-foldable payload has no scalar form — both return false
// WITHOUT emitting, and the caller loud-rejects (task #30 wide/struct
// sub-item). `tti` is the union tinfo (NAMED-peeled + TY_TAGGED-gated
// internally by flatvariantidxt). emit_phase 0 = validate only; 1 = emit.
// rob's EXTRACT ruling (#19 option A): the scalar wrapper emittaggeddata
// and the aggregate member branches (emitarraylitbytes/emitstructlitbytes)
// share this raw core so a nested tagged member rides the SAME
// (tag,payload) SSoT as a scalar tagged global. Mirror of cstage
// emit_tagged_bytes.
fn emittaggedbytes(c: *cgen, tti: *tinfo, rhs: *node, sz: i32, emit_phase: i32) bool = {
if (tti == nil) { return false; };
if (rhs == nil) {
if (emit_phase == 1) {
let zi: i32 = 0;
for (zi < sz) { emitdatawbyte(0u8); zi += 1; };
};
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 = flatvariantidxt(tti, r.type_: *tinfo, false);
if (tag < 0) { return false; };
if (nodeisstr(c, r) || nodeisslice(c, r)) { return false; };
let v: u64 = 0u64;
if (!foldintliteral(r, &v)) { return false; };
if (emit_phase == 1) {
let acc: u64 = 0u64;
let i: i32 = 0;
acc = tag: u64;
for (i < 8) { emitdatawbyte((acc & 255u64): u8); acc = acc >> 8u64; i += 1; };
acc = v;
i = 0;
for (i < 8) { emitdatawbyte((acc & 255u64): u8); acc = acc >> 8u64; i += 1; };
i = 16;
for (i < sz) { emitdatawbyte(0u8); i += 1; };
};
return true;
};
// 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 —
@@ -2496,8 +2605,7 @@ fn emittaggeddata(c: *cgen, name: str, module: str, tt: *node, rhs: *node, sz: i
emitline("DATAW ");
emitsymnamehint(c, name, module);
emitline("(SB),\"");
let zi: i32 = 0;
for (zi < sz) { emitdatawbyte(0u8); zi += 1; };
emittaggedbytes(c, tt.type_: *tinfo, rhs, sz, 1);
emitline("\"\n");
return true;
};
@@ -2559,22 +2667,14 @@ fn emittaggeddata(c: *cgen, name: str, module: str, tt: *node, rhs: *node, sz: i
};
return true;
};
let v: u64 = 0u64;
if (!foldintliteral(r, &v)) { return false; };
// int/zero payload via the shared raw core; validate (phase 0) BEFORE
// opening the directive so a non-foldable rhs returns false without
// leaving a half-written DATAW.
if (!emittaggedbytes(c, tt.type_: *tinfo, rhs, sz, 0)) { 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; };
emittaggedbytes(c, tt.type_: *tinfo, rhs, sz, 1);
emitline("\"\n");
return true;
};