wcc+w6c_ww: aggregate let/range element copies via cgplaceaddr (F5)

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).
This commit is contained in:
2026-06-04 12:22:45 +09:00
parent 403625e433
commit d642017643
7 changed files with 721 additions and 62 deletions

View File

@@ -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);

View File

@@ -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); };

View File

@@ -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";

View File

@@ -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); };

View File

@@ -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); };

View File

@@ -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); };

View File

@@ -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