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

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