wcc+w6c_ww: append() spread place-chain sources via cgplaceaddr (#35)

#35 (fold-5 blocker, PG6): the append() spread arm dispatched on
SOURCE node kind — ident-local only; both fold-5 dup shapes
loud-rejected on BOTH stages: deref-spine `append(dup,
(*tsp)[0].caps...)` (add_thread regex.ha:569/572) and indexed
`append(res, ts[0].caps...)` (search ha:820).

Route every non-ident-local source through cgplaceaddr (the
established place choke-point: C1 stores, C1.25 aggregate fields, FA1
append targets, #49 element sources): the source header ADDRESS
resolves ONCE, pre-grow, into a fresh per-site @appsprscr spill, and
every loop iteration re-reads .ptr/.len THROUGH the spilled header
after the grow. This adapts the #49 split ruling to the spread's
full-range copy: the chain's rvalues (deref-root pointer expr, index
exprs) evaluate exactly once PRE-grow — an index reading the dst's
len sees the pre-append value — while the source data base and len
re-derive live each iteration, so a source header aliasing the dst
header re-roots post-realloc. A header reached through a buffer the
grow reallocs reads the STALE copy — bit-identical under the
non-reclaiming rt/malloc, the same documented #49 stale-base hole
(pinned by the spread_selfalias_chain row). Ident-local sources keep
the legacy BP-disp emission byte-identical.

New loudness with the same fix: the spread source's stamped type must
chase to TY_SLICE/TY_STR — a [N]T array ident source previously read
its first 16 DATA bytes as a {ptr,len} header, silently. Array wiring
plus the remaining rvalue sources (CALL, slicing exprs) stay loud,
filed task #27. Global-ident sources now resolve on cstage but are
blocked by a pre-existing wwstage checker reject ("let: not
assignable", task #29) — no dual-stage row until that closes.

806_append_place: reject_spread_src GRADUATES to a runtime row
(spread_place_deref, the old reject source verbatim + readback); new
rows spread_dup_copy (PG6 verbatim: both fold-5 shapes, 56B capture
elements, copy-semantics mutation pin, want 139), spread_place_kinds
(str 24B headers / narrow i32 / empty deref source),
spread_growth_place (40-elem spread crossing cap doublings),
spread_selfalias_chain (source header inside the dst's grown buffer),
and the two new loud-tail rejects (CALL rvalue, [N]T array) pinning
the "#35:" diagnostic on both stages. 87 fixtures green (was 70),
per-row cs/ww asm byte-cmp included.

Unblocks regex fold-5a: add_thread regex.ha:569/572 + search ha:820
spreads go from loud-bound to real spread.
This commit is contained in:
2026-06-04 23:40:31 +09:00
parent 1bcf2726cf
commit 0ce98cc5dc
5 changed files with 449 additions and 81 deletions

View File

@@ -59,8 +59,21 @@
* elem_kinds_place | place-resolved SOURCE × every other | 79
* | element kind (scalar/narrow/str/slice/ |
* | tagged via the pre-existing arms) |
* reject_spread_src | non-ident spread SOURCE through a deref | BUILD_FAIL
* | target (the #35 designed boundary) |
* spread_place_deref | append(*p, (*q)[i].xs...) — the old #35 | 81
* | reject source, graduated (cgplaceaddr) |
* spread_dup_copy | PG6: deref-spine + indexed 56B-struct | 139
* | spread sources (regex.ha:569/820 dup |
* | shapes) + copy-semantics pin |
* spread_place_kinds | place-sourced spread × str header / | 82
* | narrow i32 / empty deref source |
* spread_growth_place | 40-elem place-sourced spread crossing | 83
* | cap doublings (dst base moves mid-loop) |
* spread_selfalias_chain| source header inside the dst's grown | 119
* | buffer — #49 stale-base hole pin |
* reject_spread_call_src| CALL rvalue spread source — #35 | BUILD_FAIL
* | residual (task #27) |
* reject_spread_array_src| [N]T array spread source — was a | BUILD_FAIL
* | silent 16-byte garbage header read |
* reject_call_src | CALL rvalue element source — the #49 | BUILD_FAIL
* | loud tail (#42-style bound) |
*
@@ -596,22 +609,169 @@ static const struct row rows[] = {
"};\n",
79, NULL },
/* The FA4 designed boundary (task #35, old #37): a spread SOURCE that is
* not an ident local must stay loud even now that the deref
* TARGET resolves. */
{ "reject_spread_src",
/* #35 graduation: the FA4-era designed boundary — a place-chain
* spread SOURCE — now resolves through cgplaceaddr (header
* address pre-grow, ptr/len re-read through it per iteration).
* This row is the OLD reject source verbatim, now with elements
* and a readback. */
{ "spread_place_deref",
"package main;\n"
"type holder = struct { tag: i64, xs: []i64 };\n"
"fn dup(p: *[]i64, q: *[]holder, i: i64) void = {\n"
"\tappend(*p, (*q)[i].xs...);\n"
"};\n"
"export fn main() i32 = {\n"
"\tlet inner: []i64 = [];\n"
"\tappend(inner, 5);\n"
"\tappend(inner, 9);\n"
"\tlet ys: []i64 = [];\n"
"\tlet hs: []holder = [];\n"
"\tappend(hs, holder { tag = 1, xs = inner });\n"
"\tdup(&ys, &hs, 0);\n"
"\tif (len(ys) != 2) { return 1; };\n"
"\tif (ys[0] != 5 || ys[1] != 9) { return 2; };\n"
"\treturn 81;\n"
"};\n",
81, NULL },
/* #35, the fold-5 dup shapes (PG6): (a) deref-spine source
* `(*tsp)[0].caps...` (add_thread regex.ha:569/572), (b) indexed
* source `ts[0].caps...` (search ha:820), 56B struct elements;
* plus the COPY-semantics pin — mutating the dup must not touch
* the source. */
{ "spread_dup_copy",
"package main;\n"
"type capture = struct {\n"
"\tcontent: str,\n"
"\tstart: size,\n"
"\tstart_bytesize: size,\n"
"\tend: size,\n"
"\tend_bytesize: size,\n"
"};\n"
"type thr = struct { pc: size, caps: []capture };\n"
"export fn main() i32 = {\n"
"\tlet c0: []capture = [];\n"
"\tappend(c0, capture { content = \"x\", start = 3: size, start_bytesize = 3: size, end = 8: size, end_bytesize = 8: size });\n"
"\tappend(c0, capture { content = \"yz\", start = 5: size, start_bytesize = 5: size, end = 9: size, end_bytesize = 9: size });\n"
"\tlet ts: []thr = [];\n"
"\tappend(ts, thr { pc = 0: size, caps = c0 });\n"
"\tlet tsp = &ts;\n"
"\tlet dup: []capture = [];\n"
"\tappend(dup, (*tsp)[0].caps...);\n"
"\tif (len(dup) != 2) { return 1; };\n"
"\tif (len(dup[1].content) != 2 || dup[1].content[0] != 'y') { return 2; };\n"
"\tlet res: []capture = [];\n"
"\tappend(res, ts[0].caps...);\n"
"\tif (len(res) != 2) { return 3; };\n"
"\tif (res[0].end != 8 || res[1].start != 5) { return 4; };\n"
"\tdup[0].start = 100: size;\n"
"\tif (ts[0].caps[0].start != 3) { return 5; };\n"
"\tlet acc = dup[0].start + dup[1].end + res[0].start + res[1].end_bytesize\n"
"\t\t+ ts[0].caps[0].end + (len(dup): size) * 5;\n"
"\treturn acc: i32;\n"
"};\n",
139, NULL },
/* #35 element kinds through a PLACE source: str 24B headers,
* narrow i32 (scalar load/store width), and the empty source
* (zero-iteration loop) through a deref. */
{ "spread_place_kinds",
"package main;\n"
"type holder = struct { tag: i64, ss: []str, ns: []i32 };\n"
"export fn main() i32 = {\n"
"\tlet h = holder { tag = 1, ss = [], ns = [] };\n"
"\tappend(h.ss, \"ab\");\n"
"\tappend(h.ss, \"cde\");\n"
"\tappend(h.ns, 7: i32);\n"
"\tlet hs: []holder = [];\n"
"\tappend(hs, h);\n"
"\tlet ss: []str = [];\n"
"\tappend(ss, hs[0].ss...);\n"
"\tif (len(ss) != 2) { return 1; };\n"
"\tif (len(ss[0]) != 2 || ss[0][1] != 'b') { return 2; };\n"
"\tif (len(ss[1]) != 3 || ss[1][2] != 'e') { return 3; };\n"
"\tlet ns: []i32 = [];\n"
"\tappend(ns, hs[0].ns...);\n"
"\tif (len(ns) != 1 || ns[0] != 7) { return 4; };\n"
"\tlet empty: []i64 = [];\n"
"\tlet ep = ∅\n"
"\tlet acc: []i64 = [];\n"
"\tappend(acc, (*ep)...);\n"
"\tif (len(acc) != 0) { return 5; };\n"
"\treturn 82;\n"
"};\n",
82, NULL },
/* #35 growth across cap: a 40-element place-sourced spread into a
* non-empty dst crosses several cap doublings — rt_ensure moves
* the dst base mid-loop; every slot lands because the dst header
* re-reads post-grow each iteration. */
{ "spread_growth_place",
"package main;\n"
"type holder = struct { tag: i64, xs: []i64 };\n"
"export fn main() i32 = {\n"
"\tlet big: []i64 = [];\n"
"\tlet i: i64 = 0;\n"
"\tfor (i < 40) { append(big, i); i += 1; };\n"
"\tlet hs: []holder = [];\n"
"\tappend(hs, holder { tag = 2, xs = big });\n"
"\tlet dst: []i64 = [];\n"
"\tappend(dst, 100);\n"
"\tappend(dst, hs[0].xs...);\n"
"\tif (len(dst) != 41) { return 1; };\n"
"\tif (dst[0] != 100 || dst[1] != 0 || dst[40] != 39) { return 2; };\n"
"\treturn 83;\n"
"};\n",
83, NULL },
/* #35 aliasing: the source HEADER lives inside the dst's own
* buffer (xs[0].kids reached through xs.ptr); the first grow
* reallocs that buffer. The pre-grow-resolved header address then
* reads the STALE copy — bit-identical under the non-reclaiming
* rt/malloc, the documented #49 stale-base hole. Pins that the
* spread terminates AND copies the right elements. */
{ "spread_selfalias_chain",
"package main;\n"
"type tnode = struct { v: i64, kids: []tnode };\n"
"export fn main() i32 = {\n"
"\tlet inner: []tnode = [];\n"
"\tappend(inner, tnode { v = 7, kids = [] });\n"
"\tappend(inner, tnode { v = 9, kids = [] });\n"
"\tlet xs: []tnode = [];\n"
"\tappend(xs, tnode { v = 1, kids = inner });\n"
"\tappend(xs, xs[0].kids...);\n"
"\tif (len(xs) != 3) { return 1; };\n"
"\tif (xs[0].v != 1 || xs[1].v != 7 || xs[2].v != 9) { return 2; };\n"
"\treturn (81 + xs[1].v * 2 + xs[2].v * 2 + (len(xs): i64) * 2): i32;\n"
"};\n",
119, NULL },
/* The #35 residual boundary (task #27): a CALL rvalue spread
* source has no place to resolve — stays loud until a
* scratch-receive route lands. */
{ "reject_spread_call_src",
"package main;\n"
"fn mk() []i64 = { let r: []i64 = []; append(r, 5); return r; };\n"
"export fn main() i32 = {\n"
"\tlet xs: []i64 = [];\n"
"\tappend(xs, mk()...);\n"
"\treturn 0;\n"
"};\n",
BUILD_FAIL, "#34: append() spread source shape unsupported (rule-7)" },
BUILD_FAIL, "#35: append() spread source shape unsupported (rule-7)" },
/* The #35 residual boundary (task #27): a [N]T ARRAY source place
* IS its storage, not a {ptr,len,cap} header — pre-guard the
* ident path silently read its first 16 data bytes as ptr/len.
* Loud until wired. */
{ "reject_spread_array_src",
"package main;\n"
"export fn main() i32 = {\n"
"\tlet a: [2]i64 = [3, 4];\n"
"\tlet xs: []i64 = [];\n"
"\tappend(xs, a...);\n"
"\treturn 0;\n"
"};\n",
BUILD_FAIL, "#35: append() spread source shape unsupported (rule-7)" },
/* The #49 boundary: a CALL rvalue source has no place to resolve
* — stays loud (the #42-style bound) until a scratch-receive