diff --git a/Makefile b/Makefile index be215095..49190e1c 100644 --- a/Makefile +++ b/Makefile @@ -399,6 +399,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_arrlit_overlong \ $(BIN)/test_idx_structlit_store \ $(BIN)/test_inferred_let_struct \ + $(BIN)/test_agg_assign_width \ $(BIN)/test_placeaddr_store \ $(BIN)/test_tryprop_multisuccess \ $(BIN)/test_append_place \ @@ -1101,6 +1102,20 @@ $(BIN)/test_inferred_let_struct: test/wcc/811_inferred_let_struct.c \ $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +# Task #49 (Family A, ken silent-set triage): whole-aggregate ASSIGN +# `b = a` copied word0 only — one MOVQ for any struct/array/tuple — in +# BOTH stages, byte-identical, gate-blind. Same class at the structlit +# fill field-from-ident, the deref place (#31-A) and the module-let +# global. Fixed via the ONE mem-to-mem funnel (cg_aggcopy/aggcopy fed +# by aggarg_srcaddr); non-addressable aggregate rhs dies loud. Runtime +# readback rows (the only oracle for a gate-blind class) + per-row +# asm byte-id. +$(BIN)/test_agg_assign_width: test/wcc/812_agg_assign_width.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 8be8bdb5..1a85d24f 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -2073,6 +2073,37 @@ aggarg_srcaddr(Cg *c, Node *src, int dst, Local *locals) return 0; } +/* cg_aggcopy — the ONE place-resolved mem-to-mem aggregate copy: + * sz bytes (SI) → (BX) via AX, a MOVQ run plus a 4/2/1 sized tail. + * Extracted verbatim from the C1.25 assign-resolver tail so every + * aggregate copy position (resolver field store, #49 ident reassign, + * #49 structlit fill-field) funnels through one loop — close-by- + * construction, no per-site width logic to skew. */ +static void +cg_aggcopy(Cg *c, int sz) +{ + int k = 0; + for (; k + 8 <= sz; k += 8) { + ins2(c, A_MOVQ, amem(D_SI, k), areg(D_AX)); + ins2(c, A_MOVQ, areg(D_AX), amem(D_BX, k)); + } + if (k + 4 <= sz) { + ins2(c, A_MOVL, amem(D_SI, k), areg(D_AX)); + ins2(c, A_MOVL, areg(D_AX), amem(D_BX, k)); + k += 4; + } + if (k + 2 <= sz) { + ins2(c, A_MOVW, amem(D_SI, k), areg(D_AX)); + ins2(c, A_MOVW, areg(D_AX), amem(D_BX, k)); + k += 2; + } + if (k + 1 <= sz) { + ins2(c, A_MOVB, amem(D_SI, k), areg(D_AX)); + ins2(c, A_MOVB, areg(D_AX), amem(D_BX, k)); + k += 1; + } +} + /* cgplaceaddr — compute the ADDRESS of an arbitrary place (lvalue) * expression into dst_reg; returns 1 when the shape is wired, 0 * otherwise (the caller loud-stops — rule 7, never a silent drop). @@ -3111,6 +3142,43 @@ cg_structlit_fill(Cg *c, Local **locals_p, Type *lu, Node *lit, } continue; } + /* #49 (f38b/x5f-h): an aggregate field from an ADDRESSABLE + * source expr — `outer{.., r = r}` — fell to the scalar + * tail below and stored word0 only. Funnel: source address + * via aggarg_srcaddr (SI), field address via LEAQ (BX — + * loaded AFTER the source walk, which clobbers BX/AX), then + * cg_aggcopy. Non-addressable aggregate sources (tuple-lit, + * >24B/odd-tail call) die loud — pre-#49 they were the same + * silent word0 (rule 7). FULL alias chase (#22 precedent), + * not the region's single-peel `fu` — the wwstage twin + * full-chases the stamped tinfo; a single peel here would + * miss `type b = a; type a = struct` and silently diverge. */ + Type *fagg = type_chase_named(ft); + if (fagg && (fagg->kind == TY_STRUCT || fagg->kind == TY_ARRAY + || fagg->kind == TY_TUPLE)) { + if (!aggarg_srcaddr(c, f->lhs, D_SI, *locals_p)) + fatal("structlit fill: aggregate field '%s' " + "from a non-addressable source unwired " + "(task #49/rule-7)", + f->str ? f->str : "?"); + if (mode == DST_BP) + ins2(c, A_LEAQ, amem(D_BP, disp + (int)foff), + areg(D_BX)); + else { + if (mode == DST_PTR_LOCAL) + ins2(c, A_MOVQ, amem(D_BP, srcoff), + areg(D_BX)); + else + ins2(c, A_LEAQ, masym(c, name), + areg(D_BX)); + if (disp + (int)foff != 0) + ins2(c, A_ADDQ, + aimm(disp + (int)foff), + areg(D_BX)); + } + cg_aggcopy(c, (int)fagg->size); + continue; + } cgexpr(c, f->lhs, *locals_p); /* For non-BP modes, cgexpr just clobbered BX; reload it * before the store. */ @@ -6073,14 +6141,26 @@ cgexpr(Cg *c, Node *n, Local *locals) "tagged GLOBAL lvalue unwired"); } } + /* #49 (#31-A fold): an aggregate pointee diverts the whole + * deref-assign to the resolver aggregate arm below — the + * scalar tail here stored ONE word of `*p = s` (#31-A); + * tuple-lit (#31-E) and call (#31-G) rhs now die loud there + * instead of silently truncating. str/slice pointees keep + * their 3-word arm here (byte-id-pinned). */ + int deref_agg = 0; + if (n->lhs && n->lhs->kind == N_UN && n->lhs->op == TK_STAR + && n->op == TK_ASSIGN) { + Type *du = type_chase_named(n->lhs->type); + if (du && (du->kind == TY_STRUCT + || du->kind == TY_ARRAY + || du->kind == TY_TUPLE)) + deref_agg = 1; + } /* 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. - * 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). */ + * is evaluated after the value so we don't need to spill BX. */ if (n->lhs && n->lhs->kind == N_UN && n->lhs->op == TK_STAR - && n->op == TK_ASSIGN && !place_slit) { + && n->op == TK_ASSIGN && !place_slit && !deref_agg) { 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; @@ -6259,37 +6339,61 @@ cgexpr(Cg *c, Node *n, Local *locals) break; } } - /* Struct local reassignment: `s = expr;` where s is - * a TY_STRUCT local of size <=24B. Two rhs shapes, - * mirroring cglet's N_STRUCTLIT and the call-result - * branch above: - * - N_STRUCTLIT: walk fields, store at off+foff - * directly (same shape as the let-init branch). - * - N_CALL: cgexpr → AX/DX/CX, sized stores per the - * same ASYMMETRY rules documented at the N_LET - * receive site (MOVQ for full 8B chunks plus - * MOVL/MOVW/MOVB tail). The struct-IDENT word-copy - * rhs shape (s = p) is left unwired; #5 is scoped to - * the receive side of #4's cgreturn (calls + literals). - * Sizes >24B and non-{0,1,2,4}-byte tails fall through - * to the existing scalar path. */ - if (lu && (lu->kind == TY_STRUCT || lu->kind == TY_ARRAY) - && (int)lu->size <= 24) { + /* #49: aggregate (struct/array/tuple) IDENT + * reassignment — `s = expr;`. Literal and call rhs + * keep their dedicated receive arms; every OTHER rhs + * is an addressable source and funnels through the + * ONE mem-to-mem copy (aggarg_srcaddr → SI, dst + * address → BX, cg_aggcopy — the let-init copy's + * assign-position twin). Pre-#49 any shape that + * missed an arm fell to the scalar tail below and + * word0-copied: `b = a` lost every byte past 8 (ken + * f49_min; latent because lib style is let-init). + * The block never falls through to the scalar tail + * (rule 7). Keyed on the FULL alias chase (the #22 + * type_chase_named precedent), NOT the region's + * single-peel `lu` — `type b = a; type a = struct` + * left a TY_NAMED after one peel, missing the arm + * (the wwstage twin full-chases the stamped tinfo; + * a single peel here would silently diverge). */ + Type *au = type_chase_named(lt); + if (au && (au->kind == TY_STRUCT || au->kind == TY_ARRAY + || au->kind == TY_TUPLE)) { int off = localfind(locals, n->lhs->str); - if (off != 0) { - int sz = (int)lu->size; - if (n->rhs && n->rhs->kind == N_STRUCTLIT) { + int sz = (int)au->size; + int isglob = off == 0 && let_islet(n->lhs->str); + if (off == 0 && !isglob) + fatal("unsupported assign target: " + "unresolved identifier '%s'", + n->lhs->str); + if (n->rhs && n->rhs->kind == N_STRUCTLIT) { + if (off != 0) { /* Delegate to the shared BP-relative * structlit fill helper. Handles * TK_ELLIPSIS autofill, tagged fields, * float/scalar stores, AND nested * struct-typed structlit values via - * recursion (#17 silent-zero fix). */ - cg_structlit_fill_bp(c, &locals, lu, + * recursion (#17 silent-zero fix). + * #31-B: the pre-#49 ≤24B gate is + * lifted — the fill walks fields at + * any size; the wwstage twin never + * gated, so a >24B literal reassign + * was cs-zero/ww-filled (rule-10). */ + cg_structlit_fill_bp(c, &locals, au, n->rhs, off); break; } - if (n->rhs && n->rhs->kind == N_CALL + /* Global structlit reassign rides the + * DST_GLOBAL fill (the N_DOT global arms' + * machinery); pre-#49 it fell to the + * scalar tail and zeroed word0 only. */ + cg_structlit_fill(c, &locals, au, n->rhs, + DST_GLOBAL, 0, n->lhs->str, 0); + break; + } + if (n->rhs && n->rhs->kind == N_CALL) { + if (off != 0 && au->kind != TY_TUPLE + && sz <= 24 && (sz % 8 == 0 || sz % 8 == 1 || sz % 8 == 2 || sz % 8 == 4)) { @@ -6309,10 +6413,8 @@ cgexpr(Cg *c, Node *n, Local *locals) } break; } - } else if (lu->kind == TY_ARRAY - && let_islet(n->lhs->str) - && n->rhs && n->rhs->kind == N_CALL - && n->op == TK_ASSIGN) { + if (isglob && au->kind == TY_ARRAY + && sz <= 24) { /* #272: `g = f();` where g is a GLOBAL * aggregate ≤24B. The callee leaves the result * in AX/DX/CX (#272 reg-return); the scalar IDENT @@ -6323,28 +6425,45 @@ cgexpr(Cg *c, Node *n, Local *locals) * global arm above and the #220 sret-to-symbol path. * #276: this arm is TY_ARRAY-only — a ≤24B STRUCT * global receive can be float-class (X0/X1, not - * AX/DX/CX) so it stays at its pre-existing symmetric - * fall-through; closing it needs struct_float_class - * here. No consumer. Arrays are never float-class, so - * AX/DX/CX is always correct for this arm. */ - int sz = (int)lu->size; - cgexpr(c, n->rhs, locals); - ins2(c, A_LEAQ, masym(c, n->lhs->str), areg(D_DI)); - int regs[3] = { D_AX, D_DX, D_CX }; - int full = sz / 8; - int tail = sz % 8; - for (int i = 0; i < full; i++) - ins2(c, A_MOVQ, areg(regs[i]), - amem(D_DI, i * 8)); - if (tail > 0) { - int op = (tail == 4) ? A_MOVL - : (tail == 2) ? A_MOVW - : A_MOVB; - ins2(c, op, areg(regs[full]), - amem(D_DI, full * 8)); + * AX/DX/CX) so it has no receive here; pre-#49 + * it fell through symmetric-silent, now it dies + * loud below. No consumer. Arrays are never + * float-class, so AX/DX/CX is always correct + * for this arm. */ + cgexpr(c, n->rhs, locals); + ins2(c, A_LEAQ, masym(c, n->lhs->str), areg(D_DI)); + int regs[3] = { D_AX, D_DX, D_CX }; + int full = sz / 8; + int tail = sz % 8; + for (int i = 0; i < full; i++) + ins2(c, A_MOVQ, areg(regs[i]), + amem(D_DI, i * 8)); + if (tail > 0) { + int op = (tail == 4) ? A_MOVL + : (tail == 2) ? A_MOVW + : A_MOVB; + ins2(c, op, areg(regs[full]), + amem(D_DI, full * 8)); + } + break; } + fatal("assign: aggregate call receive " + "shape unwired (task #49/#276/" + "rule-7)"); + } + if (aggarg_srcaddr(c, n->rhs, D_SI, locals)) { + if (off != 0) + ins2(c, A_LEAQ, amem(D_BP, off), + areg(D_BX)); + else + ins2(c, A_LEAQ, + masym(c, n->lhs->str), + areg(D_BX)); + cg_aggcopy(c, sz); break; } + fatal("assign: aggregate rhs shape unwired " + "(task #49/rule-7)"); } } /* F6 (cgplaceaddr, commit C1): an N_DOT lvalue none of the @@ -6449,34 +6568,7 @@ cgexpr(Cg *c, Node *n, Local *locals) if (!placed) fatal("unsupported assign target " "shape"); - int k = 0; - for (; k + 8 <= fsz; k += 8) { - ins2(c, A_MOVQ, amem(D_SI, k), - areg(D_AX)); - ins2(c, A_MOVQ, areg(D_AX), - amem(D_BX, k)); - } - if (k + 4 <= fsz) { - ins2(c, A_MOVL, amem(D_SI, k), - areg(D_AX)); - ins2(c, A_MOVL, areg(D_AX), - amem(D_BX, k)); - k += 4; - } - if (k + 2 <= fsz) { - ins2(c, A_MOVW, amem(D_SI, k), - areg(D_AX)); - ins2(c, A_MOVW, areg(D_AX), - amem(D_BX, k)); - k += 2; - } - if (k + 1 <= fsz) { - ins2(c, A_MOVB, amem(D_SI, k), - areg(D_AX)); - ins2(c, A_MOVB, areg(D_AX), - amem(D_BX, k)); - k += 1; - } + cg_aggcopy(c, fsz); break; } if (fu && (fu->kind == TY_STR diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index e4538534..b7bdb488 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -18578,6 +18578,20 @@ fn istaggedtype(c: *cgen, t: *node) bool = { return typeistagged(t.type_: *tinfo); }; +// tnodeisagg — struct/array/tuple kind off the checker-STAMPED tinfo +// (the #49 funnel predicate; #209/#211 discipline — never tnode +// names). Mirror of cstage's chase-then-kind test at the fill / +// assign aggregate arms. +fn tnodeisagg(t: *node) bool = { + if (t == nil) { return false; }; + let u: *tinfo = t.type_: *tinfo; + for (u != nil && u.kind == tykind.TY_NAMED) { u = u.under; }; + if (u == nil) { return false; }; + if (u.kind == tykind.TY_STRUCT) { return true; }; + if (u.kind == tykind.TY_ARRAY) { return true; }; + return u.kind == tykind.TY_TUPLE; +}; + // isfloattype — f32 / f64 / untyped_float (alias-aware). Cite cstage // cgen.c:117 `cg_isfloat`. Dispatches MOVSS/MOVSD-shaped paths across // cglet, cgident, cgassign, cgbin, cgcast, cgcall, cgreturn, fn- @@ -20506,6 +20520,59 @@ fn cgstructlitfill(c: *cgen, si: *structinfo, lit: *node, }; }; fi = nil; + } else if (tnodeisagg(fi.tnode)) { + // #49 (f38b/x5f-h): an aggregate (struct/array/ + // tuple) field from an ADDRESSABLE source expr — + // `outer{.., r = r}` — fell to the scalar tail + // below and stored word0 only. Funnel: source + // address via aggargsrcaddr (SI), field address + // via LEAQ/ADDQ (BX — loaded AFTER the source + // walk, which clobbers BX/AX), then aggcopy. + // Width = the checker-STAMPED tinfo size (the + // cstage fl->type->size SSoT; fi.fsz is the + // slot-padded extent and skews on maxalign<8). + // Non-addressable aggregate sources (tuple-lit, + // >24B/odd-tail call) die loud — pre-#49 the + // same silent word0 (rule 7). Mirror of cstage + // cg_structlit_fill #49 arm. + if (!aggargsrcaddr(c, fieldnode.lhs, "SI")) { + let m49g: str = "structlit fill: aggregate field '"; + os.write(2, m49g.ptr, m49g.len: u64); + os.write(2, fname.ptr, fname.len: u64); + let m49h: str = "' from a non-addressable source unwired (task #49/rule-7)\n"; + os.write(2, m49h.ptr, m49h.len: u64); + os.exit(1); + }; + if (mode == 0) { + emitline("\tLEAQ\t"); + emitoff((disp + fi.foff): i64); + emitline("(BP), BX\n"); + } else { + if (mode == 1) { + emitline("\tMOVQ\t"); + emitoff(srcoff: i64); + emitline("(BP), BX\n"); + } else { + emitline("\tLEAQ\t"); + emitsymname(c, srcname); + emitline("(SB), BX\n"); + }; + if (disp + fi.foff != 0) { + emitline("\tADDQ\t$"); + emitint((disp + fi.foff): i64); + emitline(", BX\n"); + }; + }; + let agsz49: i32 = 0; + if (fi.tnode != nil) { + let agti49: *tinfo = fi.tnode.type_: *tinfo; + for (agti49 != nil && agti49.kind == tykind.TY_NAMED) { + agti49 = agti49.under; + }; + if (agti49 != nil) { agsz49 = agti49.size: i32; }; + }; + aggcopy(c, agsz49); + fi = nil; } else { cgexpr(c, fieldnode.lhs); // For non-BP modes, cgexpr just clobbered @@ -21952,6 +22019,53 @@ fn aggargsrcaddr(c: *cgen, src: *node, dst: str) bool = { return false; }; +// aggcopy — the ONE place-resolved mem-to-mem aggregate copy: sz +// bytes (SI) → (BX) via AX, a MOVQ run plus a 4/2/1 sized tail. +// Extracted verbatim from the C1.25 assign-resolver tail so every +// aggregate copy position (resolver field store, #49 ident reassign, +// #49 structlit fill-field) funnels through one loop — close-by- +// construction, no per-site width logic to skew. Mirror of cstage +// cg_aggcopy. +fn aggcopy(c: *cgen, sz: i32) void = { + let k: i32 = 0; + for (k + 8 <= sz) { + emitline("\tMOVQ\t"); + emitoff(k: i64); + emitline("(SI), AX\n"); + emitline("\tMOVQ\tAX, "); + emitoff(k: i64); + emitline("(BX)\n"); + k += 8; + }; + if (k + 4 <= sz) { + emitline("\tMOVL\t"); + emitoff(k: i64); + emitline("(SI), AX\n"); + emitline("\tMOVL\tAX, "); + emitoff(k: i64); + emitline("(BX)\n"); + k += 4; + }; + if (k + 2 <= sz) { + emitline("\tMOVW\t"); + emitoff(k: i64); + emitline("(SI), AX\n"); + emitline("\tMOVW\tAX, "); + emitoff(k: i64); + emitline("(BX)\n"); + k += 2; + }; + if (k + 1 <= sz) { + emitline("\tMOVB\t"); + emitoff(k: i64); + emitline("(SI), AX\n"); + emitline("\tMOVB\tAX, "); + emitoff(k: i64); + emitline("(BX)\n"); + k += 1; + }; +}; + // cgplaceaddr — compute the ADDRESS of an arbitrary place (lvalue) // expression into dstreg; returns true when the shape is wired, false // otherwise (the caller loud-stops — rule 7, never a silent drop). @@ -27588,6 +27702,29 @@ fn cgassign(c: *cgen, n: *node) void = { }; }; }; + // #49 (#31-A fold): an aggregate pointee diverts the whole + // deref-assign to the resolver aggregate arm below — the scalar + // tail there stored ONE word of `*p = s` (#31-A); tuple-lit + // (#31-E) and call (#31-G) rhs now die loud there instead of + // silently truncating. str/slice pointees keep their 3-word arm + // (byte-id-pinned). Mirror of cstage deref_agg. + let derefagg: bool = false; + if (lhs != nil) { + if (lhs.kind == nkind.N_UN && lhs.op == tkind.TK_STAR + && n.op == tkind.TK_ASSIGN) { + let du: *tinfo = lhs.type_: *tinfo; + for (du != nil && du.kind == tykind.TY_NAMED) { + du = du.under; + }; + if (du != nil) { + if (du.kind == tykind.TY_STRUCT + || du.kind == tykind.TY_ARRAY + || du.kind == tykind.TY_TUPLE) { + derefagg = 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). @@ -27707,7 +27844,8 @@ fn cgassign(c: *cgen, n: *node) void = { if (lhs != nil) { if (lhs.kind == nkind.N_UN) { if (lhs.op == tkind.TK_STAR) { - if (n.op == tkind.TK_ASSIGN && !placeslit) { + if (n.op == tkind.TK_ASSIGN && !placeslit + && !derefagg) { let inner: *node = lhs.lhs; let elemstr: bool = false; let elemfloat: bool = false; @@ -30439,6 +30577,56 @@ fn cgassign(c: *cgen, n: *node) void = { return; }; }; + // #49 (global twin): aggregate module-let reassign — + // `g = a` / `g = pt{...}`. Same funnel as the local + // arm: structlit → mode-2 (DST_GLOBAL) fill; call → + // loud (#276: ≤24B struct global receive was a + // documented symmetric fall-through, now loud); + // addressable rhs → aggargsrcaddr + LEAQ g(SB), BX + // + aggcopy. Pre-#49 every shape fell to the scalar + // tail below: one MOVQ AX, g(SB). + if (n.op == tkind.TK_ASSIGN && lvftn != nil) { + let gau: *tinfo = lvftn.type_: *tinfo; + for (gau != nil && gau.kind == tykind.TY_NAMED) { + gau = gau.under; + }; + if (gau != nil) { + if (gau.kind == tykind.TY_STRUCT + || gau.kind == tykind.TY_ARRAY + || gau.kind == tykind.TY_TUPLE) { + if (n.rhs != nil && n.rhs.kind == nkind.N_STRUCTLIT) { + let gsi49: *structinfo = nil; + if (lvftn.kind == nkind.N_TNAME) { + gsi49 = structlookup(c, lvftn.str); + }; + if (gsi49 == nil) { + // wwstage-only bail (anonymous + // type; the @placescr precedent). + let m49d: str = "assign: structlit layout unresolved (rule-7)\n"; + os.write(2, m49d.ptr, m49d.len: u64); + os.exit(1); + }; + cgstructlitfill(c, gsi49, n.rhs, 2, 0, nm, 0); + return; + }; + if (n.rhs != nil && n.rhs.kind == nkind.N_CALL) { + let m49e: str = "assign: aggregate call receive shape unwired (task #49/#276/rule-7)\n"; + os.write(2, m49e.ptr, m49e.len: u64); + os.exit(1); + }; + if (aggargsrcaddr(c, n.rhs, "SI")) { + emitline("\tLEAQ\t"); + emitsymname(c, nm); + emitline("(SB), BX\n"); + aggcopy(c, gau.size: i32); + return; + }; + let m49f: str = "assign: aggregate rhs shape unwired (task #49/rule-7)\n"; + os.write(2, m49f.ptr, m49f.len: u64); + os.exit(1); + }; + }; + }; cgexpr(c, n.rhs); if (n.op == tkind.TK_ASSIGN) { // str/slice top-level let: str IS []u8, so both store the @@ -30817,6 +31005,60 @@ fn cgassign(c: *cgen, n: *node) void = { emitline("(BP)\n"); return; }; + // #49: aggregate (struct/array/tuple) IDENT + // reassignment — any rhs shape that missed the + // dedicated arms above (struct-lit fill, call + // receive, sret) funnels through the ONE mem-to-mem + // copy (aggargsrcaddr → SI, dst → BX, aggcopy), or + // dies loud. Pre-#49 it fell to the scalar tail + // below and word0-copied `b = a` (cstage cgen.c + // N_ASSIGN aggregate-ident twin). Kind keys off the + // checker-STAMPED tinfo (#209/#211 discipline). + if (n.op == tkind.TK_ASSIGN) { + let agu: *tinfo = nil; + if (lcn != nil) { + if (lcn.tnode != nil) { + agu = lcn.tnode.type_: *tinfo; + }; + }; + for (agu != nil && agu.kind == tykind.TY_NAMED) { + agu = agu.under; + }; + if (agu != nil) { + if (agu.kind == tykind.TY_STRUCT + || agu.kind == tykind.TY_ARRAY + || agu.kind == tykind.TY_TUPLE) { + if (n.rhs != nil && n.rhs.kind == nkind.N_STRUCTLIT) { + // wwstage-only bail: the fill is + // structinfo-keyed; a literal + // reaching past the name-keyed arm + // above has no registry entry + // (anonymous struct type). Loud, + // rule 7 (the resolver @placescr + // precedent); cstage fills from + // Type directly. + let m49a: str = "assign: structlit layout unresolved (rule-7)\n"; + os.write(2, m49a.ptr, m49a.len: u64); + os.exit(1); + }; + if (n.rhs != nil && n.rhs.kind == nkind.N_CALL) { + let m49b: str = "assign: aggregate call receive shape unwired (task #49/#276/rule-7)\n"; + os.write(2, m49b.ptr, m49b.len: u64); + os.exit(1); + }; + if (aggargsrcaddr(c, n.rhs, "SI")) { + emitline("\tLEAQ\t"); + emitoff(off: i64); + emitline("(BP), BX\n"); + aggcopy(c, agu.size: i32); + return; + }; + let m49c: str = "assign: aggregate rhs shape unwired (task #49/rule-7)\n"; + os.write(2, m49c.ptr, m49c.len: u64); + os.exit(1); + }; + }; + }; cgexpr(c, n.rhs); if (n.op == tkind.TK_ASSIGN) { emitline("\tMOVQ\tAX, "); @@ -31067,43 +31309,7 @@ fn cgassign(c: *cgen, n: *node) void = { os.write(2, mau.ptr, mau.len: u64); os.exit(1); }; - let kc: i32 = 0; - for (kc + 8 <= fsz) { - emitline("\tMOVQ\t"); - emitoff(kc: i64); - emitline("(SI), AX\n"); - emitline("\tMOVQ\tAX, "); - emitoff(kc: i64); - emitline("(BX)\n"); - kc += 8; - }; - if (kc + 4 <= fsz) { - emitline("\tMOVL\t"); - emitoff(kc: i64); - emitline("(SI), AX\n"); - emitline("\tMOVL\tAX, "); - emitoff(kc: i64); - emitline("(BX)\n"); - kc += 4; - }; - if (kc + 2 <= fsz) { - emitline("\tMOVW\t"); - emitoff(kc: i64); - emitline("(SI), AX\n"); - emitline("\tMOVW\tAX, "); - emitoff(kc: i64); - emitline("(BX)\n"); - kc += 2; - }; - if (kc + 1 <= fsz) { - emitline("\tMOVB\t"); - emitoff(kc: i64); - emitline("(SI), AX\n"); - emitline("\tMOVB\tAX, "); - emitoff(kc: i64); - emitline("(BX)\n"); - kc += 1; - }; + aggcopy(c, fsz); return; }; }; diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index 6eb9d7be..ff90ca1f 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -1374,6 +1374,53 @@ fn aggargsrcaddr(c: *cgen, src: *node, dst: str) bool = { return false; }; +// aggcopy — the ONE place-resolved mem-to-mem aggregate copy: sz +// bytes (SI) → (BX) via AX, a MOVQ run plus a 4/2/1 sized tail. +// Extracted verbatim from the C1.25 assign-resolver tail so every +// aggregate copy position (resolver field store, #49 ident reassign, +// #49 structlit fill-field) funnels through one loop — close-by- +// construction, no per-site width logic to skew. Mirror of cstage +// cg_aggcopy. +fn aggcopy(c: *cgen, sz: i32) void = { + let k: i32 = 0; + for (k + 8 <= sz) { + emitline("\tMOVQ\t"); + emitoff(k: i64); + emitline("(SI), AX\n"); + emitline("\tMOVQ\tAX, "); + emitoff(k: i64); + emitline("(BX)\n"); + k += 8; + }; + if (k + 4 <= sz) { + emitline("\tMOVL\t"); + emitoff(k: i64); + emitline("(SI), AX\n"); + emitline("\tMOVL\tAX, "); + emitoff(k: i64); + emitline("(BX)\n"); + k += 4; + }; + if (k + 2 <= sz) { + emitline("\tMOVW\t"); + emitoff(k: i64); + emitline("(SI), AX\n"); + emitline("\tMOVW\tAX, "); + emitoff(k: i64); + emitline("(BX)\n"); + k += 2; + }; + if (k + 1 <= sz) { + emitline("\tMOVB\t"); + emitoff(k: i64); + emitline("(SI), AX\n"); + emitline("\tMOVB\tAX, "); + emitoff(k: i64); + emitline("(BX)\n"); + k += 1; + }; +}; + // cgplaceaddr — compute the ADDRESS of an arbitrary place (lvalue) // expression into dstreg; returns true when the shape is wired, false // otherwise (the caller loud-stops — rule 7, never a silent drop). @@ -7010,6 +7057,29 @@ fn cgassign(c: *cgen, n: *node) void = { }; }; }; + // #49 (#31-A fold): an aggregate pointee diverts the whole + // deref-assign to the resolver aggregate arm below — the scalar + // tail there stored ONE word of `*p = s` (#31-A); tuple-lit + // (#31-E) and call (#31-G) rhs now die loud there instead of + // silently truncating. str/slice pointees keep their 3-word arm + // (byte-id-pinned). Mirror of cstage deref_agg. + let derefagg: bool = false; + if (lhs != nil) { + if (lhs.kind == nkind.N_UN && lhs.op == tkind.TK_STAR + && n.op == tkind.TK_ASSIGN) { + let du: *tinfo = lhs.type_: *tinfo; + for (du != nil && du.kind == tykind.TY_NAMED) { + du = du.under; + }; + if (du != nil) { + if (du.kind == tykind.TY_STRUCT + || du.kind == tykind.TY_ARRAY + || du.kind == tykind.TY_TUPLE) { + derefagg = 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). @@ -7129,7 +7199,8 @@ fn cgassign(c: *cgen, n: *node) void = { if (lhs != nil) { if (lhs.kind == nkind.N_UN) { if (lhs.op == tkind.TK_STAR) { - if (n.op == tkind.TK_ASSIGN && !placeslit) { + if (n.op == tkind.TK_ASSIGN && !placeslit + && !derefagg) { let inner: *node = lhs.lhs; let elemstr: bool = false; let elemfloat: bool = false; @@ -9861,6 +9932,56 @@ fn cgassign(c: *cgen, n: *node) void = { return; }; }; + // #49 (global twin): aggregate module-let reassign — + // `g = a` / `g = pt{...}`. Same funnel as the local + // arm: structlit → mode-2 (DST_GLOBAL) fill; call → + // loud (#276: ≤24B struct global receive was a + // documented symmetric fall-through, now loud); + // addressable rhs → aggargsrcaddr + LEAQ g(SB), BX + // + aggcopy. Pre-#49 every shape fell to the scalar + // tail below: one MOVQ AX, g(SB). + if (n.op == tkind.TK_ASSIGN && lvftn != nil) { + let gau: *tinfo = lvftn.type_: *tinfo; + for (gau != nil && gau.kind == tykind.TY_NAMED) { + gau = gau.under; + }; + if (gau != nil) { + if (gau.kind == tykind.TY_STRUCT + || gau.kind == tykind.TY_ARRAY + || gau.kind == tykind.TY_TUPLE) { + if (n.rhs != nil && n.rhs.kind == nkind.N_STRUCTLIT) { + let gsi49: *structinfo = nil; + if (lvftn.kind == nkind.N_TNAME) { + gsi49 = structlookup(c, lvftn.str); + }; + if (gsi49 == nil) { + // wwstage-only bail (anonymous + // type; the @placescr precedent). + let m49d: str = "assign: structlit layout unresolved (rule-7)\n"; + os.write(2, m49d.ptr, m49d.len: u64); + os.exit(1); + }; + cgstructlitfill(c, gsi49, n.rhs, 2, 0, nm, 0); + return; + }; + if (n.rhs != nil && n.rhs.kind == nkind.N_CALL) { + let m49e: str = "assign: aggregate call receive shape unwired (task #49/#276/rule-7)\n"; + os.write(2, m49e.ptr, m49e.len: u64); + os.exit(1); + }; + if (aggargsrcaddr(c, n.rhs, "SI")) { + emitline("\tLEAQ\t"); + emitsymname(c, nm); + emitline("(SB), BX\n"); + aggcopy(c, gau.size: i32); + return; + }; + let m49f: str = "assign: aggregate rhs shape unwired (task #49/rule-7)\n"; + os.write(2, m49f.ptr, m49f.len: u64); + os.exit(1); + }; + }; + }; cgexpr(c, n.rhs); if (n.op == tkind.TK_ASSIGN) { // str/slice top-level let: str IS []u8, so both store the @@ -10239,6 +10360,60 @@ fn cgassign(c: *cgen, n: *node) void = { emitline("(BP)\n"); return; }; + // #49: aggregate (struct/array/tuple) IDENT + // reassignment — any rhs shape that missed the + // dedicated arms above (struct-lit fill, call + // receive, sret) funnels through the ONE mem-to-mem + // copy (aggargsrcaddr → SI, dst → BX, aggcopy), or + // dies loud. Pre-#49 it fell to the scalar tail + // below and word0-copied `b = a` (cstage cgen.c + // N_ASSIGN aggregate-ident twin). Kind keys off the + // checker-STAMPED tinfo (#209/#211 discipline). + if (n.op == tkind.TK_ASSIGN) { + let agu: *tinfo = nil; + if (lcn != nil) { + if (lcn.tnode != nil) { + agu = lcn.tnode.type_: *tinfo; + }; + }; + for (agu != nil && agu.kind == tykind.TY_NAMED) { + agu = agu.under; + }; + if (agu != nil) { + if (agu.kind == tykind.TY_STRUCT + || agu.kind == tykind.TY_ARRAY + || agu.kind == tykind.TY_TUPLE) { + if (n.rhs != nil && n.rhs.kind == nkind.N_STRUCTLIT) { + // wwstage-only bail: the fill is + // structinfo-keyed; a literal + // reaching past the name-keyed arm + // above has no registry entry + // (anonymous struct type). Loud, + // rule 7 (the resolver @placescr + // precedent); cstage fills from + // Type directly. + let m49a: str = "assign: structlit layout unresolved (rule-7)\n"; + os.write(2, m49a.ptr, m49a.len: u64); + os.exit(1); + }; + if (n.rhs != nil && n.rhs.kind == nkind.N_CALL) { + let m49b: str = "assign: aggregate call receive shape unwired (task #49/#276/rule-7)\n"; + os.write(2, m49b.ptr, m49b.len: u64); + os.exit(1); + }; + if (aggargsrcaddr(c, n.rhs, "SI")) { + emitline("\tLEAQ\t"); + emitoff(off: i64); + emitline("(BP), BX\n"); + aggcopy(c, agu.size: i32); + return; + }; + let m49c: str = "assign: aggregate rhs shape unwired (task #49/rule-7)\n"; + os.write(2, m49c.ptr, m49c.len: u64); + os.exit(1); + }; + }; + }; cgexpr(c, n.rhs); if (n.op == tkind.TK_ASSIGN) { emitline("\tMOVQ\tAX, "); @@ -10489,43 +10664,7 @@ fn cgassign(c: *cgen, n: *node) void = { os.write(2, mau.ptr, mau.len: u64); os.exit(1); }; - let kc: i32 = 0; - for (kc + 8 <= fsz) { - emitline("\tMOVQ\t"); - emitoff(kc: i64); - emitline("(SI), AX\n"); - emitline("\tMOVQ\tAX, "); - emitoff(kc: i64); - emitline("(BX)\n"); - kc += 8; - }; - if (kc + 4 <= fsz) { - emitline("\tMOVL\t"); - emitoff(kc: i64); - emitline("(SI), AX\n"); - emitline("\tMOVL\tAX, "); - emitoff(kc: i64); - emitline("(BX)\n"); - kc += 4; - }; - if (kc + 2 <= fsz) { - emitline("\tMOVW\t"); - emitoff(kc: i64); - emitline("(SI), AX\n"); - emitline("\tMOVW\tAX, "); - emitoff(kc: i64); - emitline("(BX)\n"); - kc += 2; - }; - if (kc + 1 <= fsz) { - emitline("\tMOVB\t"); - emitoff(kc: i64); - emitline("(SI), AX\n"); - emitline("\tMOVB\tAX, "); - emitoff(kc: i64); - emitline("(BX)\n"); - kc += 1; - }; + aggcopy(c, fsz); return; }; }; diff --git a/selfhost/cmd/wcc/cgenutil.ww b/selfhost/cmd/wcc/cgenutil.ww index d1236356..7a588be6 100644 --- a/selfhost/cmd/wcc/cgenutil.ww +++ b/selfhost/cmd/wcc/cgenutil.ww @@ -2568,6 +2568,20 @@ fn istaggedtype(c: *cgen, t: *node) bool = { return typeistagged(t.type_: *tinfo); }; +// tnodeisagg — struct/array/tuple kind off the checker-STAMPED tinfo +// (the #49 funnel predicate; #209/#211 discipline — never tnode +// names). Mirror of cstage's chase-then-kind test at the fill / +// assign aggregate arms. +fn tnodeisagg(t: *node) bool = { + if (t == nil) { return false; }; + let u: *tinfo = t.type_: *tinfo; + for (u != nil && u.kind == tykind.TY_NAMED) { u = u.under; }; + if (u == nil) { return false; }; + if (u.kind == tykind.TY_STRUCT) { return true; }; + if (u.kind == tykind.TY_ARRAY) { return true; }; + return u.kind == tykind.TY_TUPLE; +}; + // isfloattype — f32 / f64 / untyped_float (alias-aware). Cite cstage // cgen.c:117 `cg_isfloat`. Dispatches MOVSS/MOVSD-shaped paths across // cglet, cgident, cgassign, cgbin, cgcast, cgcall, cgreturn, fn- @@ -4496,6 +4510,59 @@ fn cgstructlitfill(c: *cgen, si: *structinfo, lit: *node, }; }; fi = nil; + } else if (tnodeisagg(fi.tnode)) { + // #49 (f38b/x5f-h): an aggregate (struct/array/ + // tuple) field from an ADDRESSABLE source expr — + // `outer{.., r = r}` — fell to the scalar tail + // below and stored word0 only. Funnel: source + // address via aggargsrcaddr (SI), field address + // via LEAQ/ADDQ (BX — loaded AFTER the source + // walk, which clobbers BX/AX), then aggcopy. + // Width = the checker-STAMPED tinfo size (the + // cstage fl->type->size SSoT; fi.fsz is the + // slot-padded extent and skews on maxalign<8). + // Non-addressable aggregate sources (tuple-lit, + // >24B/odd-tail call) die loud — pre-#49 the + // same silent word0 (rule 7). Mirror of cstage + // cg_structlit_fill #49 arm. + if (!aggargsrcaddr(c, fieldnode.lhs, "SI")) { + let m49g: str = "structlit fill: aggregate field '"; + os.write(2, m49g.ptr, m49g.len: u64); + os.write(2, fname.ptr, fname.len: u64); + let m49h: str = "' from a non-addressable source unwired (task #49/rule-7)\n"; + os.write(2, m49h.ptr, m49h.len: u64); + os.exit(1); + }; + if (mode == 0) { + emitline("\tLEAQ\t"); + emitoff((disp + fi.foff): i64); + emitline("(BP), BX\n"); + } else { + if (mode == 1) { + emitline("\tMOVQ\t"); + emitoff(srcoff: i64); + emitline("(BP), BX\n"); + } else { + emitline("\tLEAQ\t"); + emitsymname(c, srcname); + emitline("(SB), BX\n"); + }; + if (disp + fi.foff != 0) { + emitline("\tADDQ\t$"); + emitint((disp + fi.foff): i64); + emitline(", BX\n"); + }; + }; + let agsz49: i32 = 0; + if (fi.tnode != nil) { + let agti49: *tinfo = fi.tnode.type_: *tinfo; + for (agti49 != nil && agti49.kind == tykind.TY_NAMED) { + agti49 = agti49.under; + }; + if (agti49 != nil) { agsz49 = agti49.size: i32; }; + }; + aggcopy(c, agsz49); + fi = nil; } else { cgexpr(c, fieldnode.lhs); // For non-BP modes, cgexpr just clobbered diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 37971012..c09f7be9 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -18578,6 +18578,20 @@ fn istaggedtype(c: *cgen, t: *node) bool = { return typeistagged(t.type_: *tinfo); }; +// tnodeisagg — struct/array/tuple kind off the checker-STAMPED tinfo +// (the #49 funnel predicate; #209/#211 discipline — never tnode +// names). Mirror of cstage's chase-then-kind test at the fill / +// assign aggregate arms. +fn tnodeisagg(t: *node) bool = { + if (t == nil) { return false; }; + let u: *tinfo = t.type_: *tinfo; + for (u != nil && u.kind == tykind.TY_NAMED) { u = u.under; }; + if (u == nil) { return false; }; + if (u.kind == tykind.TY_STRUCT) { return true; }; + if (u.kind == tykind.TY_ARRAY) { return true; }; + return u.kind == tykind.TY_TUPLE; +}; + // isfloattype — f32 / f64 / untyped_float (alias-aware). Cite cstage // cgen.c:117 `cg_isfloat`. Dispatches MOVSS/MOVSD-shaped paths across // cglet, cgident, cgassign, cgbin, cgcast, cgcall, cgreturn, fn- @@ -20506,6 +20520,59 @@ fn cgstructlitfill(c: *cgen, si: *structinfo, lit: *node, }; }; fi = nil; + } else if (tnodeisagg(fi.tnode)) { + // #49 (f38b/x5f-h): an aggregate (struct/array/ + // tuple) field from an ADDRESSABLE source expr — + // `outer{.., r = r}` — fell to the scalar tail + // below and stored word0 only. Funnel: source + // address via aggargsrcaddr (SI), field address + // via LEAQ/ADDQ (BX — loaded AFTER the source + // walk, which clobbers BX/AX), then aggcopy. + // Width = the checker-STAMPED tinfo size (the + // cstage fl->type->size SSoT; fi.fsz is the + // slot-padded extent and skews on maxalign<8). + // Non-addressable aggregate sources (tuple-lit, + // >24B/odd-tail call) die loud — pre-#49 the + // same silent word0 (rule 7). Mirror of cstage + // cg_structlit_fill #49 arm. + if (!aggargsrcaddr(c, fieldnode.lhs, "SI")) { + let m49g: str = "structlit fill: aggregate field '"; + os.write(2, m49g.ptr, m49g.len: u64); + os.write(2, fname.ptr, fname.len: u64); + let m49h: str = "' from a non-addressable source unwired (task #49/rule-7)\n"; + os.write(2, m49h.ptr, m49h.len: u64); + os.exit(1); + }; + if (mode == 0) { + emitline("\tLEAQ\t"); + emitoff((disp + fi.foff): i64); + emitline("(BP), BX\n"); + } else { + if (mode == 1) { + emitline("\tMOVQ\t"); + emitoff(srcoff: i64); + emitline("(BP), BX\n"); + } else { + emitline("\tLEAQ\t"); + emitsymname(c, srcname); + emitline("(SB), BX\n"); + }; + if (disp + fi.foff != 0) { + emitline("\tADDQ\t$"); + emitint((disp + fi.foff): i64); + emitline(", BX\n"); + }; + }; + let agsz49: i32 = 0; + if (fi.tnode != nil) { + let agti49: *tinfo = fi.tnode.type_: *tinfo; + for (agti49 != nil && agti49.kind == tykind.TY_NAMED) { + agti49 = agti49.under; + }; + if (agti49 != nil) { agsz49 = agti49.size: i32; }; + }; + aggcopy(c, agsz49); + fi = nil; } else { cgexpr(c, fieldnode.lhs); // For non-BP modes, cgexpr just clobbered @@ -21952,6 +22019,53 @@ fn aggargsrcaddr(c: *cgen, src: *node, dst: str) bool = { return false; }; +// aggcopy — the ONE place-resolved mem-to-mem aggregate copy: sz +// bytes (SI) → (BX) via AX, a MOVQ run plus a 4/2/1 sized tail. +// Extracted verbatim from the C1.25 assign-resolver tail so every +// aggregate copy position (resolver field store, #49 ident reassign, +// #49 structlit fill-field) funnels through one loop — close-by- +// construction, no per-site width logic to skew. Mirror of cstage +// cg_aggcopy. +fn aggcopy(c: *cgen, sz: i32) void = { + let k: i32 = 0; + for (k + 8 <= sz) { + emitline("\tMOVQ\t"); + emitoff(k: i64); + emitline("(SI), AX\n"); + emitline("\tMOVQ\tAX, "); + emitoff(k: i64); + emitline("(BX)\n"); + k += 8; + }; + if (k + 4 <= sz) { + emitline("\tMOVL\t"); + emitoff(k: i64); + emitline("(SI), AX\n"); + emitline("\tMOVL\tAX, "); + emitoff(k: i64); + emitline("(BX)\n"); + k += 4; + }; + if (k + 2 <= sz) { + emitline("\tMOVW\t"); + emitoff(k: i64); + emitline("(SI), AX\n"); + emitline("\tMOVW\tAX, "); + emitoff(k: i64); + emitline("(BX)\n"); + k += 2; + }; + if (k + 1 <= sz) { + emitline("\tMOVB\t"); + emitoff(k: i64); + emitline("(SI), AX\n"); + emitline("\tMOVB\tAX, "); + emitoff(k: i64); + emitline("(BX)\n"); + k += 1; + }; +}; + // cgplaceaddr — compute the ADDRESS of an arbitrary place (lvalue) // expression into dstreg; returns true when the shape is wired, false // otherwise (the caller loud-stops — rule 7, never a silent drop). @@ -27588,6 +27702,29 @@ fn cgassign(c: *cgen, n: *node) void = { }; }; }; + // #49 (#31-A fold): an aggregate pointee diverts the whole + // deref-assign to the resolver aggregate arm below — the scalar + // tail there stored ONE word of `*p = s` (#31-A); tuple-lit + // (#31-E) and call (#31-G) rhs now die loud there instead of + // silently truncating. str/slice pointees keep their 3-word arm + // (byte-id-pinned). Mirror of cstage deref_agg. + let derefagg: bool = false; + if (lhs != nil) { + if (lhs.kind == nkind.N_UN && lhs.op == tkind.TK_STAR + && n.op == tkind.TK_ASSIGN) { + let du: *tinfo = lhs.type_: *tinfo; + for (du != nil && du.kind == tykind.TY_NAMED) { + du = du.under; + }; + if (du != nil) { + if (du.kind == tykind.TY_STRUCT + || du.kind == tykind.TY_ARRAY + || du.kind == tykind.TY_TUPLE) { + derefagg = 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). @@ -27707,7 +27844,8 @@ fn cgassign(c: *cgen, n: *node) void = { if (lhs != nil) { if (lhs.kind == nkind.N_UN) { if (lhs.op == tkind.TK_STAR) { - if (n.op == tkind.TK_ASSIGN && !placeslit) { + if (n.op == tkind.TK_ASSIGN && !placeslit + && !derefagg) { let inner: *node = lhs.lhs; let elemstr: bool = false; let elemfloat: bool = false; @@ -30439,6 +30577,56 @@ fn cgassign(c: *cgen, n: *node) void = { return; }; }; + // #49 (global twin): aggregate module-let reassign — + // `g = a` / `g = pt{...}`. Same funnel as the local + // arm: structlit → mode-2 (DST_GLOBAL) fill; call → + // loud (#276: ≤24B struct global receive was a + // documented symmetric fall-through, now loud); + // addressable rhs → aggargsrcaddr + LEAQ g(SB), BX + // + aggcopy. Pre-#49 every shape fell to the scalar + // tail below: one MOVQ AX, g(SB). + if (n.op == tkind.TK_ASSIGN && lvftn != nil) { + let gau: *tinfo = lvftn.type_: *tinfo; + for (gau != nil && gau.kind == tykind.TY_NAMED) { + gau = gau.under; + }; + if (gau != nil) { + if (gau.kind == tykind.TY_STRUCT + || gau.kind == tykind.TY_ARRAY + || gau.kind == tykind.TY_TUPLE) { + if (n.rhs != nil && n.rhs.kind == nkind.N_STRUCTLIT) { + let gsi49: *structinfo = nil; + if (lvftn.kind == nkind.N_TNAME) { + gsi49 = structlookup(c, lvftn.str); + }; + if (gsi49 == nil) { + // wwstage-only bail (anonymous + // type; the @placescr precedent). + let m49d: str = "assign: structlit layout unresolved (rule-7)\n"; + os.write(2, m49d.ptr, m49d.len: u64); + os.exit(1); + }; + cgstructlitfill(c, gsi49, n.rhs, 2, 0, nm, 0); + return; + }; + if (n.rhs != nil && n.rhs.kind == nkind.N_CALL) { + let m49e: str = "assign: aggregate call receive shape unwired (task #49/#276/rule-7)\n"; + os.write(2, m49e.ptr, m49e.len: u64); + os.exit(1); + }; + if (aggargsrcaddr(c, n.rhs, "SI")) { + emitline("\tLEAQ\t"); + emitsymname(c, nm); + emitline("(SB), BX\n"); + aggcopy(c, gau.size: i32); + return; + }; + let m49f: str = "assign: aggregate rhs shape unwired (task #49/rule-7)\n"; + os.write(2, m49f.ptr, m49f.len: u64); + os.exit(1); + }; + }; + }; cgexpr(c, n.rhs); if (n.op == tkind.TK_ASSIGN) { // str/slice top-level let: str IS []u8, so both store the @@ -30817,6 +31005,60 @@ fn cgassign(c: *cgen, n: *node) void = { emitline("(BP)\n"); return; }; + // #49: aggregate (struct/array/tuple) IDENT + // reassignment — any rhs shape that missed the + // dedicated arms above (struct-lit fill, call + // receive, sret) funnels through the ONE mem-to-mem + // copy (aggargsrcaddr → SI, dst → BX, aggcopy), or + // dies loud. Pre-#49 it fell to the scalar tail + // below and word0-copied `b = a` (cstage cgen.c + // N_ASSIGN aggregate-ident twin). Kind keys off the + // checker-STAMPED tinfo (#209/#211 discipline). + if (n.op == tkind.TK_ASSIGN) { + let agu: *tinfo = nil; + if (lcn != nil) { + if (lcn.tnode != nil) { + agu = lcn.tnode.type_: *tinfo; + }; + }; + for (agu != nil && agu.kind == tykind.TY_NAMED) { + agu = agu.under; + }; + if (agu != nil) { + if (agu.kind == tykind.TY_STRUCT + || agu.kind == tykind.TY_ARRAY + || agu.kind == tykind.TY_TUPLE) { + if (n.rhs != nil && n.rhs.kind == nkind.N_STRUCTLIT) { + // wwstage-only bail: the fill is + // structinfo-keyed; a literal + // reaching past the name-keyed arm + // above has no registry entry + // (anonymous struct type). Loud, + // rule 7 (the resolver @placescr + // precedent); cstage fills from + // Type directly. + let m49a: str = "assign: structlit layout unresolved (rule-7)\n"; + os.write(2, m49a.ptr, m49a.len: u64); + os.exit(1); + }; + if (n.rhs != nil && n.rhs.kind == nkind.N_CALL) { + let m49b: str = "assign: aggregate call receive shape unwired (task #49/#276/rule-7)\n"; + os.write(2, m49b.ptr, m49b.len: u64); + os.exit(1); + }; + if (aggargsrcaddr(c, n.rhs, "SI")) { + emitline("\tLEAQ\t"); + emitoff(off: i64); + emitline("(BP), BX\n"); + aggcopy(c, agu.size: i32); + return; + }; + let m49c: str = "assign: aggregate rhs shape unwired (task #49/rule-7)\n"; + os.write(2, m49c.ptr, m49c.len: u64); + os.exit(1); + }; + }; + }; cgexpr(c, n.rhs); if (n.op == tkind.TK_ASSIGN) { emitline("\tMOVQ\tAX, "); @@ -31067,43 +31309,7 @@ fn cgassign(c: *cgen, n: *node) void = { os.write(2, mau.ptr, mau.len: u64); os.exit(1); }; - let kc: i32 = 0; - for (kc + 8 <= fsz) { - emitline("\tMOVQ\t"); - emitoff(kc: i64); - emitline("(SI), AX\n"); - emitline("\tMOVQ\tAX, "); - emitoff(kc: i64); - emitline("(BX)\n"); - kc += 8; - }; - if (kc + 4 <= fsz) { - emitline("\tMOVL\t"); - emitoff(kc: i64); - emitline("(SI), AX\n"); - emitline("\tMOVL\tAX, "); - emitoff(kc: i64); - emitline("(BX)\n"); - kc += 4; - }; - if (kc + 2 <= fsz) { - emitline("\tMOVW\t"); - emitoff(kc: i64); - emitline("(SI), AX\n"); - emitline("\tMOVW\tAX, "); - emitoff(kc: i64); - emitline("(BX)\n"); - kc += 2; - }; - if (kc + 1 <= fsz) { - emitline("\tMOVB\t"); - emitoff(kc: i64); - emitline("(SI), AX\n"); - emitline("\tMOVB\tAX, "); - emitoff(kc: i64); - emitline("(BX)\n"); - kc += 1; - }; + aggcopy(c, fsz); return; }; }; diff --git a/test/wcc/812_agg_assign_width.c b/test/wcc/812_agg_assign_width.c new file mode 100644 index 00000000..293f8cf3 --- /dev/null +++ b/test/wcc/812_agg_assign_width.c @@ -0,0 +1,498 @@ +/* + * 812_agg_assign_width — cstage and wwstage agree, byte-for-byte and + * at runtime, that an aggregate ASSIGN copies the WHOLE value (task + * #49, Family A of ken's silent-set triage, /tmp/ken_silent_triage.md). + * + * The bug: plain whole-aggregate reassignment `b = a` fell to the + * N_ASSIGN scalar tail in BOTH stages and copied ONE MOVQ — word 0 of + * a 16/24/48B struct (ken f49_min cs.s asm proof; fA_16b shows even a + * 16B {size,size} fails). Same class at three more positions: a + * struct-lit FIELD initialised from an ident source (`outer{.., r=r}`, + * ken f38b / x5f-h) word0-copied inside cg_structlit_fill; a deref + * place `*p = s` truncated to 8 bytes (#31-A); a module-let global + * `g = a` / `g = pt{...}` stored one word (scalar `MOVQ AX, g(SB)`). + * Tagged/match context was INCIDENTAL — the hole is tagged-independent + * and gate-blind (byte-identical wrongness), latent only because lib + * style is let-init (`let b = a` runs the #265/#268 full-width copy — + * the working sibling these fixes mirror). + * + * The fix (BOTH stages, converged byte-identical): ONE place-resolved + * mem-to-mem copy funnel — cg_aggcopy/aggcopy, extracted verbatim from + * the C1.25 assign-resolver tail — fed by aggarg_srcaddr (source addr → + * SI) and a LEAQ of the destination (BP slot or g(SB) symbol → BX), + * wired at: the N_ASSIGN ident-aggregate arm (local + global), the + * deref-place divert into the existing resolver aggregate arm, and the + * structlit-fill aggregate-field arm. Non-addressable aggregate rhs + * shapes die LOUD (rule 7) instead of silently truncating. + * + * MUTATION: every row below fails at master 7545bf7 (probed 2026-06-05, + * ken's matrix f49_min/f49b/f49c/fA_16b/f38b + impl-A probes pA_*: all + * non-zero exits, byte-id both stages — the class is gate-blind, so + * runtime readback is the only oracle). + * + * row | shape | want + * ---------------------+-------------------------------------+------ + * assign_16b | {size,size} b = a, x*10+y readback | 39 + * assign_24b_plain | {size,size,size} b = a, sum | 6 + * assign_48b | 6-field b = a, sum | 21 + * assign_odd_12b | {u32,u32,u32} 12B maxalign-4 (gA2) | 0 + * assign_match_bind | dst = s from a match binding over | 0 + * | (st|void), st has a tagged member | + * | (ken f49_min — the ORIGINAL #49 | + * | context, regression pin) | + * lit_field_from_ident | outer{tag, r = r} field fill (f38b) | 21 + * assign_str_field | {id, name:str} b = a, ladder | 0 + * assign_nested | {tag, inner{x,y}} b = a, sum | 6 + * arrelem_roundtrip | a[1] = b (kin, pre-wired #270-1b) | 15 + * | then cc = a[1] (the new read side) | + * assign_deref | *p = s, 24B struct (#31-A fold) w/ | 27 + * | guards either side of the pointee | + * assign_global | g = a then b = g (module-let dst | 36 + * | and src, both new arms) | + * assign_dot_source | b = o.i (N_DOT source via | 6 + * | aggarg_srcaddr) | + * assign_tuple | (size,size) u = t slot copy | 13 + * global_structlit | g = pt{...} (DST_GLOBAL fill wire) | 18 + * alias_named_assign | type row = st; b = a (ken R1/gA3b — | 0 + * | full alias chase, field-wise init) | + * alias_twolevel_assign| ali -> base -> struct chain (gA6) | 0 + * neighbor_guard | b = a with live guards either side | 0 + * | of b — over-copy smashes them | + */ +#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; }; + +static const struct row rows[] = { + { "assign_16b", + "package main;\n" + "type p2 = struct { x: size, y: size };\n" + "export fn main() i32 = {\n" + "\tlet a: p2 = p2 { x = 3: size, y = 9: size };\n" + "\tlet b: p2 = p2 { x = 0: size, y = 0: size };\n" + "\tb = a;\n" + "\treturn (b.x * 10 + b.y): i32;\n" + "};\n", + 39 }, + + { "assign_24b_plain", + "package main;\n" + "type st = struct { id: size, x: size, y: size };\n" + "export fn main() i32 = {\n" + "\tlet a: st = st { id = 1: size, x = 2: size, y = 3: size };\n" + "\tlet b: st = st { id = 0: size, x = 0: size, y = 0: size };\n" + "\tb = a;\n" + "\treturn (b.id + b.x + b.y): i32;\n" + "};\n", + 6 }, + + { "assign_48b", + "package main;\n" + "type big = struct { a: size, b: size, c: size, d: size, e: size, f: size };\n" + "export fn main() i32 = {\n" + "\tlet x: big = big { a = 1: size, b = 2: size, c = 3: size, d = 4: size, e = 5: size, f = 6: size };\n" + "\tlet y: big = big { a = 0: size, b = 0: size, c = 0: size, d = 0: size, e = 0: size, f = 0: size };\n" + "\ty = x;\n" + "\treturn (y.a + y.b + y.c + y.d + y.e + y.f): i32;\n" + "};\n", + 21 }, + + /* ken gA2 — odd size: 12B {u32,u32,u32}, maxalign 4. Pins the + * MOVQ-run + MOVL tail of the funnel (the fi.fsz slot-padded + * skew worry) — NOT a multiple of 8. Pre-fix at master: word0 + * copies a+b in one MOVQ, c lost → exit 3 (probed both stages). */ + { "assign_odd_12b", + "package main;\n" + "type tri = struct { a: u32, b: u32, c: u32 };\n" + "export fn main() i32 = {\n" + "\tlet x: tri = tri { a = 7: u32, b = 8: u32, c = 9: u32 };\n" + "\tlet y: tri = tri { a = 0: u32, b = 0: u32, c = 0: u32 };\n" + "\ty = x;\n" + "\tif (y.a != 7) { return 1; };\n" + "\tif (y.b != 8) { return 2; };\n" + "\tif (y.c != 9) { return 3; };\n" + "\treturn 0;\n" + "};\n", + 0 }, + + /* ken f49_min verbatim — the ORIGINAL #49 filing context: + * assign from a match binding, struct with a tagged member. + * Pre-fix exit 4 (dst.m's tag word never copied). */ + { "assign_match_bind", + "package main;\n" + "type st = struct { id: size, m: (void | size) };\n" + "type un = (st | void);\n" + "export fn main() i32 = {\n" + "\tlet src: un = st { id = 6: size, m = 8: size };\n" + "\tlet dst: st = st { id = 0: size, m = void };\n" + "\tmatch (src) {\n" + "\tcase let s: st => {\n" + "\t\tif (!(s.m is size)) { return 1; };\n" + "\t\tdst = s;\n" + "\t};\n" + "\tcase void => { return 2; };\n" + "\t};\n" + "\tif (dst.id != 6) { return 3; };\n" + "\tif (!(dst.m is size)) { return 4; };\n" + "\tif (dst.m as size != 8) { return 5; };\n" + "\treturn 0;\n" + "};\n", + 0 }, + + /* ken f38b shape — struct-lit FIELD from an ident source. Pre-fix + * exit: o.r.id copied (word0), x/y zero → 6. */ + { "lit_field_from_ident", + "package main;\n" + "type rep = struct { id: size, x: size, y: size };\n" + "type outer = struct { tag: size, r: rep };\n" + "export fn main() i32 = {\n" + "\tlet r: rep = rep { id = 6: size, x = 7: size, y = 8: size };\n" + "\tlet o: outer = outer { tag = 4: size, r = r };\n" + "\tif (o.tag != 4) { return 99; };\n" + "\treturn (o.r.id + o.r.x + o.r.y): i32;\n" + "};\n", + 21 }, + + /* 3-word str header inside the copied struct: the header words + * past .ptr are exactly what a word0 copy drops. Ladder form — + * `.len` in mixed arithmetic trips the PRE-EXISTING task-#26 + * typing divergence (cstage rejects), which is not this class. */ + { "assign_str_field", + "package main;\n" + "type named = struct { id: size, name: str };\n" + "export fn main() i32 = {\n" + "\tlet a: named = named { id = 7: size, name = \"hello\" };\n" + "\tlet b: named = named { id = 0: size, name = \"\" };\n" + "\tb = a;\n" + "\tif (b.id != 7) { return 1; };\n" + "\tif (b.name.len != 5) { return 2; };\n" + "\treturn 0;\n" + "};\n", + 0 }, + + { "assign_nested", + "package main;\n" + "type inner = struct { x: size, y: size };\n" + "type outer = struct { tag: size, i: inner };\n" + "export fn main() i32 = {\n" + "\tlet a: outer = outer { tag = 1: size, i = inner { x = 2: size, y = 3: size } };\n" + "\tlet b: outer = outer { tag = 0: size, i = inner { x = 0: size, y = 0: size } };\n" + "\tb = a;\n" + "\treturn (b.tag + b.i.x + b.i.y): i32;\n" + "};\n", + 6 }, + + /* a[i] = b was already wired (#270-1b INDEX-place word-copy) — + * this row pins the KIN pair: the pre-existing store plus the + * NEW ident-from-index read (`cc = a[1]` word0-copied pre-fix). */ + { "arrelem_roundtrip", + "package main;\n" + "type pt = struct { x: size, y: size, z: size };\n" + "export fn main() i32 = {\n" + "\tlet a: [2]pt = [pt { x = 0: size, y = 0: size, z = 0: size }, pt { x = 0: size, y = 0: size, z = 0: size }];\n" + "\tlet b: pt = pt { x = 4: size, y = 5: size, z = 6: size };\n" + "\ta[1] = b;\n" + "\tif (a[0].x != 0) { return 99; };\n" + "\tlet cc: pt = pt { x = 0: size, y = 0: size, z = 0: size };\n" + "\tcc = a[1];\n" + "\treturn (cc.x + cc.y + cc.z): i32;\n" + "};\n", + 15 }, + + /* #31-A fold: deref place from an addressable aggregate ident — + * pre-fix stored 8 bytes (probe pA_deref exit 2). Guards either + * side of the pointee slot per ken gA4: an over-wide copy through + * the resolved place smashes one. */ + { "assign_deref", + "package main;\n" + "type pt = struct { x: size, y: size, z: size };\n" + "export fn main() i32 = {\n" + "\tlet guard1: size = 111: size;\n" + "\tlet d: pt = pt { x = 0: size, y = 0: size, z = 0: size };\n" + "\tlet guard2: size = 222: size;\n" + "\tlet s: pt = pt { x = 8: size, y = 9: size, z = 10: size };\n" + "\tlet p: *pt = &d;\n" + "\t*p = s;\n" + "\tif (guard1 != 111) { return 98; };\n" + "\tif (guard2 != 222) { return 97; };\n" + "\treturn (d.x + d.y + d.z): i32;\n" + "};\n", + 27 }, + + /* Module-let global as BOTH destination (`g = a`, LEAQ g(SB) + * dest) and source (`b = g`, aggarg_srcaddr global arm). */ + { "assign_global", + "package main;\n" + "type pt = struct { x: size, y: size, z: size };\n" + "let g: pt = pt { x = 0: size, y = 0: size, z = 0: size };\n" + "export fn main() i32 = {\n" + "\tlet a: pt = pt { x = 11: size, y = 12: size, z = 13: size };\n" + "\tg = a;\n" + "\tif (g.z != 13) { return 99; };\n" + "\tlet b: pt = pt { x = 0: size, y = 0: size, z = 0: size };\n" + "\tb = g;\n" + "\treturn (b.x + b.y + b.z): i32;\n" + "};\n", + 36 }, + + { "assign_dot_source", + "package main;\n" + "type inner = struct { x: size, y: size, z: size };\n" + "type outer = struct { tag: size, i: inner };\n" + "export fn main() i32 = {\n" + "\tlet o: outer = outer { tag = 1: size, i = inner { x = 2: size, y = 3: size, z = 4: size } };\n" + "\tlet b: inner = inner { x = 0: size, y = 0: size, z = 0: size };\n" + "\tb = o.i;\n" + "\treturn (b.x + b.z): i32;\n" + "};\n", + 6 }, + + /* Tuple ident reassign rides the same funnel (8B/elem slots). */ + { "assign_tuple", + "package main;\n" + "export fn main() i32 = {\n" + "\tlet t: (size, size) = (4: size, 9: size);\n" + "\tlet u: (size, size) = (0: size, 0: size);\n" + "\tu = t;\n" + "\treturn (u.0 + u.1): i32;\n" + "};\n", + 13 }, + + /* Global structlit reassign — newly wired through the DST_GLOBAL + * fill (pre-fix: one scalar MOVQ of AX≈0 to g(SB)). */ + { "global_structlit", + "package main;\n" + "type pt = struct { x: size, y: size, z: size };\n" + "let g: pt = pt { x = 0: size, y = 0: size, z = 0: size };\n" + "export fn main() i32 = {\n" + "\tg = pt { x = 5: size, y = 6: size, z = 7: size };\n" + "\treturn (g.x + g.y + g.z): i32;\n" + "};\n", + 18 }, + + /* ken R1 (gA3b): ALIAS-named aggregate — the funnel dispatch must + * FULL-chase the alias (type_chase_named / chased tinfo); a + * single peel left cstage at TY_NAMED, fell to the scalar tail + * and word0-copied while wwstage copied full-width (live cs≠ww). + * FIELD-WISE init: the struct-LIT spelling can't pin the ww side + * (it louds earlier at the pre-existing task-#7 aggregate-let + * bound — the #5 alias-arc's hole, not this funnel's). */ + { "alias_named_assign", + "package main;\n" + "type st = struct { a: size, b: size };\n" + "type row = st;\n" + "export fn main() i32 = {\n" + "\tlet a: row;\n" + "\ta.a = 4; a.b = 9;\n" + "\tlet b: row;\n" + "\tb.a = 0; b.b = 0;\n" + "\tb = a;\n" + "\tif (b.a != 4) { return 1; };\n" + "\tif (b.b != 9) { return 2; };\n" + "\treturn 0;\n" + "};\n", + 0 }, + + /* ken gA6: TWO-level alias chain (`ali -> base -> struct`) — pins + * that the dispatch is a full CHASE, not a deeper fixed peel. */ + { "alias_twolevel_assign", + "package main;\n" + "type ali = base;\n" + "type base = struct { a: size, b: size, c: size };\n" + "export fn main() i32 = {\n" + "\tlet a: ali;\n" + "\ta.a = 4; a.b = 9; a.c = 13;\n" + "\tlet b: ali;\n" + "\tb.a = 0; b.b = 0; b.c = 0;\n" + "\tb = a;\n" + "\tif (b.a != 4) { return 1; };\n" + "\tif (b.b != 9) { return 2; };\n" + "\tif (b.c != 13) { return 3; };\n" + "\treturn 0;\n" + "};\n", + 0 }, + + /* Neighbor guards on both sides of the destination slot: a copy + * WIDER than size(T) (the inverse regression) smashes one. */ + { "neighbor_guard", + "package main;\n" + "type pt = struct { x: size, y: size, z: size };\n" + "export fn main() i32 = {\n" + "\tlet guard1: size = 111: size;\n" + "\tlet b: pt = pt { x = 0: size, y = 0: size, z = 0: size };\n" + "\tlet guard2: size = 222: size;\n" + "\tlet a: pt = pt { x = 1: size, y = 2: size, z = 3: size };\n" + "\tb = a;\n" + "\tif (guard1 != 111) { return 1; };\n" + "\tif (guard2 != 222) { return 2; };\n" + "\tif (b.x + b.y + b.z != 6) { return 3; };\n" + "\treturn 0;\n" + "};\n", + 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/aggas_%d_%d.ww", getpid(), i); + snprintf(tmpdir, sizeof tmpdir, "/tmp/aggas_%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) { + 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 cstage's w6c and wwstage's + * w6c_ww and diff. The class is gate-blind (both stages were wrong + * identically), so byte-id here pins that the CONVERGED fix stays + * converged. */ +static int +asm_byte_identical(const char *bin, const struct row *r, int i) +{ + char src[64], cs[64], ws[64], cmd[1024]; + snprintf(src, sizeof src, "/tmp/aggas_asm_%d_%d.ww", getpid(), i); + snprintf(cs, sizeof cs, "/tmp/aggas_asm_%d_%d_c.s", getpid(), i); + snprintf(ws, sizeof ws, "/tmp/aggas_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; + } + + 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); + unlink(src); unlink(cs); unlink(ws); + 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, "agg_assign_width: 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, + "agg_assign_width[%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++) { + total++; + if (asm_byte_identical(bin, &rows[i], i) != 0) + fail++; + } + } + + if (fail) { + fprintf(stderr, + "agg_assign_width: %d/%d fixtures failed\n", fail, total); + return 1; + } + printf("agg_assign_width: %d/%d ok\n", total, total); + return 0; +} diff --git a/test/wcc/941_tuple_slot_layout_run.c b/test/wcc/941_tuple_slot_layout_run.c index fdf63e06..406458bb 100644 --- a/test/wcc/941_tuple_slot_layout_run.c +++ b/test/wcc/941_tuple_slot_layout_run.c @@ -1389,7 +1389,14 @@ static const struct row rows[] = { K_BUILDERR, "unsupported field-read shape" }, /* rule-7: a tuple arg from a source whose cgexpr does NOT fill * the cursor (here a struct-field chain) dies loud — pre-C-t2 - * it fell to the scalar single-PUSHQ default silently. */ + * it fell to the scalar single-PUSHQ default silently. + * #49 note: the tuple field fills from an IDENT source + * (`t = tt`, the #49 fill-field copy) so the build reaches the + * pinned ARG reject. The previous tuple-LITERAL fill + * (`t = (1, 2)`) now louds EARLIER at the #49 fill arm + * ("aggregate field from a non-addressable source unwired") — + * pre-#49 it silently word0-filled and only the arg push + * loudened. */ { "t2_reject_chain_arg", "package main;\n" "type holder = struct { t: (i64, i64) };\n" @@ -1397,7 +1404,8 @@ static const struct row rows[] = { " return t.0 + t.1;\n" "};\n" "export fn main() i32 = {\n" - " let h: holder = holder{ t = (1, 2) };\n" + " let tt: (i64, i64) = (1, 2);\n" + " let h: holder = holder{ t = tt };\n" " if (sum(h.t) != 3) { return 1; };\n" " return 0;\n" "};\n", 0,