From 06b0fea98b042858400c5715d6b063f7eaa3dff4 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Thu, 4 Jun 2026 23:46:11 +0900 Subject: [PATCH] w6c+w6c_ww: struct-lit store into indexed/deref/field place fills via resolver (#20) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A struct-LITERAL rhs aimed at an N_INDEX element (a[i] = pt{...}, (*ts)[i].caps[k] = capture{...}), an N_UN deref place (*p = pt{...}), or an indexed-base FIELD place (a[i].f = pt{...}, reviewer-20 sibling) fell to a scalar store tail in BOTH stages: cgexpr on a struct literal emits nothing (AX=0) and one MOVQ zeroed the place's first word — every field silently dropped, a str-leading element's content.ptr nulled (downstream SEGFAULT). Byte-identically wrong, so every byte-id gate was blind; runtime pins added. Fix: divert struct-lit-rhs INDEX/UN-STAR/DOT-over-INDEX places past the legacy arms and widen the F6 assign-resolver gate (N_DOT -> N_DOT|N_INDEX|N_UN); the existing C1.25 aggregate arm materialises the literal into a fresh per-use @placescr slot and word-copies to the cgplaceaddr-resolved address. No new path; @placescr alloc site stays single per stage. Rider (task #32): an array-LITERAL rhs at assignment — unwired for EVERY place kind, same silent zero-word tail — now dies loud at one choke-point until the fill lands; build-fail rows pin it. Gates regex fold-5a (run_thread groupstart capture store, regex.ha:643-651). Residual adjacent gaps (deref ident-rhs truncation, >24B ident reassign cs!=ww, struct compound acceptance, value-global DATAW, tuple-lit deref truncation, CALL-rhs RAX-only store) probed pre-existing and filed as tasks #31 A-G / #32. --- Makefile | 12 + cmd/w6c/cgen.c | 63 ++- selfhost/cmd/w6c/main.combined.ww | 78 +++- selfhost/cmd/wcc/cgenexpr.ww | 78 +++- selfhost/cmd/wwdump/main.combined.ww | 78 +++- test/wcc/809_idx_structlit_store.c | 549 +++++++++++++++++++++++++++ 6 files changed, 832 insertions(+), 26 deletions(-) create mode 100644 test/wcc/809_idx_structlit_store.c diff --git a/Makefile b/Makefile index a490ceb5..dd643078 100644 --- a/Makefile +++ b/Makefile @@ -396,6 +396,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_delete_range \ $(BIN)/test_insert_elem \ $(BIN)/test_arrlit_overlong \ + $(BIN)/test_idx_structlit_store \ $(BIN)/test_placeaddr_store \ $(BIN)/test_tryprop_multisuccess \ $(BIN)/test_append_place \ @@ -1073,6 +1074,17 @@ $(BIN)/test_placeaddr_store: test/wcc/805_placeaddr_store.c \ $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +# Task #20: struct-LITERAL store into an INDEXED element (`a[i] = +# pt{...}`, `(*ts)[i].caps[k] = capture{...}`) and a DEREF place +# (`*p = pt{...}`) — pre-fix a byte-identical SILENT no-op in both +# stages (gate-blind; the regex fold-5a groupstart capture store). +# Runtime readback rows + asm byte-id per row on both drivers. +$(BIN)/test_idx_structlit_store: test/wcc/809_idx_structlit_store.c \ + $(BIN)/ww $(BIN)/w6c $(BIN)/w6c_ww $(BIN)/w6a $(BIN)/w6l \ + $(BIN)/ww_ww \ + $(LIB)/libwwrt.a | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + # F8+F9 (tasks #5/#12, regex fold-2b): `?` interim single-success gate # (|success| > 1 loud-rejected on BOTH stages until task #14's # subset-union typing) + direct `f()? is T` / `match (f()?)` reject diff --git a/cmd/w6c/cgen.c b/cmd/w6c/cgen.c index 31ea348e..33498190 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -4267,6 +4267,41 @@ cgexpr(Cg *c, Node *n, Local *locals) cgexpr(c, n->rhs, locals); break; } + /* #20 (task): struct-lit rhs into an INDEXED struct element — + * `a[i] = pt{...}`, `(*ts)[i].caps[k] = capture{...}` — a + * DEREF place (`*p = pt{...}`) or an indexed-base FIELD + * place (`a[i].f = pt{...}`, same class) skips the legacy + * arms and routes to the resolver aggregate arm below (the + * single @placescr funnel). The legacy arms' rhs handling + * (#270-1b ident/dot/deref gate; deref scalar store; the + * a[i].f fldstoreop tail) let the lit fall to a scalar + * tail: cgexpr(N_STRUCTLIT) emits nothing (AX=0) and one + * MOVQ zeroed the place's first word — every field + * silently dropped, a leading str header trashed. */ + int place_slit = 0; + if (n->lhs + && (n->lhs->kind == N_INDEX + || (n->lhs->kind == N_UN && n->lhs->op == TK_STAR) + || (n->lhs->kind == N_DOT && n->lhs->lhs + && n->lhs->lhs->kind == N_INDEX)) + && n->op == TK_ASSIGN + && n->rhs && n->rhs->kind == N_STRUCTLIT) { + Type *iet = type_chase_named(n->lhs->type); + if (iet && iet->kind == TY_STRUCT) + place_slit = 1; + } + /* Task #32: an array-LITERAL rhs at assignment is unwired + * for EVERY place kind (ident reassign, index, deref, dot) + * — only decl-init fills. Pre-#32 the same scalar tail + * zeroed one word silently; die loud until the fill lands. + * Slice-typed places are already loud in the checker. */ + if (n->op == TK_ASSIGN && n->lhs + && n->rhs && n->rhs->kind == N_ARRLIT) { + Type *alt = type_chase_named(n->lhs->type); + if (alt && alt->kind == TY_ARRAY) + fatal("array-literal store at assignment " + "unwired (task #32)"); + } /* p.x = v or p.x += v where p.x is a struct field * (direct or via *struct). For compound ops we read-modify- * write the field; for plain `=` we just write. The base @@ -4645,7 +4680,7 @@ cgexpr(Cg *c, Node *n, Local *locals) * fall through and silently drop the store. Placed before the * `!= N_IDENT` branch so both shapes share one path. */ if (n->lhs && n->lhs->kind == N_DOT && n->lhs->lhs - && n->lhs->lhs->kind == N_INDEX) { + && n->lhs->lhs->kind == N_INDEX && !place_slit) { Node *idxbase = n->lhs->lhs->lhs; Node *idx = n->lhs->lhs->rhs; if (idxbase && idxbase->kind == N_IDENT && idx) { @@ -5482,7 +5517,7 @@ cgexpr(Cg *c, Node *n, Local *locals) * ptr local) or a more complex expression like s.ptr where * s: *[]u8. We compute the base address, scale the index by * elem size, and store with the right size. */ - if (n->lhs->kind == N_INDEX && n->lhs->lhs) { + if (n->lhs->kind == N_INDEX && n->lhs->lhs && !place_slit) { Node *base = n->lhs->lhs; Type *bt = base->type; Type *u = (bt && bt->kind == TY_NAMED) ? bt->under : bt; @@ -5585,8 +5620,10 @@ cgexpr(Cg *c, Node *n, Local *locals) * address, then word-copy esz bytes: the WRITE-twin of the * #268 let-init copy loop. Source shapes mirror that loop * (ident local/global, N_DOT field via cg_dotchain_addr, - * `*p` deref); a by-value call result is the deferred #271, - * so N_CALL/literal sources fall through unchanged. */ + * `*p` deref); struct-lit sources divert at the place_slit + * gate above (#20), array-lit dies loud (task #32), and a + * by-value call result still falls to the scalar tail — + * RAX-only store, task #31-G. */ if ((is_arr || is_sl || is_ptr) && n->op == TK_ASSIGN && esubu && (esubu->kind == TY_STRUCT || esubu->kind == TY_ARRAY @@ -5943,9 +5980,12 @@ cgexpr(Cg *c, Node *n, Local *locals) } /* Deref-target assignment `*p = v;`. The size of the store is * determined by the type *p points at; the pointer expression - * is evaluated after the value so we don't need to spill BX. */ + * is evaluated after the value so we don't need to spill BX. + * Retained gap: an aggregate >8B rhs (ident, tuple-lit, call) + * truncates to one word here — task #31 A/E/G; struct-lit + * diverts at the place_slit gate, array-lit dies loud (#32). */ if (n->lhs && n->lhs->kind == N_UN && n->lhs->op == TK_STAR - && n->op == TK_ASSIGN) { + && n->op == TK_ASSIGN && !place_slit) { Type *pt = n->lhs->lhs ? n->lhs->lhs->type : NULL; Type *pu = (pt && pt->kind == TY_NAMED) ? pt->under : pt; Type *vt = (pu && pu->kind == TY_PTR) ? pu->sub : NULL; @@ -6218,8 +6258,15 @@ cgexpr(Cg *c, Node *n, Local *locals) * routes through cgplaceaddr; the load/store emission stays * here. Any N_DOT shape the resolver can't address dies * LOUD below: the pre-C1 dispatch tail silently emitted - * NOTHING (rhs unevaluated) for every such shape. */ - if (n->lhs && n->lhs->kind == N_DOT) { + * NOTHING (rhs unevaluated) for every such shape. + * #20 (task): N_INDEX and N_UN(STAR) lvalues enroll too — + * only the struct-lit-rhs diversion above reaches here + * (every other indexed/deref shape broke out of its legacy + * arm), and the C1.25 aggregate branch fills via + * @placescr. */ + if (n->lhs && (n->lhs->kind == N_DOT + || n->lhs->kind == N_INDEX + || (n->lhs->kind == N_UN && n->lhs->op == TK_STAR))) { Type *ft = n->lhs->type; Type *fu = type_chase_named(ft); int fsz = (int)(ft ? ft->size : 8); diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 38f92933..f64bfbfe 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -27248,6 +27248,42 @@ fn cgcall(c: *cgen, n: *node) void = { fn cgassign(c: *cgen, n: *node) void = { let lhs: *node = n.lhs; + // #20 (task): struct-lit rhs into an INDEXED struct element — + // `a[i] = pt{...}`, `(*ts)[i].caps[k] = capture{...}` — a + // DEREF place (`*p = pt{...}`) or an indexed-base FIELD place + // (`a[i].f = pt{...}`, same class) skips the legacy arms and + // routes to the resolver aggregate arm below (the single + // @placescr funnel). The legacy arms' rhs handling (#270-1b + // ident/dot/deref gate; deref scalar store; the a[i].f + // fldstoreop tail) let the lit fall to a scalar tail: + // cgexpr(N_STRUCTLIT) emits nothing (AX=0) and one MOVQ zeroed + // the place's first word — every field silently dropped, a + // leading str header trashed. + let placeslit: bool = false; + let placedotidx: bool = false; + if (lhs != nil) { + if (lhs.kind == nkind.N_DOT && lhs.lhs != nil) { + if (lhs.lhs.kind == nkind.N_INDEX) { + placedotidx = true; + }; + }; + if ((lhs.kind == nkind.N_INDEX + || (lhs.kind == nkind.N_UN && lhs.op == tkind.TK_STAR) + || placedotidx) + && n.op == tkind.TK_ASSIGN && n.rhs != nil) { + if (n.rhs.kind == nkind.N_STRUCTLIT) { + let iet: *tinfo = lhs.type_: *tinfo; + for (iet != nil && iet.kind == tykind.TY_NAMED) { + iet = iet.under; + }; + if (iet != nil) { + if (iet.kind == tykind.TY_STRUCT) { + placeslit = true; + }; + }; + }; + }; + }; // Discard lvalue `_ = expr;` — evaluate rhs for side effects, // write nothing. Detected by lhs being an nkind.N_IDENT with empty str // (planted by parseprimary on the tkind.TK_UNDER token). @@ -27261,6 +27297,26 @@ fn cgassign(c: *cgen, n: *node) void = { }; }; }; + // Task #32: an array-LITERAL rhs at assignment is unwired for + // EVERY place kind (ident reassign, index, deref, dot) — only + // decl-init fills. Pre-#32 the same scalar tail zeroed one + // word silently; die loud until the fill lands. Slice-typed + // places are already loud in the checker. + if (n.op == tkind.TK_ASSIGN && lhs != nil && n.rhs != nil) { + if (n.rhs.kind == nkind.N_ARRLIT) { + let alt: *tinfo = lhs.type_: *tinfo; + for (alt != nil && alt.kind == tykind.TY_NAMED) { + alt = alt.under; + }; + if (alt != nil) { + if (alt.kind == tykind.TY_ARRAY) { + let mal: str = "array-literal store at assignment unwired (task #32)\n"; + os.write(2, mal.ptr, mal.len: u64); + os.exit(1); + }; + }; + }; + }; // Tagged-union local reassignment: `r = expr;` where r has a // tagged-union type. Delegate to cgwidentaggedstore (same path // as cglet's tagged-init). Covers nullable fold, tagged source, @@ -27341,10 +27397,13 @@ fn cgassign(c: *cgen, n: *node) void = { // and BX if str), push, eval pointer, pop value, store. // We default to MOVQ (8B) since most fixtures use it; for // `*bool` / `*u8` / `*i32` we narrow via the local's tnode. + // Retained gap: an aggregate >8B rhs (ident, tuple-lit, call) + // truncates to one word here — task #31 A/E/G; struct-lit + // diverts at the placeslit gate, array-lit dies loud (#32). if (lhs != nil) { if (lhs.kind == nkind.N_UN) { if (lhs.op == tkind.TK_STAR) { - if (n.op == tkind.TK_ASSIGN) { + if (n.op == tkind.TK_ASSIGN && !placeslit) { let inner: *node = lhs.lhs; let elemstr: bool = false; let elemfloat: bool = false; @@ -27552,7 +27611,7 @@ fn cgassign(c: *cgen, n: *node) void = { // Array/slice/ptr index store: `arr[i] = v;`. Element size // from base.tnode picks MOVB vs MOVQ. if (lhs != nil) { - if (lhs.kind == nkind.N_INDEX) { + if (lhs.kind == nkind.N_INDEX && !placeslit) { if (n.op == tkind.TK_ASSIGN) { let base: *node = lhs.lhs; let idx: *node = lhs.rhs; @@ -27771,8 +27830,10 @@ fn cgassign(c: *cgen, n: *node) void = { // address, then word-copy esz bytes: the WRITE-twin of // the #268 let-init copy loop. Source shapes mirror that // loop (ident, N_DOT field via dotchainaddr, `*p` - // deref); a by-value call result is the deferred #271, so - // N_CALL/literal sources fall through unchanged. esz>8 + // deref); struct-lit sources divert at the placeslit + // gate above (#20), array-lit dies loud (task #32), and + // a by-value call result still falls to the scalar tail + // — RAX-only store, task #31-G. esz>8 // non-str/non-slice IS a struct/array/tuple here (the // tagged element already returned above; floats are ≤8). let aggsrc: bool = (n.rhs.kind == nkind.N_IDENT) @@ -28196,7 +28257,7 @@ fn cgassign(c: *cgen, n: *node) void = { // field write). if (lhs != nil) { if (lhs.kind == nkind.N_DOT && lhs.lhs != nil - && lhs.lhs.kind == nkind.N_INDEX) { + && lhs.lhs.kind == nkind.N_INDEX && !placeslit) { let idxbase: *node = lhs.lhs.lhs; let idx: *node = lhs.lhs.rhs; let fld2: str = lhs.str; @@ -30569,8 +30630,13 @@ fn cgassign(c: *cgen, n: *node) void = { // N_DOT shape the resolver can't address dies LOUD below: the // pre-C1 fall-off-the-function tail silently emitted NOTHING // (rhs unevaluated). Mirror of the cstage cgen.c N_ASSIGN arm. + // #20 (task): N_INDEX and N_UN(STAR) lvalues enroll too — only + // the struct-lit-rhs diversion above reaches here (every other + // indexed/deref shape returned from its legacy arm), and the + // C1.25 aggregate branch fills via @placescr. if (lhs != nil) { - if (lhs.kind == nkind.N_DOT) { + if (lhs.kind == nkind.N_DOT || lhs.kind == nkind.N_INDEX + || (lhs.kind == nkind.N_UN && lhs.op == tkind.TK_STAR)) { let ft: *tinfo = lhs.type_: *tinfo; let fu: *tinfo = ft; for (fu != nil && fu.kind == tykind.TY_NAMED) { diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index 2bc2c69a..844ec62d 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -6814,6 +6814,42 @@ fn cgcall(c: *cgen, n: *node) void = { fn cgassign(c: *cgen, n: *node) void = { let lhs: *node = n.lhs; + // #20 (task): struct-lit rhs into an INDEXED struct element — + // `a[i] = pt{...}`, `(*ts)[i].caps[k] = capture{...}` — a + // DEREF place (`*p = pt{...}`) or an indexed-base FIELD place + // (`a[i].f = pt{...}`, same class) skips the legacy arms and + // routes to the resolver aggregate arm below (the single + // @placescr funnel). The legacy arms' rhs handling (#270-1b + // ident/dot/deref gate; deref scalar store; the a[i].f + // fldstoreop tail) let the lit fall to a scalar tail: + // cgexpr(N_STRUCTLIT) emits nothing (AX=0) and one MOVQ zeroed + // the place's first word — every field silently dropped, a + // leading str header trashed. + let placeslit: bool = false; + let placedotidx: bool = false; + if (lhs != nil) { + if (lhs.kind == nkind.N_DOT && lhs.lhs != nil) { + if (lhs.lhs.kind == nkind.N_INDEX) { + placedotidx = true; + }; + }; + if ((lhs.kind == nkind.N_INDEX + || (lhs.kind == nkind.N_UN && lhs.op == tkind.TK_STAR) + || placedotidx) + && n.op == tkind.TK_ASSIGN && n.rhs != nil) { + if (n.rhs.kind == nkind.N_STRUCTLIT) { + let iet: *tinfo = lhs.type_: *tinfo; + for (iet != nil && iet.kind == tykind.TY_NAMED) { + iet = iet.under; + }; + if (iet != nil) { + if (iet.kind == tykind.TY_STRUCT) { + placeslit = true; + }; + }; + }; + }; + }; // Discard lvalue `_ = expr;` — evaluate rhs for side effects, // write nothing. Detected by lhs being an nkind.N_IDENT with empty str // (planted by parseprimary on the tkind.TK_UNDER token). @@ -6827,6 +6863,26 @@ fn cgassign(c: *cgen, n: *node) void = { }; }; }; + // Task #32: an array-LITERAL rhs at assignment is unwired for + // EVERY place kind (ident reassign, index, deref, dot) — only + // decl-init fills. Pre-#32 the same scalar tail zeroed one + // word silently; die loud until the fill lands. Slice-typed + // places are already loud in the checker. + if (n.op == tkind.TK_ASSIGN && lhs != nil && n.rhs != nil) { + if (n.rhs.kind == nkind.N_ARRLIT) { + let alt: *tinfo = lhs.type_: *tinfo; + for (alt != nil && alt.kind == tykind.TY_NAMED) { + alt = alt.under; + }; + if (alt != nil) { + if (alt.kind == tykind.TY_ARRAY) { + let mal: str = "array-literal store at assignment unwired (task #32)\n"; + os.write(2, mal.ptr, mal.len: u64); + os.exit(1); + }; + }; + }; + }; // Tagged-union local reassignment: `r = expr;` where r has a // tagged-union type. Delegate to cgwidentaggedstore (same path // as cglet's tagged-init). Covers nullable fold, tagged source, @@ -6907,10 +6963,13 @@ fn cgassign(c: *cgen, n: *node) void = { // and BX if str), push, eval pointer, pop value, store. // We default to MOVQ (8B) since most fixtures use it; for // `*bool` / `*u8` / `*i32` we narrow via the local's tnode. + // Retained gap: an aggregate >8B rhs (ident, tuple-lit, call) + // truncates to one word here — task #31 A/E/G; struct-lit + // diverts at the placeslit gate, array-lit dies loud (#32). if (lhs != nil) { if (lhs.kind == nkind.N_UN) { if (lhs.op == tkind.TK_STAR) { - if (n.op == tkind.TK_ASSIGN) { + if (n.op == tkind.TK_ASSIGN && !placeslit) { let inner: *node = lhs.lhs; let elemstr: bool = false; let elemfloat: bool = false; @@ -7118,7 +7177,7 @@ fn cgassign(c: *cgen, n: *node) void = { // Array/slice/ptr index store: `arr[i] = v;`. Element size // from base.tnode picks MOVB vs MOVQ. if (lhs != nil) { - if (lhs.kind == nkind.N_INDEX) { + if (lhs.kind == nkind.N_INDEX && !placeslit) { if (n.op == tkind.TK_ASSIGN) { let base: *node = lhs.lhs; let idx: *node = lhs.rhs; @@ -7337,8 +7396,10 @@ fn cgassign(c: *cgen, n: *node) void = { // address, then word-copy esz bytes: the WRITE-twin of // the #268 let-init copy loop. Source shapes mirror that // loop (ident, N_DOT field via dotchainaddr, `*p` - // deref); a by-value call result is the deferred #271, so - // N_CALL/literal sources fall through unchanged. esz>8 + // deref); struct-lit sources divert at the placeslit + // gate above (#20), array-lit dies loud (task #32), and + // a by-value call result still falls to the scalar tail + // — RAX-only store, task #31-G. esz>8 // non-str/non-slice IS a struct/array/tuple here (the // tagged element already returned above; floats are ≤8). let aggsrc: bool = (n.rhs.kind == nkind.N_IDENT) @@ -7762,7 +7823,7 @@ fn cgassign(c: *cgen, n: *node) void = { // field write). if (lhs != nil) { if (lhs.kind == nkind.N_DOT && lhs.lhs != nil - && lhs.lhs.kind == nkind.N_INDEX) { + && lhs.lhs.kind == nkind.N_INDEX && !placeslit) { let idxbase: *node = lhs.lhs.lhs; let idx: *node = lhs.lhs.rhs; let fld2: str = lhs.str; @@ -10135,8 +10196,13 @@ fn cgassign(c: *cgen, n: *node) void = { // N_DOT shape the resolver can't address dies LOUD below: the // pre-C1 fall-off-the-function tail silently emitted NOTHING // (rhs unevaluated). Mirror of the cstage cgen.c N_ASSIGN arm. + // #20 (task): N_INDEX and N_UN(STAR) lvalues enroll too — only + // the struct-lit-rhs diversion above reaches here (every other + // indexed/deref shape returned from its legacy arm), and the + // C1.25 aggregate branch fills via @placescr. if (lhs != nil) { - if (lhs.kind == nkind.N_DOT) { + if (lhs.kind == nkind.N_DOT || lhs.kind == nkind.N_INDEX + || (lhs.kind == nkind.N_UN && lhs.op == tkind.TK_STAR)) { let ft: *tinfo = lhs.type_: *tinfo; let fu: *tinfo = ft; for (fu != nil && fu.kind == tykind.TY_NAMED) { diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 3883a33e..1e732119 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -27248,6 +27248,42 @@ fn cgcall(c: *cgen, n: *node) void = { fn cgassign(c: *cgen, n: *node) void = { let lhs: *node = n.lhs; + // #20 (task): struct-lit rhs into an INDEXED struct element — + // `a[i] = pt{...}`, `(*ts)[i].caps[k] = capture{...}` — a + // DEREF place (`*p = pt{...}`) or an indexed-base FIELD place + // (`a[i].f = pt{...}`, same class) skips the legacy arms and + // routes to the resolver aggregate arm below (the single + // @placescr funnel). The legacy arms' rhs handling (#270-1b + // ident/dot/deref gate; deref scalar store; the a[i].f + // fldstoreop tail) let the lit fall to a scalar tail: + // cgexpr(N_STRUCTLIT) emits nothing (AX=0) and one MOVQ zeroed + // the place's first word — every field silently dropped, a + // leading str header trashed. + let placeslit: bool = false; + let placedotidx: bool = false; + if (lhs != nil) { + if (lhs.kind == nkind.N_DOT && lhs.lhs != nil) { + if (lhs.lhs.kind == nkind.N_INDEX) { + placedotidx = true; + }; + }; + if ((lhs.kind == nkind.N_INDEX + || (lhs.kind == nkind.N_UN && lhs.op == tkind.TK_STAR) + || placedotidx) + && n.op == tkind.TK_ASSIGN && n.rhs != nil) { + if (n.rhs.kind == nkind.N_STRUCTLIT) { + let iet: *tinfo = lhs.type_: *tinfo; + for (iet != nil && iet.kind == tykind.TY_NAMED) { + iet = iet.under; + }; + if (iet != nil) { + if (iet.kind == tykind.TY_STRUCT) { + placeslit = true; + }; + }; + }; + }; + }; // Discard lvalue `_ = expr;` — evaluate rhs for side effects, // write nothing. Detected by lhs being an nkind.N_IDENT with empty str // (planted by parseprimary on the tkind.TK_UNDER token). @@ -27261,6 +27297,26 @@ fn cgassign(c: *cgen, n: *node) void = { }; }; }; + // Task #32: an array-LITERAL rhs at assignment is unwired for + // EVERY place kind (ident reassign, index, deref, dot) — only + // decl-init fills. Pre-#32 the same scalar tail zeroed one + // word silently; die loud until the fill lands. Slice-typed + // places are already loud in the checker. + if (n.op == tkind.TK_ASSIGN && lhs != nil && n.rhs != nil) { + if (n.rhs.kind == nkind.N_ARRLIT) { + let alt: *tinfo = lhs.type_: *tinfo; + for (alt != nil && alt.kind == tykind.TY_NAMED) { + alt = alt.under; + }; + if (alt != nil) { + if (alt.kind == tykind.TY_ARRAY) { + let mal: str = "array-literal store at assignment unwired (task #32)\n"; + os.write(2, mal.ptr, mal.len: u64); + os.exit(1); + }; + }; + }; + }; // Tagged-union local reassignment: `r = expr;` where r has a // tagged-union type. Delegate to cgwidentaggedstore (same path // as cglet's tagged-init). Covers nullable fold, tagged source, @@ -27341,10 +27397,13 @@ fn cgassign(c: *cgen, n: *node) void = { // and BX if str), push, eval pointer, pop value, store. // We default to MOVQ (8B) since most fixtures use it; for // `*bool` / `*u8` / `*i32` we narrow via the local's tnode. + // Retained gap: an aggregate >8B rhs (ident, tuple-lit, call) + // truncates to one word here — task #31 A/E/G; struct-lit + // diverts at the placeslit gate, array-lit dies loud (#32). if (lhs != nil) { if (lhs.kind == nkind.N_UN) { if (lhs.op == tkind.TK_STAR) { - if (n.op == tkind.TK_ASSIGN) { + if (n.op == tkind.TK_ASSIGN && !placeslit) { let inner: *node = lhs.lhs; let elemstr: bool = false; let elemfloat: bool = false; @@ -27552,7 +27611,7 @@ fn cgassign(c: *cgen, n: *node) void = { // Array/slice/ptr index store: `arr[i] = v;`. Element size // from base.tnode picks MOVB vs MOVQ. if (lhs != nil) { - if (lhs.kind == nkind.N_INDEX) { + if (lhs.kind == nkind.N_INDEX && !placeslit) { if (n.op == tkind.TK_ASSIGN) { let base: *node = lhs.lhs; let idx: *node = lhs.rhs; @@ -27771,8 +27830,10 @@ fn cgassign(c: *cgen, n: *node) void = { // address, then word-copy esz bytes: the WRITE-twin of // the #268 let-init copy loop. Source shapes mirror that // loop (ident, N_DOT field via dotchainaddr, `*p` - // deref); a by-value call result is the deferred #271, so - // N_CALL/literal sources fall through unchanged. esz>8 + // deref); struct-lit sources divert at the placeslit + // gate above (#20), array-lit dies loud (task #32), and + // a by-value call result still falls to the scalar tail + // — RAX-only store, task #31-G. esz>8 // non-str/non-slice IS a struct/array/tuple here (the // tagged element already returned above; floats are ≤8). let aggsrc: bool = (n.rhs.kind == nkind.N_IDENT) @@ -28196,7 +28257,7 @@ fn cgassign(c: *cgen, n: *node) void = { // field write). if (lhs != nil) { if (lhs.kind == nkind.N_DOT && lhs.lhs != nil - && lhs.lhs.kind == nkind.N_INDEX) { + && lhs.lhs.kind == nkind.N_INDEX && !placeslit) { let idxbase: *node = lhs.lhs.lhs; let idx: *node = lhs.lhs.rhs; let fld2: str = lhs.str; @@ -30569,8 +30630,13 @@ fn cgassign(c: *cgen, n: *node) void = { // N_DOT shape the resolver can't address dies LOUD below: the // pre-C1 fall-off-the-function tail silently emitted NOTHING // (rhs unevaluated). Mirror of the cstage cgen.c N_ASSIGN arm. + // #20 (task): N_INDEX and N_UN(STAR) lvalues enroll too — only + // the struct-lit-rhs diversion above reaches here (every other + // indexed/deref shape returned from its legacy arm), and the + // C1.25 aggregate branch fills via @placescr. if (lhs != nil) { - if (lhs.kind == nkind.N_DOT) { + if (lhs.kind == nkind.N_DOT || lhs.kind == nkind.N_INDEX + || (lhs.kind == nkind.N_UN && lhs.op == tkind.TK_STAR)) { let ft: *tinfo = lhs.type_: *tinfo; let fu: *tinfo = ft; for (fu != nil && fu.kind == tykind.TY_NAMED) { diff --git a/test/wcc/809_idx_structlit_store.c b/test/wcc/809_idx_structlit_store.c new file mode 100644 index 00000000..cd941bd2 --- /dev/null +++ b/test/wcc/809_idx_structlit_store.c @@ -0,0 +1,549 @@ +/* + * 809_idx_structlit_store — cstage and wwstage agree, byte-for-byte and + * at runtime, that a struct-LITERAL store into an INDEXED element place + * (`a[i] = pt{...}`, `cs[i] = capture{...}`, `(*ts)[i].caps[k] = + * capture{...}`) and into a DEREF place (`*p = pt{...}`) writes every + * field (task #20; ken's p13 array find + prober PG4 slice extension; + * gates regex fold-5a's run_thread groupstart capture store, + * regex.ha:643-651). + * + * Pre-fix BOTH stages compiled these byte-identically WRONG (gate-blind + * — only a runtime pin can hold this): the N_ASSIGN N_INDEX-lhs arm's + * aggregate branch (#270-1b) gated its rhs on ident/dot/deref shapes, + * so an N_STRUCTLIT rhs fell to the scalar store tail; cgexpr on a + * struct literal emits NOTHING (AX stays 0) and the tail stored ONE + * zero word at the element base. Net effect: every literal field + * silently dropped, the element's first 8 bytes zeroed — for a + * str-leading element (the regex capture shape) that nulls content.ptr + * and the next read of the str field SEGFAULTS. The N_UN(STAR) deref + * arm (`*p = pt{...}`) had the same fall-to-scalar tail — same class. + * + * The fix (BOTH stages, converged byte-identical): a struct-lit rhs + * aimed at an N_INDEX, N_UN(STAR), or indexed-base N_DOT place + * (`a[i].f = pt{...}`, reviewer-20 sibling — the a[i].f legacy arm's + * fldstoreop tail had the same silent drop) skips the legacy arms and + * routes through the F6 assign-resolver (cgplaceaddr) — the SAME C1.25 + * aggregate arm that wires `(*ts)[i].field = capture{...}` N_DOT + * places: materialise the literal into a FRESH per-use @placescr slot + * (cg_structlit_fill_bp / cgstructlitfillbp: nested literals, str + * fields, TK_ELLIPSIS autofill), resolve the place address, word-copy + * esz bytes. Close-by-construction: every non-ident place kind (DOT / + * INDEX / UN-STAR) now funnels struct-lit stores through that single + * arm; ident places keep their enumerated fill paths. Class boundary + * for OTHER rhs kinds at these places: tuple-lit/str-lit into INDEX + * and DOT places work (pinned below); array-lit rhs at assignment is + * unwired for every place kind and now dies LOUD (task #32, build-fail + * rows below); tuple-lit through deref truncates (filed #31-E); CALL + * rhs into INDEX/UN-STAR places stores only RAX (filed #31-G). + * + * row | shape | want + * ------------------+-----------------------------------------+------ + * arr_elem | a[1] = pt{3,4}; a[1].x*10+a[1].y | 34 + * arr_neighbor | store a[1]; a[0]/a[2] field-set intact | 82 + * arr_var_idx | a[geti()] = pt{...} (runtime index) | 34 + * small_elem | 8B one-field elem (esz<=8 word-copy) | 52 + * nested_lit | a[1] = outer{ in = inner{3,4}, t=5 } | 45 + * partial_ellipsis | prefill 9,9; a[1] = pt{x=3, ...} → y=0 | 30 + * alias_lit | type qt = pt; a[1] = qt{3,4} (chase | 34 + * | peels the NAMED alias before TY_STRUCT) | + * all_default | prefill 9,9; a[1] = pt{} zeroes EVERY | 7 + * | word (fill-loop floor) + 7 | + * tuple_elem | a[1] = (3,4); copy-out readback | 34 + * dotidx_field | a[1].p = pt{3,4} (indexed-base DOT | 34 + * | place, reviewer-20 sibling fix) | + * slice_elem_str | cs[1] = capture{...} 56B str-leading | 128 + * fieldplace_slice | ts[0].caps[1] = capture{...} | 128 + * derefspine_slice | (*tsp)[0].caps[1] = capture{...} | 128 + * callee_capture | (*ts)[i].caps[k] = capture{...} in a | 197 + * | callee w/ *[]thr param; str readback via | + * | strings.compare + len + both neighbors | + * | (the EXACT fold-5a consumer shape) | + * deref_lit | *p = pt{3,4} (N_UN place, same class) | 34 + * global_arr | g[1] = pt{3,4}, g: [3]pt module let | 34 + * arrlit_idx_loud | a[1] = [3,4] — LOUD build-fail (#32) | -1 + * arrlit_deref_loud | *p = [3,4] — LOUD build-fail (#32) | -1 + * + * global_arr is RUNTIME-ONLY (byteid=0): an uninitialised [N]struct + * value-global already diverges on master — wwstage emits a zero + * `DATAW main.g(SB)` record (under-sized: 24B for a 48B array), + * cstage emits none. Pre-existing, independent of this fix (task #11 + * family: value-global data emission); the store/readback this row + * pins is symmetric and correct on both stages. The arrlit_* rows pin + * task #32's loud boundary (want -1 = driver build MUST fail; pre-#32 + * both stages silently zeroed one word); flip them to runtime rows + * when #32 wires the fill. + */ +#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; +} + +struct row { const char *label; const char *src; int want; int byteid; }; + +/* The 56B regex capture shape: str header leading + 4 size fields. + * Shared preamble for the slice rows; only the store site differs. */ +#define CAPTURE_DEFS \ + "package main;\n" \ + "type capture = struct {\n" \ + "\tcontent: str,\n" \ + "\tstart: size,\n" \ + "\tstart_bytesize: size,\n" \ + "\tend: size,\n" \ + "\tend_bytesize: size,\n" \ + "};\n" + +#define CAPTURE_SEED \ + "\tlet cs: []capture = [];\n" \ + "\tappend(cs, capture { content = \"a\", start = 1: size, " \ + "start_bytesize = 1: size, end = 1: size, " \ + "end_bytesize = 1: size });\n" \ + "\tappend(cs, capture { content = \"b\", start = 2: size, " \ + "start_bytesize = 2: size, end = 2: size, " \ + "end_bytesize = 2: size });\n" + +#define CAPTURE_STORE(place) \ + "\t" place " = capture { content = \"grp\", start = 3: size, " \ + "start_bytesize = 4: size, end = 60: size, " \ + "end_bytesize = 61: size };\n" + +static const struct row rows[] = { + { "arr_elem", + "package main;\n" + "type pt = struct { x: u64, y: u64 };\n" + "export fn main() i32 = {\n" + "\tlet a: [3]pt;\n" + "\ta[1] = pt { x = 3u64, y = 4u64 };\n" + "\treturn (a[1].x * 10u64 + a[1].y): i32;\n" + "};\n", + 34, 1 }, + + { "arr_neighbor", + "package main;\n" + "type pt = struct { x: u64, y: u64 };\n" + "export fn main() i32 = {\n" + "\tlet a: [3]pt;\n" + "\ta[0].x = 7u64; a[2].y = 9u64;\n" + "\ta[1] = pt { x = 3u64, y = 4u64 };\n" + "\treturn (a[0].x * 10u64 + a[2].y + a[1].x): i32;\n" + "};\n", + 82, 1 }, + + { "arr_var_idx", + "package main;\n" + "type pt = struct { x: u64, y: u64 };\n" + "fn geti() size = { return 1: size; };\n" + "export fn main() i32 = {\n" + "\tlet a: [3]pt;\n" + "\ta[geti()] = pt { x = 3u64, y = 4u64 };\n" + "\treturn (a[1].x * 10u64 + a[1].y): i32;\n" + "};\n", + 34, 1 }, + + { "small_elem", + "package main;\n" + "type one = struct { v: u64 };\n" + "export fn main() i32 = {\n" + "\tlet a: [2]one;\n" + "\ta[0].v = 2u64;\n" + "\ta[1] = one { v = 5u64 };\n" + "\treturn (a[1].v * 10u64 + a[0].v): i32;\n" + "};\n", + 52, 1 }, + + { "nested_lit", + "package main;\n" + "type inner = struct { a: u64, b: u64 };\n" + "type outer = struct { in: inner, t: u64 };\n" + "export fn main() i32 = {\n" + "\tlet a: [2]outer;\n" + "\ta[1] = outer { in = inner { a = 3u64, b = 4u64 }, " + "t = 5u64 };\n" + "\treturn (a[1].in.b * 10u64 + a[1].t): i32;\n" + "};\n", + 45, 1 }, + + { "partial_ellipsis", + "package main;\n" + "type pt = struct { x: u64, y: u64 };\n" + "export fn main() i32 = {\n" + "\tlet a: [3]pt;\n" + "\ta[1].x = 9u64; a[1].y = 9u64;\n" + "\ta[1] = pt { x = 3u64, ... };\n" + "\treturn (a[1].x * 10u64 + a[1].y): i32;\n" + "};\n", + 30, 1 }, + + { "alias_lit", + "package main;\n" + "type pt = struct { x: u64, y: u64 };\n" + "type qt = pt;\n" + "export fn main() i32 = {\n" + "\tlet a: [3]pt;\n" + "\ta[1] = qt { x = 3u64, y = 4u64 };\n" + "\treturn (a[1].x * 10u64 + a[1].y): i32;\n" + "};\n", + 34, 1 }, + + { "all_default", + "package main;\n" + "type pt = struct { x: u64, y: u64 };\n" + "export fn main() i32 = {\n" + "\tlet a: [3]pt;\n" + "\ta[1].x = 9u64; a[1].y = 9u64;\n" + "\ta[1] = pt {};\n" + "\treturn (a[1].x * 10u64 + a[1].y + 7u64): i32;\n" + "};\n", + 7, 1 }, + + { "tuple_elem", + "package main;\n" + "export fn main() i32 = {\n" + "\tlet a: [2](u64, u64);\n" + "\ta[1] = (3u64, 4u64);\n" + "\tlet t: (u64, u64) = a[1];\n" + "\treturn (t.0 * 10u64 + t.1): i32;\n" + "};\n", + 34, 1 }, + + { "dotidx_field", + "package main;\n" + "type pt = struct { x: u64, y: u64 };\n" + "type box = struct { p: pt, t: u64 };\n" + "export fn main() i32 = {\n" + "\tlet a: [2]box;\n" + "\ta[1].p.x = 9u64; a[1].p.y = 9u64;\n" + "\tif (a[1].p.x != 9u64 || a[1].p.y != 9u64) { return 250; };\n" + "\ta[1].p = pt { x = 3u64, y = 4u64 };\n" + "\treturn (a[1].p.x * 10u64 + a[1].p.y): i32;\n" + "};\n", + 34, 1 }, + + { "slice_elem_str", + CAPTURE_DEFS + "export fn main() i32 = {\n" + CAPTURE_SEED + CAPTURE_STORE("cs[1]") + "\tif (cs[0].start != 1) { return 250; };\n" + "\treturn (cs[1].start + cs[1].start_bytesize + cs[1].end + " + "cs[1].end_bytesize): i32;\n" + "};\n", + 128, 1 }, + + { "fieldplace_slice", + CAPTURE_DEFS + "type thr = struct { pc: size, caps: []capture };\n" + "export fn main() i32 = {\n" + CAPTURE_SEED + "\tlet ts: []thr = [];\n" + "\tappend(ts, thr { pc = 5: size, caps = cs });\n" + CAPTURE_STORE("ts[0].caps[1]") + "\tif (ts[0].caps[0].start != 1) { return 250; };\n" + "\treturn (ts[0].caps[1].start + ts[0].caps[1].start_bytesize + " + "ts[0].caps[1].end + ts[0].caps[1].end_bytesize): i32;\n" + "};\n", + 128, 1 }, + + { "derefspine_slice", + CAPTURE_DEFS + "type thr = struct { pc: size, caps: []capture };\n" + "export fn main() i32 = {\n" + CAPTURE_SEED + "\tlet ts: []thr = [];\n" + "\tappend(ts, thr { pc = 5: size, caps = cs });\n" + "\tlet tsp = &ts;\n" + CAPTURE_STORE("(*tsp)[0].caps[1]") + "\tif (ts[0].caps[0].start != 1) { return 250; };\n" + "\treturn (ts[0].caps[1].start + ts[0].caps[1].start_bytesize + " + "ts[0].caps[1].end + ts[0].caps[1].end_bytesize): i32;\n" + "};\n", + 128, 1 }, + + /* The fold-5a consumer pin: regex.ha:643-651 verbatim shape — + * callee takes *[]thr, double-indexes through the deref spine, + * stores the 56B capture literal. Readback covers the str field + * (strings.compare + len — pre-fix this SEGFAULTED on the nulled + * content.ptr), both neighbor elements (56B store must not + * smear), and all four size fields. 3+4+60+61+3*23 = 197. */ + { "callee_capture", + "package main;\n" + "\n" + "import strings;\n" + "\n" + "type capture = struct {\n" + "\tcontent: str,\n" + "\tstart: size,\n" + "\tstart_bytesize: size,\n" + "\tend: size,\n" + "\tend_bytesize: size,\n" + "};\n" + "type thr = struct { pc: size, caps: []capture };\n" + "fn store(ts: *[]thr, i: size, idx: size) void = {\n" + "\t(*ts)[i].caps[idx] = capture {\n" + "\t\tcontent = \"grp\",\n" + "\t\tstart = 3: size,\n" + "\t\tstart_bytesize = 4: size,\n" + "\t\tend = 60: size,\n" + "\t\tend_bytesize = 61: size,\n" + "\t};\n" + "};\n" + "export fn main() i32 = {\n" + "\tlet cs: []capture = [];\n" + "\tappend(cs, capture { content = \"a\", start = 1: size, " + "start_bytesize = 1: size, end = 1: size, " + "end_bytesize = 1: size });\n" + "\tappend(cs, capture { content = \"b\", start = 2: size, " + "start_bytesize = 2: size, end = 2: size, " + "end_bytesize = 2: size });\n" + "\tappend(cs, capture { content = \"c\", start = 9: size, " + "start_bytesize = 9: size, end = 9: size, " + "end_bytesize = 9: size });\n" + "\tlet ts: []thr = [];\n" + "\tappend(ts, thr { pc = 5: size, caps = cs });\n" + "\tstore(&ts, 0: size, 1: size);\n" + "\tif (strings.compare(ts[0].caps[1].content, \"grp\") != 0) " + "{ return 250; };\n" + "\tif (len(ts[0].caps[1].content) != 3) { return 249; };\n" + "\tif (ts[0].caps[0].end != 1 || ts[0].caps[2].start != 9) " + "{ return 248; };\n" + "\tif (strings.compare(ts[0].caps[2].content, \"c\") != 0) " + "{ return 247; };\n" + "\tlet acc = ts[0].caps[1].start + ts[0].caps[1].start_bytesize\n" + "\t\t+ ts[0].caps[1].end + ts[0].caps[1].end_bytesize\n" + "\t\t+ ts[0].caps[1].start * 23;\n" + "\treturn acc: i32;\n" + "};\n", + 197, 1 }, + + { "deref_lit", + "package main;\n" + "type pt = struct { x: u64, y: u64 };\n" + "export fn main() i32 = {\n" + "\tlet a: pt = pt { x = 1u64, y = 2u64 };\n" + "\tlet p: *pt = &a;\n" + "\t*p = pt { x = 3u64, y = 4u64 };\n" + "\treturn (a.x * 10u64 + a.y): i32;\n" + "};\n", + 34, 1 }, + + { "global_arr", + "package main;\n" + "type pt = struct { x: u64, y: u64 };\n" + "let g: [3]pt;\n" + "export fn main() i32 = {\n" + "\tg[1] = pt { x = 3u64, y = 4u64 };\n" + "\treturn (g[1].x * 10u64 + g[1].y): i32;\n" + "};\n", + 34, 0 }, + + { "arrlit_idx_loud", + "package main;\n" + "export fn main() i32 = {\n" + "\tlet a: [2][2]u64;\n" + "\ta[1] = [3u64, 4u64];\n" + "\treturn (a[1][0] * 10u64 + a[1][1]): i32;\n" + "};\n", + -1, 0 }, + + { "arrlit_deref_loud", + "package main;\n" + "export fn main() i32 = {\n" + "\tlet a: [2]u64;\n" + "\tlet p: *[2]u64 = &a;\n" + "\t*p = [3u64, 4u64];\n" + "\treturn (a[0] * 10u64 + a[1]): i32;\n" + "};\n", + -1, 0 }, +}; + +static int +run_driver(const char *driver, const struct row *r, int i) +{ + char src[64], tmpdir[64], cmd[1024]; + snprintf(src, sizeof src, "/tmp/isls_%d_%d.ww", getpid(), i); + snprintf(tmpdir, sizeof tmpdir, "/tmp/isls_%d_d_%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 2>/dev/null", + tmpdir, driver, src); + if (runwait(cmd) != 0) { + /* want == -1 rows pin a LOUD build refusal (task #32). */ + if (r->want != -1) + fprintf(stderr, "row[%s]: build via %s failed\n", + r->label, driver); + unlink(src); rmdir(tmpdir); + return -1; + } + + const char *base = strrchr(src, '/'); + base = base ? base + 1 : src; + char outbin[128]; + 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); rmdir(tmpdir); + return got; +} + +/* asm_byte_identical — generate .s via the driver, once with cstage's + * w6c and once with the wwstage compiler (WW_W6C override; w6a/w6l stay + * cstage — only the .s is compared), and diff. The driver route — not a + * bare `w6c src.ww` — so importing rows (callee_capture pulls strings) + * resolve; the driver leaves src.s NEXT TO the source. Pre-fix the asm + * was byte-identically WRONG (both stages dropped the fill), so these + * rows pin only that the converged fix stays symmetric; the runtime + * rows above carry correctness. */ +static int +asm_byte_identical(const char *bin, const struct row *r, int i) +{ + char src[64], srcs[64], comb[80], srco[64], tmpdir[64]; + char cs[64], ws[64], cmd[1024]; + snprintf(src, sizeof src, "/tmp/isls_asm_%d_%d.ww", getpid(), i); + snprintf(srcs, sizeof srcs, "/tmp/isls_asm_%d_%d.s", getpid(), i); + snprintf(srco, sizeof srco, "/tmp/isls_asm_%d_%d.o", getpid(), i); + snprintf(comb, sizeof comb, "/tmp/isls_asm_%d_%d.combined.ww", + getpid(), i); + snprintf(tmpdir, sizeof tmpdir, "/tmp/isls_asm_%d_d_%d", getpid(), i); + snprintf(cs, sizeof cs, "/tmp/isls_asm_%d_%d_c.s", getpid(), i); + snprintf(ws, sizeof ws, "/tmp/isls_asm_%d_%d_w.s", 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/ww build %s 2>/dev/null", + tmpdir, bin, src); + if (runwait(cmd) != 0 || rename(srcs, cs) != 0) { + fprintf(stderr, "row[%s]: cstage driver .s failed\n", + r->label); + unlink(src); unlink(srco); unlink(comb); rmdir(tmpdir); + return -1; + } + snprintf(cmd, sizeof cmd, + "cd %s && WW_W6C=%s/w6c_ww %s/ww build %s 2>/dev/null", + tmpdir, bin, bin, src); + if (runwait(cmd) != 0 || rename(srcs, ws) != 0) { + fprintf(stderr, "row[%s]: wwstage driver .s failed\n", + r->label); + unlink(src); unlink(srco); unlink(comb); unlink(cs); + rmdir(tmpdir); + return -1; + } + + FILE *fc = fopen(cs, "rb"); + FILE *fw = fopen(ws, "rb"); + int rc = 0; + if (!fc || !fw) { + rc = -1; + } else { + for (;;) { + int a = fgetc(fc); + int b = fgetc(fw); + if (a != b) { rc = -1; break; } + if (a == EOF) break; + } + } + if (fc) fclose(fc); + if (fw) fclose(fw); + if (rc != 0) + fprintf(stderr, "row[%s]: cstage vs wwstage asm differs\n", + r->label); + { + char outbin[128]; + snprintf(outbin, sizeof outbin, "%s/isls_asm_%d_%d", + tmpdir, getpid(), i); + unlink(outbin); + } + unlink(src); unlink(srco); unlink(comb); + unlink(cs); unlink(ws); + rmdir(tmpdir); + return rc; +} + +int +main(void) +{ + const char *bin = getenv("BIN"); + if (!bin) bin = "out/bin"; + char absbin[1024]; + 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[1024]; + snprintf(cdrv, sizeof cdrv, "%s/ww", bin); + char wdrv[1024]; + snprintf(wdrv, sizeof wdrv, "%s/ww_ww", bin); + + struct { const char *name; const char *path; int gated_on_existence; } + drivers[] = { + { "cstage", cdrv, 0 }, + { "wwstage", wdrv, 1 }, + { NULL, NULL, 0 }, + }; + + int n = (int)(sizeof rows / sizeof rows[0]); + int total = 0, fail = 0; + + for (int d = 0; drivers[d].name; d++) { + if (drivers[d].gated_on_existence + && access(drivers[d].path, X_OK) != 0) { + fprintf(stderr, + "idx_structlit_store: skip %s (no %s)\n", + drivers[d].name, drivers[d].path); + continue; + } + for (int i = 0; i < n; i++) { + int got = run_driver(drivers[d].path, &rows[i], i); + total++; + if (got != rows[i].want) { + fprintf(stderr, + "idx_structlit_store[%s][%s]: " + "exit=%d want=%d\n", + drivers[d].name, rows[i].label, + got, rows[i].want); + fail++; + } + } + } + + if (access(wdrv, X_OK) == 0) { + for (int i = 0; i < n; i++) { + if (!rows[i].byteid) continue; + total++; + if (asm_byte_identical(bin, &rows[i], i) != 0) + fail++; + } + } + + if (fail) { + fprintf(stderr, + "idx_structlit_store: %d/%d fixtures failed\n", + fail, total); + return 1; + } + printf("idx_structlit_store: %d/%d ok\n", total, total); + return 0; +}