From 0ce98cc5dc839f38af2442841a97e166f6e16fa6 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Thu, 4 Jun 2026 23:40:31 +0900 Subject: [PATCH] wcc+w6c_ww: append() spread place-chain sources via cgplaceaddr (#35) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #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. --- cmd/w6c/cgen.c | 77 +++++++++--- selfhost/cmd/w6c/main.combined.ww | 93 +++++++++++--- selfhost/cmd/wcc/cgenexpr.ww | 93 +++++++++++--- selfhost/cmd/wwdump/main.combined.ww | 93 +++++++++++--- test/wcc/806_append_place.c | 174 +++++++++++++++++++++++++-- 5 files changed, 449 insertions(+), 81 deletions(-) diff --git a/cmd/w6c/cgen.c b/cmd/w6c/cgen.c index 461cbd75..31ea348e 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -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)); diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 9cd26442..38f92933 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -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); diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index 0c3b1682..2bc2c69a 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -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); diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index f73d779e..3883a33e 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -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); diff --git a/test/wcc/806_append_place.c b/test/wcc/806_append_place.c index c242ede0..3626d53c 100644 --- a/test/wcc/806_append_place.c +++ b/test/wcc/806_append_place.c @@ -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