From 413aafa599123daf11322961b03bd46b9fc93b8d Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Fri, 5 Jun 2026 02:07:49 +0900 Subject: [PATCH] w6c+w6c_ww: tagged-union struct-lit payload fills via the canonical fill (#23) The widen choke-point's struct-payload arm carried its own inline N_STRUCTLIT field loop -- a parallel fill that drifted from cg_structlit_fill/cgstructlitfill: no tagged-field widen arm, so a (void|T)-typed field's raw scalar landed in the field's TAG word (silent truncation past the first tagged field, both stages, byte-id, gate-blind; prober-9 PG5). Delete both loops and delegate to the canonical fill at the payload base: one fill path, one widen path, mutually recursive. Inherits the nested-struct/call/arrlit field arms and closes a latent fsz==2 cs!=ww (old ww loop's fieldstoreop MOVW vs cstage MOVQ). Test 938: 15-row table-driven runtime readback (incl. ellipsis autofill, offset-0 tagged field, (void|str) payload, 3-level widen-fill recursion torture), all 13 bug rows silent-fail at master 6699158; 2 rows skip the byte-id check loudly (pre-existing match-on-tagged-FIELD readback cs!=ww, master-confirmed, separate family). --- Makefile | 7 + cmd/w6c/cgen.c | 57 +- selfhost/cmd/w6c/main.combined.ww | 70 +-- selfhost/cmd/wcc/cgenutil.ww | 70 +-- selfhost/cmd/wwdump/main.combined.ww | 70 +-- test/wcc/938_tagged_structlit_payload_run.c | 557 ++++++++++++++++++++ 6 files changed, 629 insertions(+), 202 deletions(-) create mode 100644 test/wcc/938_tagged_structlit_payload_run.c diff --git a/Makefile b/Makefile index dd643078..caa2b6bc 100644 --- a/Makefile +++ b/Makefile @@ -309,6 +309,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_tuple_in_union_run \ $(BIN)/test_tuple_slot_layout_run \ $(BIN)/test_tagged_tuple_widen_run \ + $(BIN)/test_tagged_structlit_payload_run \ $(BIN)/test_forrange_fieldbase_run \ $(BIN)/test_errtype_compare \ $(BIN)/test_tuple_elem_slice_len_run \ @@ -1471,6 +1472,12 @@ $(BIN)/test_tagged_tuple_widen_run: test/wcc/936_tagged_tuple_widen_run.c \ $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_tagged_structlit_payload_run: test/wcc/938_tagged_structlit_payload_run.c \ + $(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ + $(BIN)/ww_ww $(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_ww \ + $(LIB)/libwwrt.a | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + $(BIN)/test_forrange_fieldbase_run: test/wcc/937_forrange_fieldbase_run.c \ $(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ $(BIN)/ww_ww $(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_ww \ diff --git a/cmd/w6c/cgen.c b/cmd/w6c/cgen.c index 8e34002c..b0da5e65 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -2606,52 +2606,17 @@ cg_widen_tagged_store(Cg *c, Local **locals_p, Type *dst, Node *src, amem(D_BP, write_off + 8 + k)); } } else if (src->kind == N_STRUCTLIT) { - for (Node *f = src->list; f; f = f->next) { - u64 foff = 0; - int fsz = 8; - Type *ftype = NULL; - for (Tfield *fl = su->fields; fl; fl = fl->next) { - if (strcmp(fl->name, f->str) == 0) { - foff = fl->offset; - fsz = (int)(fl->type ? fl->type->size : 8); - ftype = fl->type; - break; - } - } - cgexpr(c, f->lhs, *locals_p); - int sl_isf32 = 0; - if (fld_isfloat(ftype, &sl_isf32)) { - int mov = sl_isf32 ? A_MOVSS : A_MOVSD; - ins2(c, mov, areg(D_X0), - amem(D_BP, write_off + 8 + (int)foff)); - continue; - } - Type *fu = (ftype && ftype->kind == TY_NAMED) - ? ftype->under : ftype; - /* str IS []u8 and a slice is the same 3-word - * {ptr,len,cap} header from cgexpr's AX/BX/CX - * (#1/Phase 3). The slice arm rides #38b's - * regex-shaped consumer (slice fields inside a - * union-payload struct literal); the prior - * str-only gate dropped .len/.cap via the - * scalar store below — the #24 gap's widener - * twin. */ - if (fu && (fu->kind == TY_STR - || fu->kind == TY_SLICE)) { - ins2(c, A_MOVQ, areg(D_AX), - amem(D_BP, write_off + 8 + (int)foff + 0)); - ins2(c, A_MOVQ, areg(D_BX), - amem(D_BP, write_off + 8 + (int)foff + 8)); - ins2(c, A_MOVQ, areg(D_CX), - amem(D_BP, write_off + 8 + (int)foff + 16)); - continue; - } - int op = A_MOVQ; - if (fsz == 1) op = A_MOVB; - else if (fsz == 4) op = A_MOVL; - ins2(c, op, areg(D_AX), - amem(D_BP, write_off + 8 + (int)foff)); - } + /* #23: delegate to the single fill path. The inline + * field loop this replaces was a parallel fill that + * drifted: it lacked the tagged-field widen arm, so + * a (void|T)-typed field's raw scalar landed in the + * field's TAG word (silent truncation past the first + * tagged field, both stages). Delegation also + * inherits the nested-struct / call / array-lit + * field arms; float / str / slice / scalar fields + * emit byte-identically to the old loop. */ + cg_structlit_fill(c, locals_p, su, src, + DST_BP, 0, NULL, write_off + 8); } ins2(c, A_MOVQ, aimm(tag < 0 ? 0 : tag), amem(D_BP, write_off + 0)); diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index a88aefaa..cc2b933e 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -19565,58 +19565,24 @@ fn cgwidentaggedstorebp(c: *cgen, dst: *tinfo, src: *node, slot_off: i32, slot_s let tag: i32 = taggedvariantindext(c, dt, src); if (tag < 0) { tag = 0; }; if (src.kind == nkind.N_STRUCTLIT) { - let fnode: *node = src.list; - for (fnode != nil) { - if (fnode.kind == nkind.N_FIELD) { - let fname: str = fnode.str; - let fi: *fieldinfo = si.fields; - for (fi != nil) { - if (streq(fi.fname, fname)) { - cgexpr(c, fnode.lhs); - if (isfloattype(c, fi.tnode)) { - let mov: str = "MOVSD"; - if (isf32type(c, fi.tnode)) { - mov = "MOVSS"; - }; - emitline("\t"); - emitline(mov); - emitline("\tX0, "); - emitoff((slot_off + 8 + fi.foff): i64); - emitline("(BP)\n"); - } else { if (isstrtype(c, fi.tnode) - || isslicetype(c, fi.tnode)) { - // str IS []u8 and a slice is the - // same 3-word {ptr,len,cap} header - // from cgexpr's AX/BX/CX (#1/Phase - // 3). Slice arm rides #38b's regex- - // shaped consumer — the prior str- - // only gate dropped .len/.cap (#24 - // gap's widener twin). - emitline("\tMOVQ\tAX, "); - emitoff((slot_off + 8 + fi.foff): i64); - emitline("(BP)\n"); - emitline("\tMOVQ\tBX, "); - emitoff((slot_off + 8 + fi.foff + 8): i64); - emitline("(BP)\n"); - emitline("\tMOVQ\tCX, "); - emitoff((slot_off + 8 + fi.foff + 16): i64); - emitline("(BP)\n"); - } else { - let sop: str = fieldstoreop(c, fi); - emitline("\t"); - emitline(sop); - emitline("\tAX, "); - emitoff((slot_off + 8 + fi.foff): i64); - emitline("(BP)\n"); - }; }; - fi = nil; - } else { - fi = fi.finext; - }; - }; - }; - fnode = fnode.next; - }; + // #23: delegate to the single fill path. The + // inline field loop this replaces was a + // parallel fill that drifted: it lacked the + // tagged-field widen arm, so a (void|T)-typed + // field's raw scalar landed in the field's + // TAG word (silent truncation past the first + // tagged field, both stages). Delegation also + // inherits the nested-struct / call / array- + // lit field arms; float / str / slice / + // scalar fields emit byte-identically to the + // old loop EXCEPT fsz==2 scalars, where the + // old loop's fieldstoreop emitted MOVW + // against cstage's MOVQ — a latent cs≠ww the + // fill's #13-pinned dispatch closes. Mirror + // of cstage cg_widen_tagged_store's + // N_STRUCTLIT arm. + cgstructlitfill(c, si, src, 0, 0, "", + slot_off + 8); } else { // Struct ident source: byte-copy struct words to slot+8+k. let lc: *local = localfindnode(c, src.str); diff --git a/selfhost/cmd/wcc/cgenutil.ww b/selfhost/cmd/wcc/cgenutil.ww index a31c8742..04c1aba7 100644 --- a/selfhost/cmd/wcc/cgenutil.ww +++ b/selfhost/cmd/wcc/cgenutil.ww @@ -3573,58 +3573,24 @@ fn cgwidentaggedstorebp(c: *cgen, dst: *tinfo, src: *node, slot_off: i32, slot_s let tag: i32 = taggedvariantindext(c, dt, src); if (tag < 0) { tag = 0; }; if (src.kind == nkind.N_STRUCTLIT) { - let fnode: *node = src.list; - for (fnode != nil) { - if (fnode.kind == nkind.N_FIELD) { - let fname: str = fnode.str; - let fi: *fieldinfo = si.fields; - for (fi != nil) { - if (streq(fi.fname, fname)) { - cgexpr(c, fnode.lhs); - if (isfloattype(c, fi.tnode)) { - let mov: str = "MOVSD"; - if (isf32type(c, fi.tnode)) { - mov = "MOVSS"; - }; - emitline("\t"); - emitline(mov); - emitline("\tX0, "); - emitoff((slot_off + 8 + fi.foff): i64); - emitline("(BP)\n"); - } else { if (isstrtype(c, fi.tnode) - || isslicetype(c, fi.tnode)) { - // str IS []u8 and a slice is the - // same 3-word {ptr,len,cap} header - // from cgexpr's AX/BX/CX (#1/Phase - // 3). Slice arm rides #38b's regex- - // shaped consumer — the prior str- - // only gate dropped .len/.cap (#24 - // gap's widener twin). - emitline("\tMOVQ\tAX, "); - emitoff((slot_off + 8 + fi.foff): i64); - emitline("(BP)\n"); - emitline("\tMOVQ\tBX, "); - emitoff((slot_off + 8 + fi.foff + 8): i64); - emitline("(BP)\n"); - emitline("\tMOVQ\tCX, "); - emitoff((slot_off + 8 + fi.foff + 16): i64); - emitline("(BP)\n"); - } else { - let sop: str = fieldstoreop(c, fi); - emitline("\t"); - emitline(sop); - emitline("\tAX, "); - emitoff((slot_off + 8 + fi.foff): i64); - emitline("(BP)\n"); - }; }; - fi = nil; - } else { - fi = fi.finext; - }; - }; - }; - fnode = fnode.next; - }; + // #23: delegate to the single fill path. The + // inline field loop this replaces was a + // parallel fill that drifted: it lacked the + // tagged-field widen arm, so a (void|T)-typed + // field's raw scalar landed in the field's + // TAG word (silent truncation past the first + // tagged field, both stages). Delegation also + // inherits the nested-struct / call / array- + // lit field arms; float / str / slice / + // scalar fields emit byte-identically to the + // old loop EXCEPT fsz==2 scalars, where the + // old loop's fieldstoreop emitted MOVW + // against cstage's MOVQ — a latent cs≠ww the + // fill's #13-pinned dispatch closes. Mirror + // of cstage cg_widen_tagged_store's + // N_STRUCTLIT arm. + cgstructlitfill(c, si, src, 0, 0, "", + slot_off + 8); } else { // Struct ident source: byte-copy struct words to slot+8+k. let lc: *local = localfindnode(c, src.str); diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 2b46e9c8..d19d809a 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -19565,58 +19565,24 @@ fn cgwidentaggedstorebp(c: *cgen, dst: *tinfo, src: *node, slot_off: i32, slot_s let tag: i32 = taggedvariantindext(c, dt, src); if (tag < 0) { tag = 0; }; if (src.kind == nkind.N_STRUCTLIT) { - let fnode: *node = src.list; - for (fnode != nil) { - if (fnode.kind == nkind.N_FIELD) { - let fname: str = fnode.str; - let fi: *fieldinfo = si.fields; - for (fi != nil) { - if (streq(fi.fname, fname)) { - cgexpr(c, fnode.lhs); - if (isfloattype(c, fi.tnode)) { - let mov: str = "MOVSD"; - if (isf32type(c, fi.tnode)) { - mov = "MOVSS"; - }; - emitline("\t"); - emitline(mov); - emitline("\tX0, "); - emitoff((slot_off + 8 + fi.foff): i64); - emitline("(BP)\n"); - } else { if (isstrtype(c, fi.tnode) - || isslicetype(c, fi.tnode)) { - // str IS []u8 and a slice is the - // same 3-word {ptr,len,cap} header - // from cgexpr's AX/BX/CX (#1/Phase - // 3). Slice arm rides #38b's regex- - // shaped consumer — the prior str- - // only gate dropped .len/.cap (#24 - // gap's widener twin). - emitline("\tMOVQ\tAX, "); - emitoff((slot_off + 8 + fi.foff): i64); - emitline("(BP)\n"); - emitline("\tMOVQ\tBX, "); - emitoff((slot_off + 8 + fi.foff + 8): i64); - emitline("(BP)\n"); - emitline("\tMOVQ\tCX, "); - emitoff((slot_off + 8 + fi.foff + 16): i64); - emitline("(BP)\n"); - } else { - let sop: str = fieldstoreop(c, fi); - emitline("\t"); - emitline(sop); - emitline("\tAX, "); - emitoff((slot_off + 8 + fi.foff): i64); - emitline("(BP)\n"); - }; }; - fi = nil; - } else { - fi = fi.finext; - }; - }; - }; - fnode = fnode.next; - }; + // #23: delegate to the single fill path. The + // inline field loop this replaces was a + // parallel fill that drifted: it lacked the + // tagged-field widen arm, so a (void|T)-typed + // field's raw scalar landed in the field's + // TAG word (silent truncation past the first + // tagged field, both stages). Delegation also + // inherits the nested-struct / call / array- + // lit field arms; float / str / slice / + // scalar fields emit byte-identically to the + // old loop EXCEPT fsz==2 scalars, where the + // old loop's fieldstoreop emitted MOVW + // against cstage's MOVQ — a latent cs≠ww the + // fill's #13-pinned dispatch closes. Mirror + // of cstage cg_widen_tagged_store's + // N_STRUCTLIT arm. + cgstructlitfill(c, si, src, 0, 0, "", + slot_off + 8); } else { // Struct ident source: byte-copy struct words to slot+8+k. let lc: *local = localfindnode(c, src.str); diff --git a/test/wcc/938_tagged_structlit_payload_run.c b/test/wcc/938_tagged_structlit_payload_run.c new file mode 100644 index 00000000..440dda89 --- /dev/null +++ b/test/wcc/938_tagged_structlit_payload_run.c @@ -0,0 +1,557 @@ +/* + * 938_tagged_structlit_payload_run — #23 (fold-5b prereq): tagged-union + * CONSTRUCTION from an INLINE struct-literal payload whose fields are + * themselves tagged ((void|size) — the regex inst_repeat shape, + * ref/hare/regex/regex.ha:55-60). Pre-#23 the widen choke-point's + * struct-payload arm (cg_widen_tagged_store / cgwidentaggedstorebp) + * carried its OWN inline N_STRUCTLIT field loop — a parallel fill that + * drifted from cg_structlit_fill / cgstructlitfill: it had float/str/ + * slice/scalar arms but NO tagged-field widen arm, so a (void|T)-typed + * field's raw scalar landed in the field's TAG word (silent truncation + * past the first tagged field; both stages, byte-id — gate-blind; + * prober-9 PG5 evidence, re-confirmed at 6699158). The fix deletes the + * parallel loop and delegates to the canonical fill, which also + * inherits the nested-struct field arm. + * + * row | shape | want + * ---------------------+----------------------------------------+----- + * inst_repeat_shape | rep{id,origin,min=void,max=5:size} — | + * | the PG5b repro; 48B payload | 0 + * rep3_allsize | {id,min,max} all set via `: size` | + * | casts — PG5g; tag+payload word checks | 0 + * tagged_only16 | single (void|size) field — 16B payload | 0 + * prebound_tagged_field| min = mn (pre-bound (void|size) LOCAL | + * | — tagged→tagged subset inside the fill)| 0 + * str_and_tagged | {s:str, m:(void|size)} — 3-word str | + * | arm + tagged arm in one literal | 0 + * variant_pos0 | union (rep|lit) — struct variant tag 0 | 0 + * slice_elem_cursorfit | 24B payload through append + indexed | + * | match (union 32B = exactly the 4-word | + * | tagged cursor; >32B indexed reads are | + * | the open over-cap cursor residual, | + * | task #37 — NOT pinned here) | 0 + * nested_struct_field | nested struct-typed field literal in a | + * | union payload — delegation-inherited | + * | recursion arm (pre-#23: AX-only store) | 0 + * ellipsis_autofill | `...` autofill + tagged field — the | + * | fill's TK_ELLIPSIS zero-fill runs at | + * | the widened slot's payload base | 0 + * two_tagged_offset0 | tagged field at struct OFFSET 0, a 2nd | + * | tagged field, then a trailing scalar | 0 + * nested_and_tagged | nested struct field AND tagged field | + * | in ONE literal — fill→widen→fill, both | + * | recursion directions in one shape | 0 + * voidstr_field | (void|str) tagged field — 3-word str | + * | payload through the fill's widen arm; | + * | pre-#23 the garbage tag matched NO | + * | variant (fallthrough-proof readback) | 0 + * recursion_torture | 3-level tagged(struct{tagged(struct{ | + * | tagged(leaf-lit)})}) — the widen↔fill | + * | mutual recursion terminates + correct | 0 + * payload8_boundary | struct payload exactly 8B (single size | + * | field) — smallest-payload guard | 0 + * scalar_neighbor | scalar+f64+str payload, NO tagged | + * | field — emission no-regress guard | 0 + * + * Field readback goes through MATCH BINDINGS only: direct field reads + * on a struct-w/-tagged-field LOCAL are the open #24 (wwstage + * field(SB) fallback), and tagged-field inits use explicit `: size` + * casts: the BARE untyped-int form is the open #33 wwstage mis-tag. + * Both are filed separately and out of #23's arm. A tagged field + * inside a NESTED plain-struct field is the open #38 family (fails + * identically for plain locals — pre-existing in the canonical fill/ + * read path, NOT a #23 regression; probed both ways at 6699158) and + * is deliberately NOT a row here. + * + * Every K_RUN row also asserts cstage/wwstage asm byte-id. NNN<950, + * self-contained (/tmp, no imports) — rule-14's selfhost-sibling race + * does not apply (936/941 precedent). + */ +#include +#include +#include +#include +#include +#include + +static int +runwait(const char *cmd) +{ + int rc = system(cmd); + if (rc == -1) return -1; + if (WIFEXITED(rc)) return WEXITSTATUS(rc); + return -1; +} + +static int +slurp_eq(const char *a, const char *b) +{ + FILE *fa = fopen(a, "rb"); + FILE *fb = fopen(b, "rb"); + if (!fa || !fb) { if (fa) fclose(fa); if (fb) fclose(fb); return -1; } + int rc = 0; + for (;;) { + int ca = fgetc(fa), cb = fgetc(fb); + if (ca != cb) { rc = -1; break; } + if (ca == EOF) break; + } + fclose(fa); fclose(fb); + return rc; +} + +/* noasm: skip the cs/ww byte-id check for rows whose READBACK needs an + * inner `match` on a tagged FIELD scrutinee — wwstage routes that load + * through a 4-word cursor scratch where cstage copies field words + * directly (PRE-EXISTING divergence, confirmed at master 6699158 on + * the same sources; construction asm is byte-id up to the readback — + * the regex-arc match-on-tagged-struct-field family, needs a filed + * task). The skip is printed loudly so it can't rot silently. */ +struct row { const char *label; const char *src; int want; int noasm; }; + +static const struct row rows[] = { + /* the PG5b repro verbatim: only id/origin survived pre-#23 + * (min's tag word read 0 = void by accident, max's read 5). */ + { "inst_repeat_shape", + "package main;\n" + "type inst_lit = rune;\n" + "type rep = struct {\n" + " id: size,\n" + " origin: size,\n" + " min: (void | size),\n" + " max: (void | size),\n" + "};\n" + "type inst = (inst_lit | rep);\n" + "export fn main() i32 = {\n" + " let v: inst = rep { id = 1: size, origin = 2: size, min = void, max = 5: size };\n" + " match (v) {\n" + " case let ir: rep => {\n" + " if (ir.id != 1) { return 1; };\n" + " if (ir.origin != 2) { return 2; };\n" + " if (!(ir.min is void)) { return 3; };\n" + " if (!(ir.max is size)) { return 4; };\n" + " if (ir.max as size != 5) { return 5; };\n" + " };\n" + " case let l: inst_lit => { return 6; };\n" + " };\n" + " return 0;\n" + "};\n", 0, 0 }, + /* PG5g: every tagged field set — pre-#23 the raw 2/5 landed in + * the tag words and both is-tests failed. */ + { "rep3_allsize", + "package main;\n" + "type inst_lit = rune;\n" + "type rep3 = struct { id: size, min: (void | size), max: (void | size) };\n" + "type inst = (inst_lit | rep3);\n" + "export fn main() i32 = {\n" + " let v: inst = rep3 { id = 1: size, min = 2: size, max = 5: size };\n" + " match (v) {\n" + " case let ir: rep3 => {\n" + " if (ir.id != 1) { return 1; };\n" + " if (!(ir.min is size)) { return 2; };\n" + " if (ir.min as size != 2) { return 3; };\n" + " if (!(ir.max is size)) { return 4; };\n" + " if (ir.max as size != 5) { return 5; };\n" + " };\n" + " case let l: inst_lit => { return 6; };\n" + " };\n" + " return 0;\n" + "};\n", 0, 0 }, + /* minimal payload: the tagged field IS the whole struct. */ + { "tagged_only16", + "package main;\n" + "type only = struct { m: (void | size) };\n" + "type u = (rune | only);\n" + "export fn main() i32 = {\n" + " let v: u = only { m = 7: size };\n" + " match (v) {\n" + " case let o: only => {\n" + " if (!(o.m is size)) { return 1; };\n" + " if (o.m as size != 7) { return 2; };\n" + " };\n" + " case => return 3;\n" + " };\n" + " return 0;\n" + "};\n", 0, 0 }, + /* field value is a pre-bound tagged LOCAL — the fill's widen arm + * takes the tagged→tagged identity-copy route. */ + { "prebound_tagged_field", + "package main;\n" + "type inst_lit = rune;\n" + "type rep3 = struct { id: size, min: (void | size), max: (void | size) };\n" + "type inst = (inst_lit | rep3);\n" + "export fn main() i32 = {\n" + " let mn: (void | size) = 2: size;\n" + " let v: inst = rep3 { id = 3: size, min = mn, max = void };\n" + " match (v) {\n" + " case let ir: rep3 => {\n" + " if (ir.id != 3) { return 1; };\n" + " if (!(ir.min is size)) { return 2; };\n" + " if (ir.min as size != 2) { return 3; };\n" + " if (!(ir.max is void)) { return 4; };\n" + " };\n" + " case let l: inst_lit => { return 5; };\n" + " };\n" + " return 0;\n" + "};\n", 0, 0 }, + /* str field (3-word arm) and tagged field in the same literal — + * the arms must not disturb each other's offsets. */ + { "str_and_tagged", + "package main;\n" + "type inst_lit = rune;\n" + "type named = struct { s: str, m: (void | size) };\n" + "type inst = (inst_lit | named);\n" + "export fn main() i32 = {\n" + " let v: inst = named { s = \"hi\", m = 9: size };\n" + " match (v) {\n" + " case let nm: named => {\n" + " if (len(nm.s) != 2) { return 1; };\n" + " if (!(nm.m is size)) { return 2; };\n" + " if (nm.m as size != 9) { return 3; };\n" + " };\n" + " case let l: inst_lit => { return 4; };\n" + " };\n" + " return 0;\n" + "};\n", 0, 0 }, + /* struct variant FIRST in the union — tag 0; pins that the fix + * is position-independent. */ + { "variant_pos0", + "package main;\n" + "type inst_lit = rune;\n" + "type rep3 = struct { id: size, min: (void | size), max: (void | size) };\n" + "type inst = (rep3 | inst_lit);\n" + "export fn main() i32 = {\n" + " let v: inst = rep3 { id = 1: size, min = void, max = 8: size };\n" + " match (v) {\n" + " case let ir: rep3 => {\n" + " if (ir.id != 1) { return 1; };\n" + " if (!(ir.min is void)) { return 2; };\n" + " if (!(ir.max is size)) { return 3; };\n" + " if (ir.max as size != 8) { return 4; };\n" + " };\n" + " case let l: inst_lit => { return 5; };\n" + " };\n" + " return 0;\n" + "};\n", 0, 0 }, + /* construction → append → indexed match, end-to-end. Payload 24B + * keeps the union at 32B — exactly the 4-word tagged cursor, so + * the indexed read is in-cap. The >32B indexed read truncates at + * the cursor (open over-cap residual, #22b/#36 family) — the + * 48B-payload rows above therefore match the LOCAL only. */ + { "slice_elem_cursorfit", + "package main;\n" + "type inst_lit = rune;\n" + "type rep1 = struct { id: size, m: (void | size) };\n" + "type inst = (inst_lit | rep1);\n" + "export fn main() i32 = {\n" + " let insts: []inst = [];\n" + " append(insts, ('a': inst_lit));\n" + " let v: inst = rep1 { id = 4: size, m = 6: size };\n" + " append(insts, v);\n" + " if (len(insts) != 2) { return 1; };\n" + " match (insts[1]) {\n" + " case let r: rep1 => {\n" + " if (r.id != 4) { return 2; };\n" + " if (!(r.m is size)) { return 3; };\n" + " if (r.m as size != 6) { return 4; };\n" + " };\n" + " case let l: inst_lit => { return 5; };\n" + " };\n" + " return 0;\n" + "};\n", 0, 0 }, + /* nested struct-typed field literal inside the union payload — + * rides the canonical fill's recursion arm the delegation + * inherits (pre-#23 the parallel loop stored AX only). */ + { "nested_struct_field", + "package main;\n" + "type inner = struct { a: size, b: size };\n" + "type outer = struct { id: size, in_: inner };\n" + "type u = (rune | outer);\n" + "export fn main() i32 = {\n" + " let v: u = outer { id = 1: size, in_ = inner { a = 2: size, b = 3: size } };\n" + " match (v) {\n" + " case let o: outer => {\n" + " if (o.id != 1) { return 1; };\n" + " if (o.in_.a != 2) { return 2; };\n" + " if (o.in_.b != 3) { return 3; };\n" + " };\n" + " case => return 4;\n" + " };\n" + " return 0;\n" + "};\n", 0, 0 }, + /* `...` autofill + tagged field (ken n7): the fill's TK_ELLIPSIS + * zero-fill loop must run at the widened slot's payload base, not + * the slot base — pre-#23 exit 3 (raw 5 in m's tag word). */ + { "ellipsis_autofill", + "package main;\n" + "type st = struct { a: u64, m: (void | size) };\n" + "export fn main() i32 = {\n" + " let e: (void | st) = st { m = 5: size, ... };\n" + " match (e) {\n" + " case let s: st => {\n" + " if (s.a != 0u64) { return 2; };\n" + " if (!(s.m is size)) { return 3; };\n" + " if (s.m as size != 5) { return 4; };\n" + " };\n" + " case void => { return 9; };\n" + " };\n" + " return 0;\n" + "};\n", 0, 0 }, + /* tagged field at struct OFFSET 0 plus a second tagged field and + * a TRAILING scalar (ken n2b) — pins offset-0 tag placement and + * the scalar offset computed past two 16B tagged fields. */ + { "two_tagged_offset0", + "package main;\n" + "type d2 = struct { m: (void | size), n: (void | i64), z: u64 };\n" + "export fn main() i32 = {\n" + " let e: (void | d2) = d2 { m = 4: size, n = (-9): i64, z = 77u64 };\n" + " match (e) {\n" + " case let s: d2 => {\n" + " if (!(s.m is size)) { return 2; };\n" + " if (s.m as size != 4) { return 3; };\n" + " if (!(s.n is i64)) { return 4; };\n" + " if (s.n as i64 != -9) { return 5; };\n" + " if (s.z != 77u64) { return 6; };\n" + " };\n" + " case void => { return 9; };\n" + " };\n" + " return 0;\n" + "};\n", 0, 0 }, + /* nested struct field AND tagged field in the SAME literal (ken + * n5b) — exercises fill→fill (nested) and fill→widen (tagged) + * from one delegated call. */ + { "nested_and_tagged", + "package main;\n" + "type inn = struct { v: u64 };\n" + "type outt = struct { i: inn, m: (void | size) };\n" + "export fn main() i32 = {\n" + " let e: (void | outt) = outt { i = inn { v = 11u64 }, m = 8: size };\n" + " match (e) {\n" + " case let s: outt => {\n" + " if (s.i.v != 11u64) { return 2; };\n" + " if (!(s.m is size)) { return 3; };\n" + " if (s.m as size != 8) { return 4; };\n" + " };\n" + " case void => { return 9; };\n" + " };\n" + " return 0;\n" + "};\n", 0, 0 }, + /* (void|str) tagged field — 3-word str payload through the fill's + * widen arm. Readback initialises `got = 9` BEFORE the inner + * match so a garbage tag matching NO variant can't fall through + * to a fake 0 (pre-#23 the raw str ptr landed in the tag word + * and exactly that fallthrough faked a pass). */ + { "voidstr_field", + "package main;\n" + "type st = struct { id: size, s: (void | str) };\n" + "type u = (rune | st);\n" + "export fn main() i32 = {\n" + " let v: u = st { id = 3: size, s = \"hello\" };\n" + " match (v) {\n" + " case let o: st => {\n" + " if (o.id != 3) { return 1; };\n" + " let got: i32 = 9;\n" + " match (o.s) {\n" + " case let sv: str => {\n" + " if (len(sv) == 5) { got = 0; } else { got = 2; };\n" + " };\n" + " case void => { got = 3; };\n" + " };\n" + " return got;\n" + " };\n" + " case => return 4;\n" + " };\n" + " return 5;\n" + "};\n", 0, 1 }, + /* 3-level widen↔fill mutual recursion: tagged(struct{tagged( + * struct{tagged(struct-lit)})}) — termination AND correctness of + * the #23 delegation cycle (pre-#23 exit 4: mid's tag dropped). */ + { "recursion_torture", + "package main;\n" + "type leaf = struct { p: size };\n" + "type t1 = (void | leaf);\n" + "type mid = struct { t: t1 };\n" + "type t2 = (void | mid);\n" + "type outr = struct { id: size, t: t2 };\n" + "type u = (rune | outr);\n" + "export fn main() i32 = {\n" + " let v: u = outr { id = 1: size, t = mid { t = leaf { p = 9: size } } };\n" + " match (v) {\n" + " case let o: outr => {\n" + " if (o.id != 1) { return 1; };\n" + " match (o.t) {\n" + " case let m: mid => {\n" + " match (m.t) {\n" + " case let l: leaf => {\n" + " if (l.p != 9) { return 2; };\n" + " return 0;\n" + " };\n" + " case void => { return 3; };\n" + " };\n" + " };\n" + " case void => { return 4; };\n" + " };\n" + " };\n" + " case => return 5;\n" + " };\n" + " return 6;\n" + "};\n", 0, 1 }, + /* struct payload exactly 8B — the smallest payload through the + * widen→fill delegation (boundary guard; worked pre-#23 too). */ + { "payload8_boundary", + "package main;\n" + "type one = struct { a: size };\n" + "type u = (rune | one);\n" + "export fn main() i32 = {\n" + " let v: u = one { a = 42: size };\n" + " match (v) {\n" + " case let o: one => { if (o.a != 42) { return 1; }; };\n" + " case => return 2;\n" + " };\n" + " return 0;\n" + "};\n", 0, 0 }, + /* no tagged field at all — the previously-working scalar/float/ + * str arms must emit unchanged through the delegation. */ + { "scalar_neighbor", + "package main;\n" + "type plain = struct { a: size, f: f64, s: str };\n" + "type u = (rune | plain);\n" + "export fn main() i32 = {\n" + " let v: u = plain { a = 11: size, f = 2.5, s = \"xyz\" };\n" + " match (v) {\n" + " case let p: plain => {\n" + " if (p.a != 11) { return 1; };\n" + " if (p.f != 2.5) { return 2; };\n" + " if (len(p.s) != 3) { return 3; };\n" + " };\n" + " case => return 4;\n" + " };\n" + " return 0;\n" + "};\n", 0, 0 }, +}; + +static int +run_driver(const char *driver, const struct row *r, int i) +{ + char src[96], tmpdir[96], errf[96], cmd[1024]; + snprintf(src, sizeof src, "/tmp/tsp_%d_%d.ww", getpid(), i); + snprintf(tmpdir, sizeof tmpdir, "/tmp/tsp_%d_d_%d", getpid(), i); + snprintf(errf, sizeof errf, "/tmp/tsp_%d_e_%d", getpid(), i); + + FILE *f = fopen(src, "wb"); + if (!f) return -1; + fputs(r->src, f); + fclose(f); + + mkdir(tmpdir, 0755); + snprintf(cmd, sizeof cmd, "cd %s && %s build %s >/dev/null 2>%s", + tmpdir, driver, src, errf); + int brc = runwait(cmd); + if (brc != 0) { + fprintf(stderr, "row[%s]: build via %s failed\n", + r->label, driver); + unlink(src); unlink(errf); rmdir(tmpdir); + return -1; + } + + const char *base = strrchr(src, '/'); + base = base ? base + 1 : src; + char outbin[256]; + snprintf(outbin, sizeof outbin, "%s/%s", tmpdir, base); + char *dot = strrchr(outbin, '.'); + if (dot && strcmp(dot, ".ww") == 0) *dot = '\0'; + int got = runwait(outbin); + + unlink(src); unlink(outbin); unlink(errf); rmdir(tmpdir); + if (got != r->want) { + fprintf(stderr, "row[%s]: %s exit %d, want %d\n", + r->label, driver, got, r->want); + return 1; + } + return 0; +} + +/* cs==ww .s byte-id (rule 10). */ +static int +asm_byte_identical(const char *bin, const struct row *r, int i) +{ + char src[96], cs[96], ws[96], cmd[1024]; + snprintf(src, sizeof src, "/tmp/tsp_asm_%d_%d.ww", getpid(), i); + snprintf(cs, sizeof cs, "/tmp/tsp_asm_%d_%d_c.s", getpid(), i); + snprintf(ws, sizeof ws, "/tmp/tsp_asm_%d_%d_w.s", getpid(), i); + + FILE *f = fopen(src, "wb"); + if (!f) return -1; + fputs(r->src, f); + fclose(f); + + snprintf(cmd, sizeof cmd, "%s/w6c -o %s %s 2>/dev/null", bin, cs, src); + if (runwait(cmd) != 0) { + fprintf(stderr, "row[%s]: w6c errored\n", r->label); + unlink(src); + return -1; + } + snprintf(cmd, sizeof cmd, "%s/w6c_ww -o %s %s 2>/dev/null", + bin, ws, src); + if (runwait(cmd) != 0) { + fprintf(stderr, "row[%s]: w6c_ww errored\n", r->label); + unlink(src); unlink(cs); + return -1; + } + int rc = slurp_eq(cs, ws); + if (rc != 0) + fprintf(stderr, "row[%s]: cstage vs wwstage asm differs\n", + r->label); + unlink(src); unlink(cs); unlink(ws); + return rc; +} + +int +main(void) +{ + const char *bin = getenv("BIN"); + if (!bin) bin = "out/bin"; + char absbin[2080]; + if (bin[0] != '/') { + char cwd[1024]; + if (getcwd(cwd, sizeof cwd) == NULL) return 1; + snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin); + bin = absbin; + } + + char cdrv[2120], wdrv[2120]; + snprintf(cdrv, sizeof cdrv, "%s/ww", bin); + snprintf(wdrv, sizeof wdrv, "%s/ww_ww", bin); + + int n = (int)(sizeof rows / sizeof rows[0]); + int total = 0, fail = 0; + + for (int i = 0; i < n; i++) { + total++; + if (run_driver(cdrv, &rows[i], i) != 0) fail++; + } + if (access(wdrv, X_OK) == 0) { + for (int i = 0; i < n; i++) { + total++; + if (run_driver(wdrv, &rows[i], i) != 0) fail++; + } + for (int i = 0; i < n; i++) { + if (rows[i].noasm) { + printf("row[%s]: asm byte-id SKIPPED " + "(pre-existing match-on-tagged-field " + "cs/ww readback divergence, " + "master-confirmed)\n", rows[i].label); + continue; + } + total++; + if (asm_byte_identical(bin, &rows[i], i) != 0) fail++; + } + } + + if (fail) { + fprintf(stderr, "tagged_structlit_payload: %d/%d checks failed\n", + fail, total); + return 1; + } + printf("tagged_structlit_payload: %d/%d ok\n", total, total); + return 0; +}