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

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