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

@@ -7481,19 +7481,56 @@ cgexpr(Cg *c, Node *n, Local *locals)
* local ident used to fall PAST the spread arm
* into the single-value stores with the
* N_SPREAD node (cstage garbage store; wwstage
* silently SKIPPED it — divergent). Deferred
* source shape, task #37. */
* silently SKIPPED it — divergent). */
if (vn->kind == N_SPREAD) {
if (!vn->lhs || vn->lhs->kind != N_IDENT)
fatal("#34: append() spread source "
/* #35: only a {ptr,len,cap}-headered
* source reads as a header below; a
* [N]T array place IS its storage —
* the ident path used to read its
* first 16 data bytes as ptr/len,
* silently. Loud until wired (task
* #27); str shares the slice header
* layout. */
Type *itu = vn->lhs
? type_chase_named(vn->lhs->type) : NULL;
if (itu == NULL || (itu->kind != TY_SLICE
&& itu->kind != TY_STR))
fatal("#35: append() spread source "
"shape unsupported (rule-7)");
if (localfind(locals, vn->lhs->str) == 0)
fatal("#34: append() spread source "
"ident is not a local (rule-7)");
}
if (vn->kind == N_SPREAD &&
vn->lhs && vn->lhs->kind == N_IDENT) {
int it_off = localfind(locals, vn->lhs->str);
int it_off = 0;
int it_scr = 0;
if (vn->lhs->kind == N_IDENT)
it_off = localfind(locals, vn->lhs->str);
if (it_off == 0) {
/* #35: place-chain source (deref
* spine, indexed chain, global
* ident — the regex.ha:569/820
* dup shapes) resolves its header
* ADDRESS through cgplaceaddr
* ONCE, pre-grow: the chain's
* rvalues run exactly once (the
* #49 split's pre-grow half) and
* every iteration re-reads
* .ptr/.len THROUGH the spilled
* header post-grow (the live
* re-derivation half). A header
* reached through a buffer the
* grow reallocs keeps Hare's
* stale-base hole — see the #49
* comment below. Rvalue sources
* (CALL, slicing exprs) have no
* place — loud, task #27. Fresh
* spill slot per SITE for the
* same nesting reason as
* @apphdrscr above. */
if (!cgplaceaddr(c, vn->lhs, D_BX, locals))
fatal("#35: append() spread source "
"shape unsupported (rule-7)");
it_scr = local_alloc(c, &locals,
"@appsprscr", 8, cg_frame);
ins2(c, A_MOVQ, areg(D_BX),
amem(D_BP, it_scr));
}
int load_op = fldloadop(esub, esz);
/* push counter (i) on stack */
ins2(c, A_SUBQ, aimm(8), areg(D_SP));
@@ -7502,7 +7539,11 @@ cgexpr(Cg *c, Node *n, Local *locals)
char *le = mklabel(c, "spr_e");
label(c, ll);
ins2(c, A_MOVQ, amem(D_SP, 0), areg(D_CX));
ins2(c, A_MOVQ, amem(D_BP, it_off + 8), areg(D_DX));
if (it_scr != 0) {
ins2(c, A_MOVQ, amem(D_BP, it_scr), areg(D_DX));
ins2(c, A_MOVQ, amem(D_DX, 8), areg(D_DX));
} else
ins2(c, A_MOVQ, amem(D_BP, it_off + 8), areg(D_DX));
ins2(c, A_CMPQ, areg(D_DX), areg(D_CX));
ins1(c, A_JGE, abranch(le));
if (el_wide) {
@@ -7522,7 +7563,11 @@ cgexpr(Cg *c, Node *n, Local *locals)
ins2(c, A_MOVQ, aimm(esz), areg(D_AX));
ins2(c, A_IMULQ, areg(D_AX), areg(D_CX));
}
ins2(c, A_MOVQ, amem(D_BP, it_off), areg(D_BX));
if (it_scr != 0) {
ins2(c, A_MOVQ, amem(D_BP, it_scr), areg(D_BX));
ins2(c, A_MOVQ, amem(D_BX, 0), areg(D_BX));
} else
ins2(c, A_MOVQ, amem(D_BP, it_off), areg(D_BX));
ins2(c, A_ADDQ, areg(D_CX), areg(D_BX));
cg_append_slot(c, sn_direct, sn_off,
sn_scr, esz, D_DX);
@@ -7553,7 +7598,11 @@ cgexpr(Cg *c, Node *n, Local *locals)
continue;
}
/* AX = items.ptr[i] */
ins2(c, A_MOVQ, amem(D_BP, it_off), areg(D_BX));
if (it_scr != 0) {
ins2(c, A_MOVQ, amem(D_BP, it_scr), areg(D_BX));
ins2(c, A_MOVQ, amem(D_BX, 0), areg(D_BX));
} else
ins2(c, A_MOVQ, amem(D_BP, it_off), areg(D_BX));
if (esz > 1) {
ins2(c, A_MOVQ, aimm(esz), areg(D_AX));
ins2(c, A_IMULQ, areg(D_AX), areg(D_CX));

View File

@@ -25244,21 +25244,53 @@ fn cgappend(c: *cgen, n: *node) void = {
// #34 review: a non-ident/non-local spread source used
// to be silently SKIPPED here while cstage fell past
// its spread arm into the single-value stores with the
// N_SPREAD node (garbage store) — divergent. Deferred
// source shape, task #37.
// N_SPREAD node (garbage store) — divergent.
let it: *node = vn.lhs;
if (it == nil || it.kind != nkind.N_IDENT) {
let m34p: str = "#34: append() spread source shape unsupported (rule-7)\n";
os.write(2, m34p.ptr, m34p.len: u64);
// #35: only a {ptr,len,cap}-headered source reads as a
// header below; a [N]T array place IS its storage — the
// ident path used to read its first 16 data bytes as
// ptr/len, silently. Loud until wired (task #27); str
// shares the slice header layout.
let itu: *tinfo = nil;
if (it != nil) { itu = it.type_: *tinfo; };
for (itu != nil && itu.kind == tykind.TY_NAMED) { itu = itu.under; };
if (itu == nil || (itu.kind != tykind.TY_SLICE
&& itu.kind != tykind.TY_STR)) {
let m35p: str = "#35: append() spread source shape unsupported (rule-7)\n";
os.write(2, m35p.ptr, m35p.len: u64);
os.exit(1);
};
let itlocal: *local = localfindnode(c, it.str);
if (itlocal == nil) {
let m34q: str = "#34: append() spread source ident is not a local (rule-7)\n";
os.write(2, m34q.ptr, m34q.len: u64);
os.exit(1);
let it_off: i32 = 0;
let itscr: i32 = 0;
if (it.kind == nkind.N_IDENT) {
let itlocal: *local = localfindnode(c, it.str);
if (itlocal != nil) { it_off = itlocal.off; };
};
if (it_off == 0) {
// #35: place-chain source (deref spine, indexed
// chain, global ident — the regex.ha:569/820 dup
// shapes) resolves its header ADDRESS through
// cgplaceaddr ONCE, pre-grow: the chain's rvalues
// run exactly once (the #49 split's pre-grow
// half) and every iteration re-reads .ptr/.len
// THROUGH the spilled header post-grow (the live
// re-derivation half). A header reached through a
// buffer the grow reallocs keeps Hare's
// stale-base hole — see the #49 comment below.
// Rvalue sources (CALL, slicing exprs) have no
// place — loud, task #27. Fresh spill slot per
// SITE for the same nesting reason as @apphdrscr
// above.
if (!cgplaceaddr(c, it, "BX")) {
let m35q: str = "#35: append() spread source shape unsupported (rule-7)\n";
os.write(2, m35q.ptr, m35q.len: u64);
os.exit(1);
};
itscr = localalloc(c, "@appsprscr", 8, nil);
emitline("\tMOVQ\tBX, ");
emitoff(itscr: i64);
emitline("(BP)\n");
};
let it_off: i32 = itlocal.off;
let load_op: str = tnodeloadop(c, etnode, esz);
if (!sndirect) {
// FA1: no etnode behind `*p` — signedness off the
@@ -25272,9 +25304,16 @@ fn cgappend(c: *cgen, n: *node) void = {
let le: str = mklabel(c, "spr_e");
emitlabel(ll);
emitline("\tMOVQ\t(SP), CX\n");
emitline("\tMOVQ\t");
emitoff((it_off + 8): i64);
emitline("(BP), DX\n");
if (itscr != 0) {
emitline("\tMOVQ\t");
emitoff(itscr: i64);
emitline("(BP), DX\n");
emitline("\tMOVQ\t8(DX), DX\n");
} else {
emitline("\tMOVQ\t");
emitoff((it_off + 8): i64);
emitline("(BP), DX\n");
};
emitline("\tCMPQ\tDX, CX\n");
emitline("\tJGE\t"); emitline(le); emitline("\n");
if (elwide) {
@@ -25293,9 +25332,16 @@ fn cgappend(c: *cgen, n: *node) void = {
emitline(", AX\n");
emitline("\tIMULQ\tAX, CX\n");
};
emitline("\tMOVQ\t");
emitoff(it_off: i64);
emitline("(BP), BX\n");
if (itscr != 0) {
emitline("\tMOVQ\t");
emitoff(itscr: i64);
emitline("(BP), BX\n");
emitline("\tMOVQ\t(BX), BX\n");
} else {
emitline("\tMOVQ\t");
emitoff(it_off: i64);
emitline("(BP), BX\n");
};
emitline("\tADDQ\tCX, BX\n");
cgappendslot(c, sndirect, sn_off, snscr, esz, "DX");
let wk: i32 = 0;
@@ -25342,9 +25388,16 @@ fn cgappend(c: *cgen, n: *node) void = {
vn = vn.next;
continue;
};
emitline("\tMOVQ\t");
emitoff(it_off: i64);
emitline("(BP), BX\n");
if (itscr != 0) {
emitline("\tMOVQ\t");
emitoff(itscr: i64);
emitline("(BP), BX\n");
emitline("\tMOVQ\t(BX), BX\n");
} else {
emitline("\tMOVQ\t");
emitoff(it_off: i64);
emitline("(BP), BX\n");
};
if (esz > 1) {
emitline("\tMOVQ\t$");
emitint(esz: i64);

View File

@@ -4810,21 +4810,53 @@ fn cgappend(c: *cgen, n: *node) void = {
// #34 review: a non-ident/non-local spread source used
// to be silently SKIPPED here while cstage fell past
// its spread arm into the single-value stores with the
// N_SPREAD node (garbage store) — divergent. Deferred
// source shape, task #37.
// N_SPREAD node (garbage store) — divergent.
let it: *node = vn.lhs;
if (it == nil || it.kind != nkind.N_IDENT) {
let m34p: str = "#34: append() spread source shape unsupported (rule-7)\n";
os.write(2, m34p.ptr, m34p.len: u64);
// #35: only a {ptr,len,cap}-headered source reads as a
// header below; a [N]T array place IS its storage — the
// ident path used to read its first 16 data bytes as
// ptr/len, silently. Loud until wired (task #27); str
// shares the slice header layout.
let itu: *tinfo = nil;
if (it != nil) { itu = it.type_: *tinfo; };
for (itu != nil && itu.kind == tykind.TY_NAMED) { itu = itu.under; };
if (itu == nil || (itu.kind != tykind.TY_SLICE
&& itu.kind != tykind.TY_STR)) {
let m35p: str = "#35: append() spread source shape unsupported (rule-7)\n";
os.write(2, m35p.ptr, m35p.len: u64);
os.exit(1);
};
let itlocal: *local = localfindnode(c, it.str);
if (itlocal == nil) {
let m34q: str = "#34: append() spread source ident is not a local (rule-7)\n";
os.write(2, m34q.ptr, m34q.len: u64);
os.exit(1);
let it_off: i32 = 0;
let itscr: i32 = 0;
if (it.kind == nkind.N_IDENT) {
let itlocal: *local = localfindnode(c, it.str);
if (itlocal != nil) { it_off = itlocal.off; };
};
if (it_off == 0) {
// #35: place-chain source (deref spine, indexed
// chain, global ident — the regex.ha:569/820 dup
// shapes) resolves its header ADDRESS through
// cgplaceaddr ONCE, pre-grow: the chain's rvalues
// run exactly once (the #49 split's pre-grow
// half) and every iteration re-reads .ptr/.len
// THROUGH the spilled header post-grow (the live
// re-derivation half). A header reached through a
// buffer the grow reallocs keeps Hare's
// stale-base hole — see the #49 comment below.
// Rvalue sources (CALL, slicing exprs) have no
// place — loud, task #27. Fresh spill slot per
// SITE for the same nesting reason as @apphdrscr
// above.
if (!cgplaceaddr(c, it, "BX")) {
let m35q: str = "#35: append() spread source shape unsupported (rule-7)\n";
os.write(2, m35q.ptr, m35q.len: u64);
os.exit(1);
};
itscr = localalloc(c, "@appsprscr", 8, nil);
emitline("\tMOVQ\tBX, ");
emitoff(itscr: i64);
emitline("(BP)\n");
};
let it_off: i32 = itlocal.off;
let load_op: str = tnodeloadop(c, etnode, esz);
if (!sndirect) {
// FA1: no etnode behind `*p` — signedness off the
@@ -4838,9 +4870,16 @@ fn cgappend(c: *cgen, n: *node) void = {
let le: str = mklabel(c, "spr_e");
emitlabel(ll);
emitline("\tMOVQ\t(SP), CX\n");
emitline("\tMOVQ\t");
emitoff((it_off + 8): i64);
emitline("(BP), DX\n");
if (itscr != 0) {
emitline("\tMOVQ\t");
emitoff(itscr: i64);
emitline("(BP), DX\n");
emitline("\tMOVQ\t8(DX), DX\n");
} else {
emitline("\tMOVQ\t");
emitoff((it_off + 8): i64);
emitline("(BP), DX\n");
};
emitline("\tCMPQ\tDX, CX\n");
emitline("\tJGE\t"); emitline(le); emitline("\n");
if (elwide) {
@@ -4859,9 +4898,16 @@ fn cgappend(c: *cgen, n: *node) void = {
emitline(", AX\n");
emitline("\tIMULQ\tAX, CX\n");
};
emitline("\tMOVQ\t");
emitoff(it_off: i64);
emitline("(BP), BX\n");
if (itscr != 0) {
emitline("\tMOVQ\t");
emitoff(itscr: i64);
emitline("(BP), BX\n");
emitline("\tMOVQ\t(BX), BX\n");
} else {
emitline("\tMOVQ\t");
emitoff(it_off: i64);
emitline("(BP), BX\n");
};
emitline("\tADDQ\tCX, BX\n");
cgappendslot(c, sndirect, sn_off, snscr, esz, "DX");
let wk: i32 = 0;
@@ -4908,9 +4954,16 @@ fn cgappend(c: *cgen, n: *node) void = {
vn = vn.next;
continue;
};
emitline("\tMOVQ\t");
emitoff(it_off: i64);
emitline("(BP), BX\n");
if (itscr != 0) {
emitline("\tMOVQ\t");
emitoff(itscr: i64);
emitline("(BP), BX\n");
emitline("\tMOVQ\t(BX), BX\n");
} else {
emitline("\tMOVQ\t");
emitoff(it_off: i64);
emitline("(BP), BX\n");
};
if (esz > 1) {
emitline("\tMOVQ\t$");
emitint(esz: i64);

View File

@@ -25244,21 +25244,53 @@ fn cgappend(c: *cgen, n: *node) void = {
// #34 review: a non-ident/non-local spread source used
// to be silently SKIPPED here while cstage fell past
// its spread arm into the single-value stores with the
// N_SPREAD node (garbage store) — divergent. Deferred
// source shape, task #37.
// N_SPREAD node (garbage store) — divergent.
let it: *node = vn.lhs;
if (it == nil || it.kind != nkind.N_IDENT) {
let m34p: str = "#34: append() spread source shape unsupported (rule-7)\n";
os.write(2, m34p.ptr, m34p.len: u64);
// #35: only a {ptr,len,cap}-headered source reads as a
// header below; a [N]T array place IS its storage — the
// ident path used to read its first 16 data bytes as
// ptr/len, silently. Loud until wired (task #27); str
// shares the slice header layout.
let itu: *tinfo = nil;
if (it != nil) { itu = it.type_: *tinfo; };
for (itu != nil && itu.kind == tykind.TY_NAMED) { itu = itu.under; };
if (itu == nil || (itu.kind != tykind.TY_SLICE
&& itu.kind != tykind.TY_STR)) {
let m35p: str = "#35: append() spread source shape unsupported (rule-7)\n";
os.write(2, m35p.ptr, m35p.len: u64);
os.exit(1);
};
let itlocal: *local = localfindnode(c, it.str);
if (itlocal == nil) {
let m34q: str = "#34: append() spread source ident is not a local (rule-7)\n";
os.write(2, m34q.ptr, m34q.len: u64);
os.exit(1);
let it_off: i32 = 0;
let itscr: i32 = 0;
if (it.kind == nkind.N_IDENT) {
let itlocal: *local = localfindnode(c, it.str);
if (itlocal != nil) { it_off = itlocal.off; };
};
if (it_off == 0) {
// #35: place-chain source (deref spine, indexed
// chain, global ident — the regex.ha:569/820 dup
// shapes) resolves its header ADDRESS through
// cgplaceaddr ONCE, pre-grow: the chain's rvalues
// run exactly once (the #49 split's pre-grow
// half) and every iteration re-reads .ptr/.len
// THROUGH the spilled header post-grow (the live
// re-derivation half). A header reached through a
// buffer the grow reallocs keeps Hare's
// stale-base hole — see the #49 comment below.
// Rvalue sources (CALL, slicing exprs) have no
// place — loud, task #27. Fresh spill slot per
// SITE for the same nesting reason as @apphdrscr
// above.
if (!cgplaceaddr(c, it, "BX")) {
let m35q: str = "#35: append() spread source shape unsupported (rule-7)\n";
os.write(2, m35q.ptr, m35q.len: u64);
os.exit(1);
};
itscr = localalloc(c, "@appsprscr", 8, nil);
emitline("\tMOVQ\tBX, ");
emitoff(itscr: i64);
emitline("(BP)\n");
};
let it_off: i32 = itlocal.off;
let load_op: str = tnodeloadop(c, etnode, esz);
if (!sndirect) {
// FA1: no etnode behind `*p` — signedness off the
@@ -25272,9 +25304,16 @@ fn cgappend(c: *cgen, n: *node) void = {
let le: str = mklabel(c, "spr_e");
emitlabel(ll);
emitline("\tMOVQ\t(SP), CX\n");
emitline("\tMOVQ\t");
emitoff((it_off + 8): i64);
emitline("(BP), DX\n");
if (itscr != 0) {
emitline("\tMOVQ\t");
emitoff(itscr: i64);
emitline("(BP), DX\n");
emitline("\tMOVQ\t8(DX), DX\n");
} else {
emitline("\tMOVQ\t");
emitoff((it_off + 8): i64);
emitline("(BP), DX\n");
};
emitline("\tCMPQ\tDX, CX\n");
emitline("\tJGE\t"); emitline(le); emitline("\n");
if (elwide) {
@@ -25293,9 +25332,16 @@ fn cgappend(c: *cgen, n: *node) void = {
emitline(", AX\n");
emitline("\tIMULQ\tAX, CX\n");
};
emitline("\tMOVQ\t");
emitoff(it_off: i64);
emitline("(BP), BX\n");
if (itscr != 0) {
emitline("\tMOVQ\t");
emitoff(itscr: i64);
emitline("(BP), BX\n");
emitline("\tMOVQ\t(BX), BX\n");
} else {
emitline("\tMOVQ\t");
emitoff(it_off: i64);
emitline("(BP), BX\n");
};
emitline("\tADDQ\tCX, BX\n");
cgappendslot(c, sndirect, sn_off, snscr, esz, "DX");
let wk: i32 = 0;
@@ -25342,9 +25388,16 @@ fn cgappend(c: *cgen, n: *node) void = {
vn = vn.next;
continue;
};
emitline("\tMOVQ\t");
emitoff(it_off: i64);
emitline("(BP), BX\n");
if (itscr != 0) {
emitline("\tMOVQ\t");
emitoff(itscr: i64);
emitline("(BP), BX\n");
emitline("\tMOVQ\t(BX), BX\n");
} else {
emitline("\tMOVQ\t");
emitoff(it_off: i64);
emitline("(BP), BX\n");
};
if (esz > 1) {
emitline("\tMOVQ\t$");
emitint(esz: i64);

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