From d642017643c2c285b1eae8550c18153470215f7c Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Thu, 4 Jun 2026 12:22:45 +0900 Subject: [PATCH] wcc+w6c_ww: aggregate let/range element copies via cgplaceaddr (F5) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit F5 (task #7): the N_LET aggregate-copy arm's source-addr enumeration (cgen.c #265/#268) had TY_ARRAY-ident/N_DOT/N_INDEX bases but no TY_SLICE base and no deref-spine shapes, so `let e: th = xs[0]` fell out with havesrc=0 — cstage emitted NOTHING (slot uninitialised), wwstage fell to its scalar default (8B truncation): gate-blind cs≠ww (p6min13). Every remaining ADDRESSABLE rhs now resolves through cgplaceaddr (the C1 resolver; enumerated arms dispatch first, their asm untouched), and the arm closes by construction with a loud tail — nothing below it can initialise a >8B struct/array slot, so any unhandled rhs shape dies loud instead of silently. A pre-tail #38b guard keeps the established `?`/`!`-on-sret loud-stop marker in wwstage (mirror of cstage's pre-arm fatal; pre-fix that shape reached the cgtryunw/cgtryprop gates which the tail now pre-empts in let position). Reviewer-C2 inheritance: `let c: capture = (*ts)[i].cap` (aggregate leaf behind a deref spine) — wwstage's documented cgdot aggregate-leaf loud is retired for let position (cglet routes the copy through the resolver before cgexpr sees the leaf; the loud stays as the guard for non-let expr positions), and cstage's silent no-copy on the same shape is fixed by the same resolver fallback. By-value RANGE payloads ride the same class: N_FORRANGE's single-bind load truncated every aggregate element to one fldloadop word. Both stages now word-copy the full element extent (MOVQ run + sized MOVL/MOVW/MOVB tail, the #270-1b idiom) for esz > 8. wwstage esz is re-keyed elemsizeof→elemsizeofc (the 8-sentinel hid struct elements from the copy gate — the #8 named-narrow precedent), with a stamped-slc.type_ fallback + element-tnode synthesis for non-ident scrutinees (tinfo SSoT, #209/#211). The wwstage checker now binds the ELEMENT type on single-bind ranges via a synthetic N_LET binder node (mirror of cstage check.c N_FORRANGE scope_define(..., elem, ...)); pre-fix the binding's decl was the N_FORRANGE node itself, so any field read off a by-value binding asserttyped-bailed. The checker half folds in under rule 11 because the split is unsound in either order: cgen-first is untestable (every field read off the binding still bails), checker-first converts that loud bail into the 8B SILENT truncation — only the pair closes the class. FC0 graduates: regex.finish's by-value range over 24B charset elems (non-ident scrutinee re.charsets) was the lib/regex byte-cmp's ONLY hunk since fold-1 — cstage 8-of-24-byte copy + IMULQ $24 vs wwstage 1-byte MOVZBQ, runtime-masked by the no-op loop body. The byte-cmp is now ZERO hunks (regex_test.combined.ww, w6c vs w6c_ww). #36 disposition: NOT folded. p6min9/p6min10's remaining failure is the struct-ident field rhs inside a struct LITERAL (cg_structlit_fill under-copy) — a different choke-point from the let-copy source-addr machinery; they still exit 4 here and stay blocked on #36 (read half landed in C2). Residual filed as task #43: an UNANNOTATED aggregate let (`let e = xs[0]`) still skips the wwstage arm (aggn/letslotsize are annotation-keyed; cstage keys the stamped n->type and now full-copies) — cs≠ww on that shape remains, #38-family. A landmine comment in test 805 marks the gap. test 805: +6 rows — let-from-slice-elem 16B (p6min13 verbatim) / 24B/40B/12B(MOVQ+MOVL tail) matrix / deref-spine leaf / by-value range ([]struct both-fields sum, []capture 40B, []str 24B header) / range edges (empty slice, by-VALUE binder-mutation pin, 12B elem MOVL tail) / reject row pinning the loud-tail text on both stages. All six fail at the pristine parent 403625e (re-verified post-rebase; 121 prior fixtures stay green there). --- cmd/w6c/cgen.c | 63 ++++++++- selfhost/cmd/w6c/main.combined.ww | 176 +++++++++++++++++++++--- selfhost/cmd/wcc/cgenexpr.ww | 10 +- selfhost/cmd/wcc/cgenstmt.ww | 138 +++++++++++++++++-- selfhost/cmd/wcc/check.ww | 28 +++- selfhost/cmd/wwdump/main.combined.ww | 176 +++++++++++++++++++++--- test/wcc/805_placeaddr_store.c | 192 +++++++++++++++++++++++++++ 7 files changed, 721 insertions(+), 62 deletions(-) diff --git a/cmd/w6c/cgen.c b/cmd/w6c/cgen.c index 62a85eaa..47977fb7 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -10472,6 +10472,18 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame) } } } + /* C4 (F5, task #7): the remaining ADDRESSABLE rhs + * shapes — a slice-base element (`= xs[0]`; the arms + * above have TY_ARRAY/N_DOT/N_INDEX bases but no + * TY_SLICE base) and deref-spine leaves + * (`= (*ts)[i].cap`) — resolve through cgplaceaddr + * (the C1 resolver; enumerated arms dispatch first so + * their asm is untouched). Pre-C4 these fell out with + * havesrc=0: cstage emitted NOTHING (slot + * uninitialised), wwstage's scalar default truncated + * to 8B — gate-blind cs≠ww. */ + if (!havesrc && cgplaceaddr(c, n->rhs, D_SI, *locals)) + havesrc = 1; if (havesrc) { int k = 0; for (; k + 8 <= sz; k += 8) { @@ -10503,6 +10515,11 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame) } break; } + /* C4: nothing below this arm can initialise a >8B + * struct/array slot — every fall-through was a silent + * miscompile (rule 7). */ + fatal("let: aggregate init from unhandled rhs shape " + "(task #7/rule-7)"); } if (n->rhs && sz == 8) { cgexpr(c, n->rhs, *locals); @@ -11513,11 +11530,47 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame) ins2(c, A_MOVQ, amem(D_BP, boff), areg(D_BX)); } ins2(c, A_ADDQ, areg(D_AX), areg(D_BX)); - /* load each binding from BX + foff into its slot */ - for (int b = 0; b < nbinds; b++) { - int op = fldloadop(binds[b].ftype, binds[b].sz); - ins2(c, op, amem(D_BX, binds[b].foff), areg(D_AX)); - ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, binds[b].off)); + /* load each binding from BX + foff into its slot. C4 (F5/FC0, + * task #7): a by-value AGGREGATE element (struct / tuple / + * str/slice header, esz > 8) copies its FULL extent — the + * single fldloadop word truncated it to 8B, so every field + * past word 0 (str/slice .len/.cap included) read stale slot + * bytes (regex.finish's 24B charset binding, gate-blind + * cs≠ww). Same word-run + sized-tail idiom as the N_LET + * aggregate copy. */ + if (!destruct && esz > 8) { + int k = 0; + for (; k + 8 <= esz; k += 8) { + ins2(c, A_MOVQ, amem(D_BX, k), areg(D_AX)); + ins2(c, A_MOVQ, areg(D_AX), + amem(D_BP, binds[0].off + k)); + } + if (k + 4 <= esz) { + ins2(c, A_MOVL, amem(D_BX, k), areg(D_AX)); + ins2(c, A_MOVL, areg(D_AX), + amem(D_BP, binds[0].off + k)); + k += 4; + } + if (k + 2 <= esz) { + ins2(c, A_MOVW, amem(D_BX, k), areg(D_AX)); + ins2(c, A_MOVW, areg(D_AX), + amem(D_BP, binds[0].off + k)); + k += 2; + } + if (k + 1 <= esz) { + ins2(c, A_MOVB, amem(D_BX, k), areg(D_AX)); + ins2(c, A_MOVB, areg(D_AX), + amem(D_BP, binds[0].off + k)); + k += 1; + } + } else { + for (int b = 0; b < nbinds; b++) { + int op = fldloadop(binds[b].ftype, binds[b].sz); + ins2(c, op, amem(D_BX, binds[b].foff), + areg(D_AX)); + ins2(c, A_MOVQ, areg(D_AX), + amem(D_BP, binds[b].off)); + } } cgstmt(c, n->body, locals, frame); label(c, rpost); diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 1c55d8e7..d30252e2 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -10740,7 +10740,33 @@ fn resolvewalk(c: *checker, n: *node) void = { let bnm: str = n.str; if (bnm.len > 0) { checkmoduleshadow(c, bnm, "binding"); - scopedefine(c.cur, bnm, skind.SK_VAR, nil, n); + // C4 (task #7): bind the ELEMENT type so field + // reads off a by-value aggregate binding + // (`for (let t .. threads) { t.pc }`) resolve — + // pre-C4 the binding's decl was the N_FORRANGE + // node itself, whose .lhs is the SCRUTINEE expr, + // so exprtype's decl.lhs read handed the dot a + // non-type node and asserttyped bailed (cstage + // types it: check.c N_FORRANGE scope_define(..., + // elem, ...)). Synthetic N_LET binder whose .lhs + // is the element tnode — the stamptuplebinds + // `b.lhs = et` idiom. A str scrutinee keeps the + // old decl: cgen synthesises the u8 elem there + // and no dot applies to a u8 binding. + let et: *node = nil; + let it: *node = exprtype(c, n.lhs, nil); + if (it != nil) { + if (it.kind == nkind.N_TSLICE) { et = it.lhs; }; + if (it.kind == nkind.N_TARRAY) { et = it.lhs; }; + }; + if (et != nil) { + let bn: *node = newnode(nkind.N_LET, n.file, n.line, n.col); + bn.str = bnm; + bn.lhs = et; + scopedefine(c.cur, bnm, skind.SK_VAR, nil, bn); + } else { + scopedefine(c.cur, bnm, skind.SK_VAR, nil, n); + }; }; }; if (n.body != nil) { resolvewalk(c, n.body); }; @@ -23700,11 +23726,11 @@ fn cgdot(c: *cgen, n: *node) void = { os.write(2, mt.ptr, mt.len: u64); os.exit(1); }; - // ww-asymmetric in LET position: cstage's let-init - // consumes `let c = (*ts)[i].cap` BEFORE its N_DOT - // tail and emits NO copy (the F5 bug) — cs-builds/ - // ww-louds on that shape until the F5 let-copy - // lands (task #7). Absent from the gate corpus. + // LET-position aggregate leaves route through cglet's + // resolver copy (C4, task #7) before cgexpr ever sees + // them; this loud guards the remaining non-let expr + // positions (no register convention for a >8B leaf), + // symmetric with cstage's read-resolver tail. if (rdu.kind == tykind.TY_STRUCT || rdu.kind == tykind.TY_TUPLE) { let ma: str = "read-resolver: aggregate field read not wired (rule-7)\n"; @@ -32116,6 +32142,18 @@ fn cglet(c: *cgen, n: *node) void = { }; }; }; }; + // C4 (F5, task #7): the remaining ADDRESSABLE rhs + // shapes — a slice-base element (`= xs[0]`; the arms + // above have TY_ARRAY/N_DOT/N_INDEX bases but no + // TY_SLICE base) and deref-spine leaves + // (`= (*ts)[i].cap`) — resolve through cgplaceaddr + // (the C1 resolver; enumerated arms dispatch first so + // their asm is untouched). Pre-C4 these fell through + // to the scalar default's 8B truncation while cstage + // emitted NOTHING — gate-blind cs≠ww. + if (!havesrc) { + if (cgplaceaddr(c, rhs, "SI")) { havesrc = true; }; + }; if (havesrc) { let k: i32 = 0; for (k + 8 <= aggn) { @@ -32157,6 +32195,30 @@ fn cglet(c: *cgen, n: *node) void = { c.lastwasreturn = 0; return; }; + // #38b (rule 7): `?`/`!` over an sret-class call into + // an aggregate let — keep the established #38b/#40 + // loud-stop marker (mirror of cstage's pre-arm fatal, + // cgen.c N_LET; pre-C4 this shape fell through to the + // cgtryunw/cgtryprop gates, which the C4 tail below + // now pre-empts in let position). + if (rhs.kind == nkind.N_TRYUNW + || rhs.kind == nkind.N_TRYPROP) { + if (rhs.lhs != nil) { + if (rhs.lhs.kind == nkind.N_CALL) { + if (callsretsize(c, rhs.lhs) > 0) { + let m38f: str = "#38b: `?`/`!` on an sret-class call result unwired (mem-based unwrap is a #40-family follow-up)\n"; + os.write(2, m38f.ptr, m38f.len: u64); + os.exit(1); + }; + }; + }; + }; + // C4: nothing below this arm can initialise a >8B + // struct/array slot — the scalar default's 8B store + // was a silent truncation (rule 7). + let mf5: str = "let: aggregate init from unhandled rhs shape (task #7/rule-7)\n"; + os.write(2, mf5.ptr, mf5.len: u64); + os.exit(1); }; cgexpr(c, rhs); // Float local: cgexpr leaves the value in X0. Spill via @@ -32807,7 +32869,12 @@ fn cgforrange(c: *cgen, n: *node) void = { // no slot-padding) so e.g. `(i64, i64)` is 16, `(i32, i32)` is 8. // elemsizeof returns 8 for non-primitive elem, which would be // wrong here — compute from the tuple param walk instead. - let esz: i32 = elemsizeof(slctn); + // C4 (task #7): elemsizeofc, not elemsizeof — a struct element + // (`[]thread`, 16B) hit elemsizeof's 8-sentinel while cstage reads + // the stamped slc->type sub size (IMULQ $8 vs $16, gate-blind + // cs≠ww). elemsizeofc recovers the width from the stamped tinfo + // (the #8 named-narrow precedent). + let esz: i32 = elemsizeofc(c, slctn); if (elemt != nil) { if (elemt.kind == nkind.N_TTUPLE) { let total: i32 = 0; @@ -32819,6 +32886,31 @@ fn cgforrange(c: *cgen, n: *node) void = { esz = total; }; }; + // C4 (FC0, task #7): a non-ident scrutinee (`re.charsets`) has no + // local tnode — slctn is nil, so esz fell to 1 and the binding + // registered typeless (cstage reads the stamped slc->type: esz 24, + // slice-header readbacks → cs≠ww). Derive both from the checker- + // stamped slc.type_ (tinfo SSoT, the #209/#211 discipline); the + // synthesised N_TNAME carries the element tinfo so cgident's + // str/slice/float keys read it like a declared local (the str→u8 + // synthesis precedent above). + if (slctn == nil && slc != nil) { + let sti2: *tinfo = slc.type_: *tinfo; + for (sti2 != nil && sti2.kind == tykind.TY_NAMED) { sti2 = sti2.under; }; + if (sti2 != nil) { + if (sti2.kind == tykind.TY_SLICE + || sti2.kind == tykind.TY_STR + || sti2.kind == tykind.TY_ARRAY) { + if (sti2.sub != nil) { + esz = sti2.sub.size: i32; + let en: *node = newnode(nkind.N_TNAME, slc.file, slc.line, slc.col); + en.str = sti2.sub.name; + en.type_ = sti2.sub: *void; + elemt = en; + }; + }; + }; + }; let destruct: bool = (n.list != nil); // .rgi (counter) + .rgl (length) scratch slots. @@ -32987,18 +33079,64 @@ fn cgforrange(c: *cgen, n: *node) void = { // Per-binding load from BX+foff. Signedness comes from bind_signed // (set via paramissigned → fieldissignedc), so enum-aliased narrows // pick the right MOVS*Q without a literal-name gate. - let b: i32 = 0; - for (b < nbinds) { - let op: str = loadopsz(bind_signed[b], bind_sz[b]); - emitline("\t"); - emitline(op); - emitline("\t"); - emitoff(bind_foff[b]: i64); - emitline("(BX), AX\n"); - emitline("\tMOVQ\tAX, "); - emitoff(bind_off[b]: i64); - emitline("(BP)\n"); - b += 1; + // C4 (F5/FC0, task #7): a by-value AGGREGATE element (struct / + // tuple / str/slice header, esz > 8) copies its FULL extent — the + // single load word truncated it to 8B, so every field past word 0 + // (str/slice .len/.cap included) read stale slot bytes + // (regex.finish's 24B charset binding, gate-blind cs≠ww). Same + // word-run + sized-tail idiom as the cglet aggregate copy. + if (!destruct && esz > 8) { + let k: i32 = 0; + for (k + 8 <= esz) { + emitline("\tMOVQ\t"); + emitoff(k: i64); + emitline("(BX), AX\n"); + emitline("\tMOVQ\tAX, "); + emitoff((bind_off[0] + k): i64); + emitline("(BP)\n"); + k += 8; + }; + if (k + 4 <= esz) { + emitline("\tMOVL\t"); + emitoff(k: i64); + emitline("(BX), AX\n"); + emitline("\tMOVL\tAX, "); + emitoff((bind_off[0] + k): i64); + emitline("(BP)\n"); + k += 4; + }; + if (k + 2 <= esz) { + emitline("\tMOVW\t"); + emitoff(k: i64); + emitline("(BX), AX\n"); + emitline("\tMOVW\tAX, "); + emitoff((bind_off[0] + k): i64); + emitline("(BP)\n"); + k += 2; + }; + if (k + 1 <= esz) { + emitline("\tMOVB\t"); + emitoff(k: i64); + emitline("(BX), AX\n"); + emitline("\tMOVB\tAX, "); + emitoff((bind_off[0] + k): i64); + emitline("(BP)\n"); + k += 1; + }; + } else { + let b: i32 = 0; + for (b < nbinds) { + let op: str = loadopsz(bind_signed[b], bind_sz[b]); + emitline("\t"); + emitline(op); + emitline("\t"); + emitoff(bind_foff[b]: i64); + emitline("(BX), AX\n"); + emitline("\tMOVQ\tAX, "); + emitoff(bind_off[b]: i64); + emitline("(BP)\n"); + b += 1; + }; }; if (n.body != nil) { cgstmt(c, n.body); }; diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index 7f9e12c1..d98e177d 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -3606,11 +3606,11 @@ fn cgdot(c: *cgen, n: *node) void = { os.write(2, mt.ptr, mt.len: u64); os.exit(1); }; - // ww-asymmetric in LET position: cstage's let-init - // consumes `let c = (*ts)[i].cap` BEFORE its N_DOT - // tail and emits NO copy (the F5 bug) — cs-builds/ - // ww-louds on that shape until the F5 let-copy - // lands (task #7). Absent from the gate corpus. + // LET-position aggregate leaves route through cglet's + // resolver copy (C4, task #7) before cgexpr ever sees + // them; this loud guards the remaining non-let expr + // positions (no register convention for a >8B leaf), + // symmetric with cstage's read-resolver tail. if (rdu.kind == tykind.TY_STRUCT || rdu.kind == tykind.TY_TUPLE) { let ma: str = "read-resolver: aggregate field read not wired (rule-7)\n"; diff --git a/selfhost/cmd/wcc/cgenstmt.ww b/selfhost/cmd/wcc/cgenstmt.ww index b1d98078..b411b52a 100644 --- a/selfhost/cmd/wcc/cgenstmt.ww +++ b/selfhost/cmd/wcc/cgenstmt.ww @@ -2543,6 +2543,18 @@ fn cglet(c: *cgen, n: *node) void = { }; }; }; }; + // C4 (F5, task #7): the remaining ADDRESSABLE rhs + // shapes — a slice-base element (`= xs[0]`; the arms + // above have TY_ARRAY/N_DOT/N_INDEX bases but no + // TY_SLICE base) and deref-spine leaves + // (`= (*ts)[i].cap`) — resolve through cgplaceaddr + // (the C1 resolver; enumerated arms dispatch first so + // their asm is untouched). Pre-C4 these fell through + // to the scalar default's 8B truncation while cstage + // emitted NOTHING — gate-blind cs≠ww. + if (!havesrc) { + if (cgplaceaddr(c, rhs, "SI")) { havesrc = true; }; + }; if (havesrc) { let k: i32 = 0; for (k + 8 <= aggn) { @@ -2584,6 +2596,30 @@ fn cglet(c: *cgen, n: *node) void = { c.lastwasreturn = 0; return; }; + // #38b (rule 7): `?`/`!` over an sret-class call into + // an aggregate let — keep the established #38b/#40 + // loud-stop marker (mirror of cstage's pre-arm fatal, + // cgen.c N_LET; pre-C4 this shape fell through to the + // cgtryunw/cgtryprop gates, which the C4 tail below + // now pre-empts in let position). + if (rhs.kind == nkind.N_TRYUNW + || rhs.kind == nkind.N_TRYPROP) { + if (rhs.lhs != nil) { + if (rhs.lhs.kind == nkind.N_CALL) { + if (callsretsize(c, rhs.lhs) > 0) { + let m38f: str = "#38b: `?`/`!` on an sret-class call result unwired (mem-based unwrap is a #40-family follow-up)\n"; + os.write(2, m38f.ptr, m38f.len: u64); + os.exit(1); + }; + }; + }; + }; + // C4: nothing below this arm can initialise a >8B + // struct/array slot — the scalar default's 8B store + // was a silent truncation (rule 7). + let mf5: str = "let: aggregate init from unhandled rhs shape (task #7/rule-7)\n"; + os.write(2, mf5.ptr, mf5.len: u64); + os.exit(1); }; cgexpr(c, rhs); // Float local: cgexpr leaves the value in X0. Spill via @@ -3234,7 +3270,12 @@ fn cgforrange(c: *cgen, n: *node) void = { // no slot-padding) so e.g. `(i64, i64)` is 16, `(i32, i32)` is 8. // elemsizeof returns 8 for non-primitive elem, which would be // wrong here — compute from the tuple param walk instead. - let esz: i32 = elemsizeof(slctn); + // C4 (task #7): elemsizeofc, not elemsizeof — a struct element + // (`[]thread`, 16B) hit elemsizeof's 8-sentinel while cstage reads + // the stamped slc->type sub size (IMULQ $8 vs $16, gate-blind + // cs≠ww). elemsizeofc recovers the width from the stamped tinfo + // (the #8 named-narrow precedent). + let esz: i32 = elemsizeofc(c, slctn); if (elemt != nil) { if (elemt.kind == nkind.N_TTUPLE) { let total: i32 = 0; @@ -3246,6 +3287,31 @@ fn cgforrange(c: *cgen, n: *node) void = { esz = total; }; }; + // C4 (FC0, task #7): a non-ident scrutinee (`re.charsets`) has no + // local tnode — slctn is nil, so esz fell to 1 and the binding + // registered typeless (cstage reads the stamped slc->type: esz 24, + // slice-header readbacks → cs≠ww). Derive both from the checker- + // stamped slc.type_ (tinfo SSoT, the #209/#211 discipline); the + // synthesised N_TNAME carries the element tinfo so cgident's + // str/slice/float keys read it like a declared local (the str→u8 + // synthesis precedent above). + if (slctn == nil && slc != nil) { + let sti2: *tinfo = slc.type_: *tinfo; + for (sti2 != nil && sti2.kind == tykind.TY_NAMED) { sti2 = sti2.under; }; + if (sti2 != nil) { + if (sti2.kind == tykind.TY_SLICE + || sti2.kind == tykind.TY_STR + || sti2.kind == tykind.TY_ARRAY) { + if (sti2.sub != nil) { + esz = sti2.sub.size: i32; + let en: *node = newnode(nkind.N_TNAME, slc.file, slc.line, slc.col); + en.str = sti2.sub.name; + en.type_ = sti2.sub: *void; + elemt = en; + }; + }; + }; + }; let destruct: bool = (n.list != nil); // .rgi (counter) + .rgl (length) scratch slots. @@ -3414,18 +3480,64 @@ fn cgforrange(c: *cgen, n: *node) void = { // Per-binding load from BX+foff. Signedness comes from bind_signed // (set via paramissigned → fieldissignedc), so enum-aliased narrows // pick the right MOVS*Q without a literal-name gate. - let b: i32 = 0; - for (b < nbinds) { - let op: str = loadopsz(bind_signed[b], bind_sz[b]); - emitline("\t"); - emitline(op); - emitline("\t"); - emitoff(bind_foff[b]: i64); - emitline("(BX), AX\n"); - emitline("\tMOVQ\tAX, "); - emitoff(bind_off[b]: i64); - emitline("(BP)\n"); - b += 1; + // C4 (F5/FC0, task #7): a by-value AGGREGATE element (struct / + // tuple / str/slice header, esz > 8) copies its FULL extent — the + // single load word truncated it to 8B, so every field past word 0 + // (str/slice .len/.cap included) read stale slot bytes + // (regex.finish's 24B charset binding, gate-blind cs≠ww). Same + // word-run + sized-tail idiom as the cglet aggregate copy. + if (!destruct && esz > 8) { + let k: i32 = 0; + for (k + 8 <= esz) { + emitline("\tMOVQ\t"); + emitoff(k: i64); + emitline("(BX), AX\n"); + emitline("\tMOVQ\tAX, "); + emitoff((bind_off[0] + k): i64); + emitline("(BP)\n"); + k += 8; + }; + if (k + 4 <= esz) { + emitline("\tMOVL\t"); + emitoff(k: i64); + emitline("(BX), AX\n"); + emitline("\tMOVL\tAX, "); + emitoff((bind_off[0] + k): i64); + emitline("(BP)\n"); + k += 4; + }; + if (k + 2 <= esz) { + emitline("\tMOVW\t"); + emitoff(k: i64); + emitline("(BX), AX\n"); + emitline("\tMOVW\tAX, "); + emitoff((bind_off[0] + k): i64); + emitline("(BP)\n"); + k += 2; + }; + if (k + 1 <= esz) { + emitline("\tMOVB\t"); + emitoff(k: i64); + emitline("(BX), AX\n"); + emitline("\tMOVB\tAX, "); + emitoff((bind_off[0] + k): i64); + emitline("(BP)\n"); + k += 1; + }; + } else { + let b: i32 = 0; + for (b < nbinds) { + let op: str = loadopsz(bind_signed[b], bind_sz[b]); + emitline("\t"); + emitline(op); + emitline("\t"); + emitoff(bind_foff[b]: i64); + emitline("(BX), AX\n"); + emitline("\tMOVQ\tAX, "); + emitoff(bind_off[b]: i64); + emitline("(BP)\n"); + b += 1; + }; }; if (n.body != nil) { cgstmt(c, n.body); }; diff --git a/selfhost/cmd/wcc/check.ww b/selfhost/cmd/wcc/check.ww index a5cc1568..8f111660 100644 --- a/selfhost/cmd/wcc/check.ww +++ b/selfhost/cmd/wcc/check.ww @@ -413,7 +413,33 @@ fn resolvewalk(c: *checker, n: *node) void = { let bnm: str = n.str; if (bnm.len > 0) { checkmoduleshadow(c, bnm, "binding"); - scopedefine(c.cur, bnm, skind.SK_VAR, nil, n); + // C4 (task #7): bind the ELEMENT type so field + // reads off a by-value aggregate binding + // (`for (let t .. threads) { t.pc }`) resolve — + // pre-C4 the binding's decl was the N_FORRANGE + // node itself, whose .lhs is the SCRUTINEE expr, + // so exprtype's decl.lhs read handed the dot a + // non-type node and asserttyped bailed (cstage + // types it: check.c N_FORRANGE scope_define(..., + // elem, ...)). Synthetic N_LET binder whose .lhs + // is the element tnode — the stamptuplebinds + // `b.lhs = et` idiom. A str scrutinee keeps the + // old decl: cgen synthesises the u8 elem there + // and no dot applies to a u8 binding. + let et: *node = nil; + let it: *node = exprtype(c, n.lhs, nil); + if (it != nil) { + if (it.kind == nkind.N_TSLICE) { et = it.lhs; }; + if (it.kind == nkind.N_TARRAY) { et = it.lhs; }; + }; + if (et != nil) { + let bn: *node = newnode(nkind.N_LET, n.file, n.line, n.col); + bn.str = bnm; + bn.lhs = et; + scopedefine(c.cur, bnm, skind.SK_VAR, nil, bn); + } else { + scopedefine(c.cur, bnm, skind.SK_VAR, nil, n); + }; }; }; if (n.body != nil) { resolvewalk(c, n.body); }; diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index b6fb8ca0..cf7cbd8d 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -10740,7 +10740,33 @@ fn resolvewalk(c: *checker, n: *node) void = { let bnm: str = n.str; if (bnm.len > 0) { checkmoduleshadow(c, bnm, "binding"); - scopedefine(c.cur, bnm, skind.SK_VAR, nil, n); + // C4 (task #7): bind the ELEMENT type so field + // reads off a by-value aggregate binding + // (`for (let t .. threads) { t.pc }`) resolve — + // pre-C4 the binding's decl was the N_FORRANGE + // node itself, whose .lhs is the SCRUTINEE expr, + // so exprtype's decl.lhs read handed the dot a + // non-type node and asserttyped bailed (cstage + // types it: check.c N_FORRANGE scope_define(..., + // elem, ...)). Synthetic N_LET binder whose .lhs + // is the element tnode — the stamptuplebinds + // `b.lhs = et` idiom. A str scrutinee keeps the + // old decl: cgen synthesises the u8 elem there + // and no dot applies to a u8 binding. + let et: *node = nil; + let it: *node = exprtype(c, n.lhs, nil); + if (it != nil) { + if (it.kind == nkind.N_TSLICE) { et = it.lhs; }; + if (it.kind == nkind.N_TARRAY) { et = it.lhs; }; + }; + if (et != nil) { + let bn: *node = newnode(nkind.N_LET, n.file, n.line, n.col); + bn.str = bnm; + bn.lhs = et; + scopedefine(c.cur, bnm, skind.SK_VAR, nil, bn); + } else { + scopedefine(c.cur, bnm, skind.SK_VAR, nil, n); + }; }; }; if (n.body != nil) { resolvewalk(c, n.body); }; @@ -23700,11 +23726,11 @@ fn cgdot(c: *cgen, n: *node) void = { os.write(2, mt.ptr, mt.len: u64); os.exit(1); }; - // ww-asymmetric in LET position: cstage's let-init - // consumes `let c = (*ts)[i].cap` BEFORE its N_DOT - // tail and emits NO copy (the F5 bug) — cs-builds/ - // ww-louds on that shape until the F5 let-copy - // lands (task #7). Absent from the gate corpus. + // LET-position aggregate leaves route through cglet's + // resolver copy (C4, task #7) before cgexpr ever sees + // them; this loud guards the remaining non-let expr + // positions (no register convention for a >8B leaf), + // symmetric with cstage's read-resolver tail. if (rdu.kind == tykind.TY_STRUCT || rdu.kind == tykind.TY_TUPLE) { let ma: str = "read-resolver: aggregate field read not wired (rule-7)\n"; @@ -32116,6 +32142,18 @@ fn cglet(c: *cgen, n: *node) void = { }; }; }; }; + // C4 (F5, task #7): the remaining ADDRESSABLE rhs + // shapes — a slice-base element (`= xs[0]`; the arms + // above have TY_ARRAY/N_DOT/N_INDEX bases but no + // TY_SLICE base) and deref-spine leaves + // (`= (*ts)[i].cap`) — resolve through cgplaceaddr + // (the C1 resolver; enumerated arms dispatch first so + // their asm is untouched). Pre-C4 these fell through + // to the scalar default's 8B truncation while cstage + // emitted NOTHING — gate-blind cs≠ww. + if (!havesrc) { + if (cgplaceaddr(c, rhs, "SI")) { havesrc = true; }; + }; if (havesrc) { let k: i32 = 0; for (k + 8 <= aggn) { @@ -32157,6 +32195,30 @@ fn cglet(c: *cgen, n: *node) void = { c.lastwasreturn = 0; return; }; + // #38b (rule 7): `?`/`!` over an sret-class call into + // an aggregate let — keep the established #38b/#40 + // loud-stop marker (mirror of cstage's pre-arm fatal, + // cgen.c N_LET; pre-C4 this shape fell through to the + // cgtryunw/cgtryprop gates, which the C4 tail below + // now pre-empts in let position). + if (rhs.kind == nkind.N_TRYUNW + || rhs.kind == nkind.N_TRYPROP) { + if (rhs.lhs != nil) { + if (rhs.lhs.kind == nkind.N_CALL) { + if (callsretsize(c, rhs.lhs) > 0) { + let m38f: str = "#38b: `?`/`!` on an sret-class call result unwired (mem-based unwrap is a #40-family follow-up)\n"; + os.write(2, m38f.ptr, m38f.len: u64); + os.exit(1); + }; + }; + }; + }; + // C4: nothing below this arm can initialise a >8B + // struct/array slot — the scalar default's 8B store + // was a silent truncation (rule 7). + let mf5: str = "let: aggregate init from unhandled rhs shape (task #7/rule-7)\n"; + os.write(2, mf5.ptr, mf5.len: u64); + os.exit(1); }; cgexpr(c, rhs); // Float local: cgexpr leaves the value in X0. Spill via @@ -32807,7 +32869,12 @@ fn cgforrange(c: *cgen, n: *node) void = { // no slot-padding) so e.g. `(i64, i64)` is 16, `(i32, i32)` is 8. // elemsizeof returns 8 for non-primitive elem, which would be // wrong here — compute from the tuple param walk instead. - let esz: i32 = elemsizeof(slctn); + // C4 (task #7): elemsizeofc, not elemsizeof — a struct element + // (`[]thread`, 16B) hit elemsizeof's 8-sentinel while cstage reads + // the stamped slc->type sub size (IMULQ $8 vs $16, gate-blind + // cs≠ww). elemsizeofc recovers the width from the stamped tinfo + // (the #8 named-narrow precedent). + let esz: i32 = elemsizeofc(c, slctn); if (elemt != nil) { if (elemt.kind == nkind.N_TTUPLE) { let total: i32 = 0; @@ -32819,6 +32886,31 @@ fn cgforrange(c: *cgen, n: *node) void = { esz = total; }; }; + // C4 (FC0, task #7): a non-ident scrutinee (`re.charsets`) has no + // local tnode — slctn is nil, so esz fell to 1 and the binding + // registered typeless (cstage reads the stamped slc->type: esz 24, + // slice-header readbacks → cs≠ww). Derive both from the checker- + // stamped slc.type_ (tinfo SSoT, the #209/#211 discipline); the + // synthesised N_TNAME carries the element tinfo so cgident's + // str/slice/float keys read it like a declared local (the str→u8 + // synthesis precedent above). + if (slctn == nil && slc != nil) { + let sti2: *tinfo = slc.type_: *tinfo; + for (sti2 != nil && sti2.kind == tykind.TY_NAMED) { sti2 = sti2.under; }; + if (sti2 != nil) { + if (sti2.kind == tykind.TY_SLICE + || sti2.kind == tykind.TY_STR + || sti2.kind == tykind.TY_ARRAY) { + if (sti2.sub != nil) { + esz = sti2.sub.size: i32; + let en: *node = newnode(nkind.N_TNAME, slc.file, slc.line, slc.col); + en.str = sti2.sub.name; + en.type_ = sti2.sub: *void; + elemt = en; + }; + }; + }; + }; let destruct: bool = (n.list != nil); // .rgi (counter) + .rgl (length) scratch slots. @@ -32987,18 +33079,64 @@ fn cgforrange(c: *cgen, n: *node) void = { // Per-binding load from BX+foff. Signedness comes from bind_signed // (set via paramissigned → fieldissignedc), so enum-aliased narrows // pick the right MOVS*Q without a literal-name gate. - let b: i32 = 0; - for (b < nbinds) { - let op: str = loadopsz(bind_signed[b], bind_sz[b]); - emitline("\t"); - emitline(op); - emitline("\t"); - emitoff(bind_foff[b]: i64); - emitline("(BX), AX\n"); - emitline("\tMOVQ\tAX, "); - emitoff(bind_off[b]: i64); - emitline("(BP)\n"); - b += 1; + // C4 (F5/FC0, task #7): a by-value AGGREGATE element (struct / + // tuple / str/slice header, esz > 8) copies its FULL extent — the + // single load word truncated it to 8B, so every field past word 0 + // (str/slice .len/.cap included) read stale slot bytes + // (regex.finish's 24B charset binding, gate-blind cs≠ww). Same + // word-run + sized-tail idiom as the cglet aggregate copy. + if (!destruct && esz > 8) { + let k: i32 = 0; + for (k + 8 <= esz) { + emitline("\tMOVQ\t"); + emitoff(k: i64); + emitline("(BX), AX\n"); + emitline("\tMOVQ\tAX, "); + emitoff((bind_off[0] + k): i64); + emitline("(BP)\n"); + k += 8; + }; + if (k + 4 <= esz) { + emitline("\tMOVL\t"); + emitoff(k: i64); + emitline("(BX), AX\n"); + emitline("\tMOVL\tAX, "); + emitoff((bind_off[0] + k): i64); + emitline("(BP)\n"); + k += 4; + }; + if (k + 2 <= esz) { + emitline("\tMOVW\t"); + emitoff(k: i64); + emitline("(BX), AX\n"); + emitline("\tMOVW\tAX, "); + emitoff((bind_off[0] + k): i64); + emitline("(BP)\n"); + k += 2; + }; + if (k + 1 <= esz) { + emitline("\tMOVB\t"); + emitoff(k: i64); + emitline("(BX), AX\n"); + emitline("\tMOVB\tAX, "); + emitoff((bind_off[0] + k): i64); + emitline("(BP)\n"); + k += 1; + }; + } else { + let b: i32 = 0; + for (b < nbinds) { + let op: str = loadopsz(bind_signed[b], bind_sz[b]); + emitline("\t"); + emitline(op); + emitline("\t"); + emitoff(bind_foff[b]: i64); + emitline("(BX), AX\n"); + emitline("\tMOVQ\tAX, "); + emitoff(bind_off[b]: i64); + emitline("(BP)\n"); + b += 1; + }; }; if (n.body != nil) { cgstmt(c, n.body); }; diff --git a/test/wcc/805_placeaddr_store.c b/test/wcc/805_placeaddr_store.c index e65e045b..8b9599ae 100644 --- a/test/wcc/805_placeaddr_store.c +++ b/test/wcc/805_placeaddr_store.c @@ -105,6 +105,25 @@ * | (*ts)[i] compound/store (p7 hot shape) | * c3_free_operand | FA6: free((*ts)[i].slicefield) reads | 69 * | through the operand (pA9) | + * c4_let_slice_elem | F5: let e = xs[0], 16B struct elem | 70 + * | (p6min13 graduated — cstage emitted | + * | NOTHING, wwstage 8B truncation) | + * c4_let_elem_sizes | F5: 24B/40B/12B(MOVQ+MOVL tail) elems | 71 + * c4_let_deref_leaf | reviewer-C2 inheritance: let c = | 72 + * | (*ts)[i].cap (40B leaf behind deref | + * | spine; ww loud retired, cs no-copy | + * | fixed) | + * c4_range_byvalue | F5/FC0: by-value range payload full- | 73 + * | width copy — []struct (sum BOTH | + * | fields), []capture 40B (str field | + * | header), []str (24B header .len) | + * c4_range_edges | range edges: empty slice (zero iters), | 74 + * | binder MUTATION (by-VALUE: writes must | + * | not reach the slice), 12B elem | + * | (MOVQ+MOVL tail in the range copy) | + * reject_let_unhandled| `let p: pair = mk()!` — non-place rhs | BUILD_FAIL + * | dies at C4's loud let tail (was | + * | SILENT: cs no init, ww 8B) | * * BUILD_FAIL rows also assert the diagnostic TEXT (stderr substring, * both stages) — a build that fails for any other reason (parse error, @@ -1114,6 +1133,179 @@ static const struct row rows[] = { "\treturn 69;\n" "};\n", 69, NULL }, + + /* C4 (F5, task #7): `let e = xs[0]` from a SLICE base — the #265 + * source-addr enumeration had TY_ARRAY/N_DOT/N_INDEX bases but no + * TY_SLICE base, so cstage emitted NOTHING (slot uninitialised) + * and wwstage fell to its scalar default (8B truncation) — + * gate-blind cs≠ww. p6min13 graduated verbatim; the slice base + * now resolves through cgplaceaddr. + * + * LANDMINE (task #43): the UNANNOTATED form `let e = xs[0]` is + * still cs≠ww — wwstage's aggn/letslotsize gates are annotation- + * keyed and skip the copy arm (8B truncation) while cstage keys + * the stamped n->type and full-copies. No dual-driver row can pin + * a divergent shape; every row below annotates. Do NOT read the + * annotated rows as covering the inferred form. */ + { "c4_let_slice_elem", + "package main;\n" + "type th = struct { pc: size, matched: bool };\n" + "export fn main() i32 = {\n" + "\tlet xs: []th = [];\n" + "\tappend(xs, th { pc = 7, matched = true });\n" + "\tlet e: th = xs[0];\n" + "\tif (e.pc != 7) { return 1; };\n" + "\tif (!e.matched) { return 2; };\n" + "\treturn 70;\n" + "};\n", + 70, NULL }, + + /* C4 size matrix: 24B (3-word run), 40B (str header + 2 words), + * and 12B (maxalign 4 → MOVQ+MOVL tail) elements. */ + { "c4_let_elem_sizes", + "package main;\n" + "type t3 = struct { a: i64, b: i64, c: i64 };\n" + "type capture = struct { content: str, start: size, end: size };\n" + "type t12 = struct { x: u32, y: u32, z: u32 };\n" + "export fn main() i32 = {\n" + "\tlet xs: []t3 = [];\n" + "\tappend(xs, t3 { a = 1, b = 2, c = 3 });\n" + "\tappend(xs, t3 { a = 4, b = 5, c = 6 });\n" + "\tlet e: t3 = xs[1];\n" + "\tif (e.a != 4) { return 1; };\n" + "\tif (e.b != 5) { return 2; };\n" + "\tif (e.c != 6) { return 3; };\n" + "\tlet cs: []capture = [];\n" + "\tappend(cs, capture { content = \"abc\", start = 11, end = 13 });\n" + "\tlet c: capture = cs[0];\n" + "\tif (c.content.len != 3) { return 4; };\n" + "\tif (c.start != 11) { return 5; };\n" + "\tif (c.end != 13) { return 6; };\n" + "\tlet ns: []t12 = [];\n" + "\tappend(ns, t12 { x = 7u32, y = 8u32, z = 9u32 });\n" + "\tlet n: t12 = ns[0];\n" + "\tif (n.x != 7u32) { return 7; };\n" + "\tif (n.y != 8u32) { return 8; };\n" + "\tif (n.z != 9u32) { return 9; };\n" + "\treturn 71;\n" + "};\n", + 71, NULL }, + + /* Reviewer-C2 inheritance: aggregate leaf behind a deref spine + * into a let — `let c = (*ts)[i].cap`. Pre-C4 wwstage loud-bailed + * at its cgdot aggregate-leaf gate (correct per rule 7) while + * cstage SILENTLY emitted no copy; both now route the let copy + * through the resolver. */ + { "c4_let_deref_leaf", + "package main;\n" + "type capture = struct { content: str, start: size, end: size };\n" + "type t = struct { pc: size, cap: capture };\n" + "fn grab(ts: *[]t, i: size) i32 = {\n" + "\tlet c: capture = (*ts)[i].cap;\n" + "\tif (c.start != 11) { return 1; };\n" + "\tif (c.end != 13) { return 2; };\n" + "\tif (c.content.len != 3) { return 3; };\n" + "\treturn 0;\n" + "};\n" + "export fn main() i32 = {\n" + "\tlet ts: []t = [];\n" + "\tappend(ts, t { pc = 5, cap = capture { content = \"abc\", start = 11, end = 13 } });\n" + "\tlet rc: i32 = grab(&ts, 0);\n" + "\tif (rc != 0) { return rc; };\n" + "\treturn 72;\n" + "};\n", + 72, NULL }, + + /* C4 (F5/FC0): by-value range payload copies through the same + * full-width machinery — pre-C4 the single fldloadop word + * truncated every aggregate element to 8B (cstage), wwstage's + * structural esz fell to its 8-sentinel (or 1 for non-ident + * scrutinees: regex.finish's cs≠ww hunk) and the ww checker left + * the binding typeless (asserttyped bail on any field read). + * Rows: []struct sum of BOTH fields, []capture (40B, str header + * inside), []str (24B header, .len readback). */ + { "c4_range_byvalue", + "package main;\n" + "type th = struct { pc: size, weight: size };\n" + "type capture = struct { content: str, start: size, end: size };\n" + "export fn main() i32 = {\n" + "\tlet xs: []th = [];\n" + "\tappend(xs, th { pc = 7, weight = 100 });\n" + "\tappend(xs, th { pc = 8, weight = 200 });\n" + "\tlet total: size = 0;\n" + "\tfor (let t .. xs) {\n" + "\t\ttotal += t.pc;\n" + "\t\ttotal += t.weight;\n" + "\t};\n" + "\tif (total != 315) { return 1; };\n" + "\tlet cs: []capture = [];\n" + "\tappend(cs, capture { content = \"abc\", start = 11, end = 13 });\n" + "\tappend(cs, capture { content = \"wy\", start = 21, end = 23 });\n" + "\tlet csum: size = 0;\n" + "\tfor (let cp .. cs) {\n" + "\t\tcsum += cp.content.len: size;\n" + "\t\tcsum += cp.start;\n" + "\t\tcsum += cp.end;\n" + "\t};\n" + "\tif (csum != 73) { return 2; };\n" + "\tlet ss: []str = [];\n" + "\tappend(ss, \"hello\");\n" + "\tappend(ss, \"worldly\");\n" + "\tlet slen: size = 0;\n" + "\tfor (let s .. ss) {\n" + "\t\tslen += s.len: size;\n" + "\t};\n" + "\tif (slen != 12) { return 3; };\n" + "\treturn 73;\n" + "};\n", + 73, NULL }, + + /* C4 range edges: zero-iteration over an empty slice; binder + * MUTATION pinning Hare's by-VALUE payload semantics (writes to + * the binder land in its frame slot copy, never the slice — a + * by-reference regression would flip xs[0]); and a 12B element + * exercising the sized MOVL tail of the range word-copy (the + * 16B/24B/40B rows above are all full-word). */ + { "c4_range_edges", + "package main;\n" + "type th = struct { pc: size, matched: bool };\n" + "type t12 = struct { x: u32, y: u32, z: u32 };\n" + "export fn main() i32 = {\n" + "\tlet es: []th = [];\n" + "\tfor (let t .. es) { return 1; };\n" + "\tlet xs: []th = [];\n" + "\tappend(xs, th { pc = 7, matched = true });\n" + "\tlet cnt: size = 0;\n" + "\tfor (let t2 .. xs) { t2.pc = 99; t2.matched = false; cnt += 1; };\n" + "\tif (cnt != 1) { return 2; };\n" + "\tif (xs[0].pc != 7) { return 3; };\n" + "\tif (!xs[0].matched) { return 4; };\n" + "\tlet ns: []t12 = [];\n" + "\tappend(ns, t12 { x = 1u32, y = 2u32, z = 3u32 });\n" + "\tappend(ns, t12 { x = 4u32, y = 5u32, z = 6u32 });\n" + "\tlet sum: size = 0;\n" + "\tfor (let n .. ns) { sum += n.x: size; sum += n.y: size; sum += n.z: size; };\n" + "\tif (sum != 21) { return 5; };\n" + "\treturn 74;\n" + "};\n", + 74, NULL }, + + /* C4 loud tail: a non-place rhs (tagged unwrap into a struct let) + * has no source address and nothing below the aggregate arm can + * initialise a >8B slot — pre-C4 cstage emitted NO init and + * wwstage stored 8B, both silent. */ + { "reject_let_unhandled", + "package main;\n" + "type pair = struct { a: i64, b: i64 };\n" + "fn mk() (pair | nomem) = {\n" + "\treturn pair { a = 1, b = 2 };\n" + "};\n" + "export fn main() i32 = {\n" + "\tlet p: pair = mk()!;\n" + "\treturn p.a: i32;\n" + "};\n", + BUILD_FAIL, + "let: aggregate init from unhandled rhs shape (task #7/rule-7)" }, }; /* errlog_has — the build-failure stderr must carry the row's expected