Files
ww/test/wcc/806_append_place.c
Hojun-Cho 0ce98cc5dc 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.
2026-06-05 00:00:00 +09:00

971 lines
35 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 806_append_place — cstage and wwstage agree, byte-for-byte and at
* runtime, that `append(*p, v)` / `append(*p, items...)` through a
* pointer-to-slice place lands the element (FA1, task #15 of the regex
* fold-2b blockers; the add_thread hot shape `threads: *[]thread`).
*
* Pre-fix this was ONE mirrored choke-point with TWO failure modes:
* cstage 0-defaulted the header base for any non-ident target
* (`sn_off = (sn->kind==N_IDENT) ? localfind : 0`), so 0(BP)/8(BP)
* became the "slice header" and rt_ensure corrupted the CALLER frame
* (SIGSEGV); wwstage cgappend silently emitted NOTHING (gate-blind
* cs≠ww). The fix re-keys the append lowering from BP-displacement
* assumptions onto a resolver-provided header PLACE (cgplaceaddr's
* third consumer after C1 assign-stores and C1.25 aggregate-field
* stores): the derived header address is spilled to a per-fn
* @apphdrscr slot so it survives rt_ensure (realloc moves .ptr, never
* the header), and every header access reloads from the slot.
* Ident-local targets keep the legacy BP-disp emission byte-identical.
* Any target place the resolver can't address dies LOUD (rule 7) —
* the old silent corruption can't come back through an unwired shape.
*
* row | shape | want
* ---------------------+-----------------------------------------+------
* scalar_param | append(*p, v) i64 via *[]i64 param | 60
* scalar_u8_multi | append(*q, a, b) u8 via local ptr | 61
* spread_param | append(*p, src...) i64 spread | 62
* spread_narrow_signed | append(*p, src...) i32 (MOVSXD load | 63
* | keyed off stamped tinfo, no tnode) |
* spread_wide | append(*p, src...) str (24B word-copy) | 64
* str_elem | append(*p, "hi") 3-word header | 65
* slice_elem | append(*p, one) [][]u8 3-word header | 66
* tagged_elem | append(*p, 42) (i64|void) widen-box | 67
* struct_lit | pA6 verbatim: append(*p, box{...}) | 68
* struct_ident | append(*p, b) local struct word-copy | 69
* realloc_loop | 100 appends via param, branched callee, | 70
* | cap-crossing reallocs, full multi-field |
* | readback + caller-frame sentinels |
* deref_spine | append((*q)[i].xs, v) resolver spine | 71
* neutral_direct | ident-local appends (the legacy arm — | 72
* | runtime-pins the asm-neutrality claim) |
* reentrant_value | append(*p, match{.. append(*q,..) ..}) | 33
* | — nested indirect append inside the |
* | outer's value expr; pins the per-SITE |
* | @apphdrscr slot (a shared slot clobbers)|
* identroot_dot | h.xs via *holder param — C1.5's reject, | 73
* | graduated by C2's resolver ident root |
* idxfield_struct | append(res, threads[i].root_capture) | 74
* | 56B indexed-FIELD source (#49, ha:819) |
* idx_computed_elem | append(ds, bs[i + 1]) computed-index | 75
* | whole-element source |
* derefspine_field | append(*r, (*q)[i].rc) param spines | 76
* | both sides |
* deref_src | append(*ds, *p) deref source | 77
* self_append_realloc | append(bs, bs[i]) ×33 — source inside | 78
* | the grown slice, resolve-after-grow |
* selfidx_oldlen_once | append(bs, bs[f(bs)]) — index expr runs | 80
* | ONCE, PRE-grow (sees old len): the #49 |
* | ruling's split-order semantics pin |
* elem_kinds_place | place-resolved SOURCE × every other | 79
* | element kind (scalar/narrow/str/slice/ |
* | tagged via the pre-existing arms) |
* 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) |
*
* BUILD_FAIL rows also assert the diagnostic TEXT (stderr substring,
* both stages) — a build that fails for any other reason (parse error,
* crash) is a vacuous reject and fails the row.
*
* Every non-BUILD_FAIL row also asserts cstage/wwstage asm byte-id.
*
* Readbacks deliberately avoid the still-open F2/F5 read shapes
* (`len(xs[i].field)`, `let s = xs[i].field`) — those are tracked
* separately; this test pins the append WRITE path.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/wait.h>
static int
runwait(const char *cmd)
{
int rc = system(cmd);
if (rc == -1) return -1;
if (WIFEXITED(rc)) return WEXITSTATUS(rc);
return -1;
}
/* want == BUILD_FAIL: the row must FAIL to build on both stages AND
* emit expect_err on stderr (rule 7 — never a silent acceptance;
* without the message check a row would pass vacuously on any
* unrelated build failure). */
#define BUILD_FAIL (-2147483647 - 1)
struct row {
const char *label;
const char *src;
int want;
const char *expect_err; /* BUILD_FAIL rows: required stderr substring */
};
static const struct row rows[] = {
/* The FA1 minimum: two appends through a *[]i64 param, then a
* third through a local pointer binding. */
{ "scalar_param",
"package main;\n"
"fn add(p: *[]i64, v: i64) void = { append(*p, v); };\n"
"export fn main() i32 = {\n"
"\tlet xs: []i64 = [];\n"
"\tadd(&xs, 5);\n"
"\tadd(&xs, 9);\n"
"\tif (len(xs) != 2) { return 1; };\n"
"\tif (xs[0] != 5 || xs[1] != 9) { return 2; };\n"
"\tlet q: *[]i64 = &xs;\n"
"\tappend(*q, 11);\n"
"\tif (len(xs) != 3 || xs[2] != 11) { return 3; };\n"
"\treturn 60;\n"
"};\n",
60, NULL },
/* esz=1 store (MOVB) + multi-value list through one resolve. */
{ "scalar_u8_multi",
"package main;\n"
"export fn main() i32 = {\n"
"\tlet bs: []u8 = [];\n"
"\tlet q: *[]u8 = &bs;\n"
"\tappend(*q, 1u8, 2u8);\n"
"\tif (len(bs) != 2) { return 1; };\n"
"\tif (bs[0] != 1u8 || bs[1] != 2u8) { return 2; };\n"
"\treturn 61;\n"
"};\n",
61, NULL },
{ "spread_param",
"package main;\n"
"fn addall(p: *[]i64, src: []i64) void = { append(*p, src...); };\n"
"export fn main() i32 = {\n"
"\tlet xs: []i64 = [];\n"
"\tappend(xs, 1, 2);\n"
"\tlet ys: []i64 = [];\n"
"\taddall(&ys, xs);\n"
"\taddall(&ys, xs);\n"
"\tif (len(ys) != 4) { return 1; };\n"
"\tif (ys[0] != 1 || ys[1] != 2 || ys[3] != 2) { return 2; };\n"
"\treturn 62;\n"
"};\n",
62, NULL },
/* Indirect mode has no declared tnode — the spread element LOAD
* op (sign extension) must come off the stamped tinfo; negative
* i32 elements pin MOVSXD. */
{ "spread_narrow_signed",
"package main;\n"
"fn addall(p: *[]i32, src: []i32) void = { append(*p, src...); };\n"
"export fn main() i32 = {\n"
"\tlet xs: []i32 = [];\n"
"\tappend(xs, -7i32, 9i32);\n"
"\tlet ys: []i32 = [];\n"
"\taddall(&ys, xs);\n"
"\tif (len(ys) != 2) { return 1; };\n"
"\tif (ys[0] != -7i32) { return 2; };\n"
"\tif (ys[1] != 9i32) { return 3; };\n"
"\treturn 63;\n"
"};\n",
63, NULL },
/* Wide (24B) spread element word-copy through the deref target. */
{ "spread_wide",
"package main;\n"
"fn addall(p: *[]str, src: []str) void = { append(*p, src...); };\n"
"export fn main() i32 = {\n"
"\tlet ss: []str = [];\n"
"\tappend(ss, \"ab\");\n"
"\tappend(ss, \"cde\");\n"
"\tlet tt: []str = [];\n"
"\taddall(&tt, ss);\n"
"\tif (len(tt) != 2) { return 1; };\n"
"\tlet a: str = tt[0];\n"
"\tlet b: str = tt[1];\n"
"\tif (a.len != 2 || b.len != 3) { return 2; };\n"
"\tif (b[0] != 'c') { return 3; };\n"
"\treturn 64;\n"
"};\n",
64, NULL },
/* str element: cgexpr leaves {ptr,len,cap} in AX/BX/CX; all three
* must survive rt_ensure AND the indirect header reload. */
{ "str_elem",
"package main;\n"
"fn adds(p: *[]str, v: str) void = { append(*p, v); };\n"
"export fn main() i32 = {\n"
"\tlet ss: []str = [];\n"
"\tadds(&ss, \"hi\");\n"
"\tadds(&ss, \"world\");\n"
"\tif (len(ss) != 2) { return 1; };\n"
"\tlet a: str = ss[0];\n"
"\tlet b: str = ss[1];\n"
"\tif (a.len != 2 || b.len != 5) { return 2; };\n"
"\tif (a[0] != 'h' || b[0] != 'w') { return 3; };\n"
"\treturn 65;\n"
"};\n",
65, NULL },
{ "slice_elem",
"package main;\n"
"fn addv(p: *[][]u8, v: []u8) void = { append(*p, v); };\n"
"export fn main() i32 = {\n"
"\tlet vv: [][]u8 = [];\n"
"\tlet one: []u8 = [];\n"
"\tappend(one, 5u8, 6u8);\n"
"\taddv(&vv, one);\n"
"\tif (len(vv) != 1) { return 1; };\n"
"\tlet got: []u8 = vv[0];\n"
"\tif (len(got) != 2 || got[1] != 6u8) { return 2; };\n"
"\treturn 66;\n"
"};\n",
66, NULL },
/* Tagged element: the #12 widen choke-point boxes through the
* resolver-derived slot pointer (grow-first, no register form). */
{ "tagged_elem",
"package main;\n"
"type tu = (i64 | void);\n"
"fn addt(p: *[]tu, v: i64) void = { append(*p, v); };\n"
"export fn main() i32 = {\n"
"\tlet ts: []tu = [];\n"
"\taddt(&ts, 42);\n"
"\tif (len(ts) != 1) { return 1; };\n"
"\tlet t0: tu = ts[0];\n"
"\tif (!(t0 is i64)) { return 2; };\n"
"\treturn 67;\n"
"};\n",
67, NULL },
/* The pA6 repro, verbatim shapes: struct-literal element fill
* through @appendscr while the header address sits in @apphdrscr. */
{ "struct_lit",
"package main;\n"
"type box = struct { pc: size, matched: bool };\n"
"fn add(p: *[]box, v: size) void = {\n"
"\tappend(*p, box { pc = v, matched = false });\n"
"};\n"
"export fn main() i32 = {\n"
"\tlet bs: []box = [];\n"
"\tadd(&bs, 5);\n"
"\tadd(&bs, 9);\n"
"\tif (len(bs) != 2) { return 1; };\n"
"\tif (bs[0].pc != 5) { return 2; };\n"
"\tif (bs[1].pc != 9) { return 3; };\n"
"\treturn 68;\n"
"};\n",
68, NULL },
{ "struct_ident",
"package main;\n"
"type box = struct { pc: size, matched: bool };\n"
"fn add(p: *[]box, v: size) void = {\n"
"\tlet b: box = box { pc = v, matched = true };\n"
"\tappend(*p, b);\n"
"};\n"
"export fn main() i32 = {\n"
"\tlet bs: []box = [];\n"
"\tadd(&bs, 7);\n"
"\tif (len(bs) != 1) { return 1; };\n"
"\tif (bs[0].pc != 7) { return 2; };\n"
"\tif (!bs[0].matched) { return 3; };\n"
"\treturn 69;\n"
"};\n",
69, NULL },
/* The corruption symptom row: 100 appends through the param in a
* loop with a branched callee — crosses several cap-doubling
* reallocs. Sentinels on BOTH sides of the slice local pin the
* caller frame (pre-fix cstage incremented 8(BP) and passed (BP)
* to rt_ensure → caller-frame corruption); the full readback pins
* every element across the realloc moves. */
{ "realloc_loop",
"package main;\n"
"type box = struct { pc: size, matched: bool };\n"
"fn add(p: *[]box, v: size) void = {\n"
"\tif (v % 2 == 0) {\n"
"\t\tappend(*p, box { pc = v, matched = true });\n"
"\t} else {\n"
"\t\tappend(*p, box { pc = v, matched = false });\n"
"\t};\n"
"};\n"
"export fn main() i32 = {\n"
"\tlet lo: i64 = 0x5151;\n"
"\tlet bs: []box = [];\n"
"\tlet hi: i64 = 0x7272;\n"
"\tlet i: size = 0;\n"
"\tfor (i < 100) {\n"
"\t\tadd(&bs, i);\n"
"\t\ti += 1;\n"
"\t};\n"
"\tif (len(bs) != 100) { return 1; };\n"
"\tlet j: size = 0;\n"
"\tfor (j < 100) {\n"
"\t\tif (bs[j].pc != j) { return 2; };\n"
"\t\tif (bs[j].matched != (j % 2 == 0)) { return 3; };\n"
"\t\tj += 1;\n"
"\t};\n"
"\tif (lo != 0x5151 || hi != 0x7272) { return 4; };\n"
"\treturn 70;\n"
"};\n",
70, NULL },
/* Deeper resolver spine: the header is a slice FIELD of an element
* behind a deref ((*q)[i].xs — N_DOT over N_INDEX over N_UN).
* Readback via a *holder ptr-dot, a sound read path. */
{ "deref_spine",
"package main;\n"
"type holder = struct { tag: i64, xs: []i64 };\n"
"fn addspine(q: *[]holder, i: i64, v: i64) void = {\n"
"\tappend((*q)[i].xs, v);\n"
"};\n"
"export fn main() i32 = {\n"
"\tlet hs: []holder = [];\n"
"\tlet h0: holder = holder { tag = 1, xs = [] };\n"
"\tappend(hs, h0);\n"
"\taddspine(&hs, 0, 41);\n"
"\taddspine(&hs, 0, 43);\n"
"\tlet hp: *holder = &hs[0];\n"
"\tlet g: []i64 = hp.xs;\n"
"\tif (len(g) != 2) { return 1; };\n"
"\tif (g[0] != 41 || g[1] != 43) { return 2; };\n"
"\tif (hp.tag != 1) { return 3; };\n"
"\treturn 71;\n"
"};\n",
71, NULL },
/* Ident-local targets stay with the legacy BP-disp emission (the
* resolver must never fire for them) — this row runtime-pins the
* shapes the before/after asm sweep diffed statically. */
{ "neutral_direct",
"package main;\n"
"type box = struct { pc: size, matched: bool };\n"
"export fn main() i32 = {\n"
"\tlet xs: []i64 = [];\n"
"\tappend(xs, 7);\n"
"\tappend(xs, 8, 9);\n"
"\tlet ys: []i64 = [];\n"
"\tappend(ys, xs...);\n"
"\tlet ss: []str = [];\n"
"\tappend(ss, \"hi\");\n"
"\tlet ps: []box = [];\n"
"\tappend(ps, box { pc = 1, matched = false });\n"
"\tif (len(xs) != 3 || xs[2] != 9) { return 1; };\n"
"\tif (len(ys) != 3 || ys[0] != 7) { return 2; };\n"
"\tif (len(ss) != 1 || len(ps) != 1) { return 3; };\n"
"\treturn 72;\n"
"};\n",
72, NULL },
/* Reentrancy: a nested append-through-pointer inside the outer
* append's VALUE expression (match-yield arm) spills its own
* header address. Pins the per-SITE @apphdrscr slot — a shared
* per-fn slot hands the outer's post-rt_ensure reloads the inner
* target's header and the outer element lands in the wrong
* slice (silent, both counts wrong). */
{ "reentrant_value",
"package main;\n"
"type tu = (i64 | void);\n"
"fn nest(p: *[]i64, q: *[]i64, t: tu) void = {\n"
"\tappend(*p, match (t) {\n"
"\tcase let x: i64 => {\n"
"\t\tappend(*q, 500);\n"
"\t\tyield x;\n"
"\t};\n"
"\tcase void => {\n"
"\t\tyield 0;\n"
"\t};\n"
"\t});\n"
"};\n"
"export fn main() i32 = {\n"
"\tlet a: []i64 = [];\n"
"\tlet b: []i64 = [];\n"
"\tlet t: tu = 7;\n"
"\tnest(&a, &b, t);\n"
"\tif (len(a) != 1) { return 1; };\n"
"\tif (len(b) != 1) { return 2; };\n"
"\tif (a[0] != 7) { return 3; };\n"
"\tif (b[0] != 500) { return 4; };\n"
"\treturn 33;\n"
"};\n",
33, NULL },
/* Ident-rooted dot target (h.xs through *holder): C1.5's loud
* boundary (the resolver had no ident root then; pre-FA1 this
* shape silently corrupted the frame in cstage), graduated by
* C2's cgplaceaddr N_IDENT root — the *holder base derefs once
* inside the N_DOT hop and the header place lands on &h.xs.
* Readbacks use the .len pseudo, not len(): len() of a non-tuple
* N_DOT is the pre-existing F2 enumeration gap (task #10). */
{ "identroot_dot",
"package main;\n"
"type holder = struct { tag: i64, xs: []i64 };\n"
"fn addfield(h: *holder, v: i64) void = { append(h.xs, v); };\n"
"export fn main() i32 = {\n"
"\tlet hl: holder = holder { tag = 2, xs = [] };\n"
"\taddfield(&hl, 9);\n"
"\taddfield(&hl, 11);\n"
"\tif (hl.xs[0] != 9) { return 1; };\n"
"\tif (hl.xs[1] != 11) { return 2; };\n"
"\tif (hl.xs.len != 2) { return 3; };\n"
"\tif (hl.tag != 2) { return 4; };\n"
"\treturn 73;\n"
"};\n",
73, NULL },
/* #49 (#35's single-element sibling): the struct-element append
* arm dispatched on SOURCE node kind — N_STRUCTLIT and N_IDENT
* only; every place-resolvable chain (the regex.ha:819
* result-build line `append(res, threads[best_idx].root_capture)`)
* died on the rule-7 fatal. The fix routes the remaining shapes
* through cgplaceaddr (the C1 resolver): dst slot spills to
* @appendscr, source resolves AFTER the grow, whole-width
* word-copy through DX. Five rows: indexed-field 56B capture,
* computed-index whole element, deref-spine through params,
* deref source, self-append across reallocs. */
{ "idxfield_struct",
"package main;\n"
"type capture = struct { content: str, start: size,\n"
"\tstart_bytesize: size, end: size, end_bytesize: size };\n"
"type thread = struct { pc: size, root_capture: capture,\n"
"\tmatched: bool };\n"
"export fn main() i32 = {\n"
"\tlet threads: []thread = [];\n"
"\tappend(threads, thread { pc = 7, ... });\n"
"\tappend(threads, thread { pc = 1, root_capture = capture {\n"
"\t\tcontent = \"bcd\", start = 1, start_bytesize = 2,\n"
"\t\tend = 4, end_bytesize = 5 }, ... });\n"
"\tlet res: []capture = [];\n"
"\tlet i: size = 1;\n"
"\tappend(res, threads[i].root_capture);\n"
"\tif (len(res) != 1) { return 1; };\n"
"\tif (res[0].start != 1) { return 2; };\n"
"\tif (res[0].start_bytesize != 2) { return 3; };\n"
"\tif (res[0].end != 4) { return 4; };\n"
"\tif (res[0].end_bytesize != 5) { return 5; };\n"
"\tif (res[0].content.len != 3) { return 6; };\n"
"\tif (threads[1].root_capture.end != 4) { return 7; };\n"
"\treturn 74;\n"
"};\n",
74, NULL },
{ "idx_computed_elem",
"package main;\n"
"type box = struct { pc: size, a: i64, b: i64, x: i64 };\n"
"export fn main() i32 = {\n"
"\tlet bs: []box = [];\n"
"\tappend(bs, box { pc = 1, a = 10, ... });\n"
"\tappend(bs, box { pc = 2, a = 20, ... });\n"
"\tappend(bs, box { pc = 3, a = 30, b = 7, x = 8 });\n"
"\tlet ds: []box = [];\n"
"\tlet i: i64 = 1;\n"
"\tappend(ds, bs[i + 1]);\n"
"\tif (len(ds) != 1) { return 1; };\n"
"\tif (ds[0].pc != 3) { return 2; };\n"
"\tif (ds[0].a != 30) { return 3; };\n"
"\tif (ds[0].b != 7 || ds[0].x != 8) { return 4; };\n"
"\treturn 75;\n"
"};\n",
75, NULL },
{ "derefspine_field",
"package main;\n"
"type cap = struct { s: size, e: size };\n"
"type th = struct { pc: size, rc: cap, m: bool };\n"
"fn pick(r: *[]cap, q: *[]th, i: size) void = {\n"
"\tappend(*r, (*q)[i].rc);\n"
"};\n"
"export fn main() i32 = {\n"
"\tlet ts: []th = [];\n"
"\tappend(ts, th { pc = 1, rc = cap { s = 5, e = 9 }, ... });\n"
"\tlet rs: []cap = [];\n"
"\tpick(&rs, &ts, 0);\n"
"\tif (len(rs) != 1) { return 1; };\n"
"\tif (rs[0].s != 5) { return 2; };\n"
"\tif (rs[0].e != 9) { return 3; };\n"
"\treturn 76;\n"
"};\n",
76, NULL },
{ "deref_src",
"package main;\n"
"type box = struct { pc: size, a: i64, b: i64, x: i64 };\n"
"fn addsrc(ds: *[]box, p: *box) void = { append(*ds, *p); };\n"
"export fn main() i32 = {\n"
"\tlet b: box = box { pc = 4, a = 40, b = 41, x = 42 };\n"
"\tlet ds: []box = [];\n"
"\taddsrc(&ds, &b);\n"
"\tif (len(ds) != 1) { return 1; };\n"
"\tif (ds[0].pc != 4) { return 2; };\n"
"\tif (ds[0].a != 40 || ds[0].b != 41 || ds[0].x != 42) { return 3; };\n"
"\treturn 77;\n"
"};\n",
77, NULL },
/* Self-append: the source element lives in the SLICE BEING GROWN,
* so its address is only valid post-rt_ensure — pins the
* resolve-AFTER-grow order across several cap-doubling reallocs.
* Sentinels around the header pin the frame. */
{ "self_append_realloc",
"package main;\n"
"type box = struct { pc: size, a: i64, b: i64, x: i64 };\n"
"export fn main() i32 = {\n"
"\tlet lo: i64 = 111;\n"
"\tlet bs: []box = [];\n"
"\tlet hi: i64 = 222;\n"
"\tappend(bs, box { pc = 9, a = 100, b = 200, x = 300 });\n"
"\tlet i: i32 = 0;\n"
"\tfor (i < 33) {\n"
"\t\tappend(bs, bs[i]);\n"
"\t\ti += 1;\n"
"\t};\n"
"\tif (len(bs) != 34) { return 1; };\n"
"\tif (bs[0].pc != 9 || bs[33].pc != 9) { return 2; };\n"
"\tif (bs[17].a != 100 || bs[33].b != 200) { return 3; };\n"
"\tif (bs[33].x != 300) { return 4; };\n"
"\tif (lo != 111 || hi != 222) { return 5; };\n"
"\treturn 78;\n"
"};\n",
78, NULL },
/* The #49 ruling's order pin, positive semantics row (converged
* with Hare, not a documented divergence): the source chain's
* rvalues — here a CALLED index helper reading the slice's len —
* evaluate exactly ONCE, PRE-grow. calls != 1 catches a
* double-evaluation; bs[2] != old-last catches a post-grow
* resolution (the helper would see the bumped len and return the
* uninitialized new slot — the pre-split emission failed exactly
* there, exit 3). */
{ "selfidx_oldlen_once",
"package main;\n"
"type box = struct { pc: i64, a: i64 };\n"
"let calls: i64 = 0;\n"
"fn lastidx(xs: []box) i64 = {\n"
"\tcalls += 1;\n"
"\tlet n: i64 = len(xs): i64;\n"
"\treturn n - 1;\n"
"};\n"
"export fn main() i32 = {\n"
"\tlet bs: []box = [];\n"
"\tappend(bs, box { pc = 11, a = 1 });\n"
"\tappend(bs, box { pc = 4242, a = 2 });\n"
"\tappend(bs, bs[lastidx(bs)]);\n"
"\tif (calls != 1) { return 1; };\n"
"\tif (len(bs) != 3) { return 2; };\n"
"\tif (bs[2].pc != 4242 || bs[2].a != 2) { return 3; };\n"
"\tif (bs[0].pc != 11) { return 4; };\n"
"\treturn 80;\n"
"};\n",
80, NULL },
/* Element-kind × place-source matrix: struct was the ONLY kind
* the source-shape dispatch rejected — scalar (wide + narrow),
* str-header, slice-header and tagged sources route through the
* pre-existing cgexpr-based arms and already worked at 796d41b.
* This row is the regression net pinning that matrix (runtime +
* byte-id, dual-driver), not a fix pin. */
{ "elem_kinds_place",
"package main;\n"
"type box = struct { n: i64, m: i32, name: str, xs: []u8 };\n"
"type tv = (i64 | void);\n"
"export fn main() i32 = {\n"
"\tlet inner: []u8 = [];\n"
"\tappend(inner, 7u8);\n"
"\tappend(inner, 8u8);\n"
"\tlet bs: []box = [];\n"
"\tappend(bs, box { n = 41, m = 9, name = \"hello\", xs = inner });\n"
"\tlet ns: []i64 = [];\n"
"\tappend(ns, bs[0].n);\n"
"\tlet ms: []i32 = [];\n"
"\tappend(ms, bs[0].m);\n"
"\tlet ss: []str = [];\n"
"\tappend(ss, bs[0].name);\n"
"\tlet vs: [][]u8 = [];\n"
"\tappend(vs, bs[0].xs);\n"
"\tlet ts: []tv = [];\n"
"\tappend(ts, bs[0].n);\n"
"\tif (len(ns) != 1 || ns[0] != 41) { return 1; };\n"
"\tif (len(ms) != 1 || ms[0] != 9) { return 2; };\n"
"\tif (len(ss) != 1 || len(ss[0]) != 5) { return 3; };\n"
"\tif (len(vs) != 1 || len(vs[0]) != 2) { return 4; };\n"
"\tif (vs[0][1] != 8u8) { return 5; };\n"
"\tmatch (ts[0]) {\n"
"\tcase let v: i64 => { if (v != 41) { return 6; }; };\n"
"\tcase void => { return 7; };\n"
"\t};\n"
"\treturn 79;\n"
"};\n",
79, NULL },
/* #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 = &empty;\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, "#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
* route lands. */
{ "reject_call_src",
"package main;\n"
"type box = struct { pc: size, a: i64 };\n"
"fn mk() box = { return box { pc = 1, a = 2 }; };\n"
"export fn main() i32 = {\n"
"\tlet bs: []box = [];\n"
"\tappend(bs, mk());\n"
"\treturn 0;\n"
"};\n",
BUILD_FAIL, "#34: append() struct element source shape unsupported (rule-7)" },
};
/* errlog_has — the build-failure stderr must carry the row's expected
* diagnostic; any other failure (parse error, crash) is a vacuous
* reject and must not pass. */
static int
errlog_has(const char *path, const char *needle)
{
FILE *f = fopen(path, "rb");
if (!f) return 0;
char buf[8192];
size_t got = fread(buf, 1, sizeof buf - 1, f);
fclose(f);
buf[got] = '\0';
return strstr(buf, needle) != NULL;
}
static int
run_driver(const char *driver, const struct row *r, int i)
{
char src[64], tmpdir[64], errlog[80], cmd[1200];
snprintf(src, sizeof src, "/tmp/applp_%d_%d.ww", getpid(), i);
snprintf(tmpdir, sizeof tmpdir, "/tmp/applp_%d_d_%d", getpid(), i);
snprintf(errlog, sizeof errlog, "%s.err", src);
FILE *f = fopen(src, "wb");
if (!f) return -1;
fputs(r->src, f);
fclose(f);
mkdir(tmpdir, 0755);
snprintf(cmd, sizeof cmd, "cd %s && %s build %s 2>%s",
tmpdir, driver, src, errlog);
if (runwait(cmd) != 0) {
int rc = -1;
if (r->want != BUILD_FAIL) {
fprintf(stderr, "row[%s]: build via %s failed\n",
r->label, driver);
} else if (r->expect_err &&
!errlog_has(errlog, r->expect_err)) {
fprintf(stderr, "row[%s]: %s build failed without "
"expected diagnostic \"%s\"\n",
r->label, driver, r->expect_err);
rc = -3; /* failed, but for the wrong reason */
}
unlink(src); unlink(errlog); rmdir(tmpdir);
return rc;
}
const char *base = strrchr(src, '/');
base = base ? base + 1 : src;
char outbin[128];
snprintf(outbin, sizeof outbin, "%s/%s", tmpdir, base);
char *dot = strrchr(outbin, '.');
if (dot && strcmp(dot, ".ww") == 0) *dot = '\0';
int got = runwait(outbin);
unlink(src); unlink(errlog); unlink(outbin); rmdir(tmpdir);
return got;
}
/* asm_byte_identical — generate .s via cstage's w6c and wwstage's
* w6c_ww and diff. The header-place emission is written fresh on both
* sides, so this is the converged-by-construction gate: any drift in
* the resolve/spill/reload sequence shows here. */
static int
asm_byte_identical(const char *bin, const struct row *r, int i)
{
char src[64], cs[64], ws[64], cmd[1024];
snprintf(src, sizeof src, "/tmp/applp_asm_%d_%d.ww", getpid(), i);
snprintf(cs, sizeof cs, "/tmp/applp_asm_%d_%d_c.s", getpid(), i);
snprintf(ws, sizeof ws, "/tmp/applp_asm_%d_%d_w.s", getpid(), i);
FILE *f = fopen(src, "wb");
if (!f) return -1;
fputs(r->src, f);
fclose(f);
snprintf(cmd, sizeof cmd, "%s/w6c -o %s %s 2>/dev/null", bin, cs, src);
if (runwait(cmd) != 0) {
fprintf(stderr, "row[%s]: w6c errored\n", r->label);
unlink(src);
return -1;
}
snprintf(cmd, sizeof cmd, "%s/w6c_ww -o %s %s 2>/dev/null",
bin, ws, src);
if (runwait(cmd) != 0) {
fprintf(stderr, "row[%s]: w6c_ww errored\n", r->label);
unlink(src); unlink(cs);
return -1;
}
FILE *fc = fopen(cs, "rb");
FILE *fw = fopen(ws, "rb");
int rc = 0;
if (!fc || !fw) {
rc = -1;
} else {
for (;;) {
int a = fgetc(fc);
int b = fgetc(fw);
if (a != b) { rc = -1; break; }
if (a == EOF) break;
}
}
if (fc) fclose(fc);
if (fw) fclose(fw);
if (rc != 0)
fprintf(stderr, "row[%s]: cstage vs wwstage asm differs\n",
r->label);
unlink(src); unlink(cs); unlink(ws);
return rc;
}
int
main(void)
{
const char *bin = getenv("BIN");
if (!bin) bin = "out/bin";
char absbin[2080];
if (bin[0] != '/') {
char cwd[1024];
if (getcwd(cwd, sizeof cwd) == NULL) return 1;
snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin);
bin = absbin;
}
char cdrv[2120];
snprintf(cdrv, sizeof cdrv, "%s/ww", bin);
char wdrv[2120];
snprintf(wdrv, sizeof wdrv, "%s/ww_ww", bin);
struct { const char *name; const char *path; int gated_on_existence; }
drivers[] = {
{ "cstage", cdrv, 0 },
{ "wwstage", wdrv, 1 },
{ NULL, NULL, 0 },
};
int n = (int)(sizeof rows / sizeof rows[0]);
int total = 0, fail = 0;
for (int d = 0; drivers[d].name; d++) {
if (drivers[d].gated_on_existence
&& access(drivers[d].path, X_OK) != 0) {
fprintf(stderr, "append_place: skip %s (no %s)\n",
drivers[d].name, drivers[d].path);
continue;
}
for (int i = 0; i < n; i++) {
int got = run_driver(drivers[d].path, &rows[i], i);
total++;
int bad = rows[i].want == BUILD_FAIL
? (got != -1) : (got != rows[i].want);
if (bad) {
fprintf(stderr,
"append_place[%s][%s]: exit=%d want=%d\n",
drivers[d].name, rows[i].label,
got, rows[i].want);
fail++;
}
}
}
if (access(wdrv, X_OK) == 0) {
for (int i = 0; i < n; i++) {
if (rows[i].want == BUILD_FAIL)
continue;
total++;
if (asm_byte_identical(bin, &rows[i], i) != 0)
fail++;
}
}
if (fail) {
fprintf(stderr,
"append_place: %d/%d fixtures failed\n", fail, total);
return 1;
}
printf("append_place: %d fixtures passed\n", total);
return 0;
}