Files
ww/test/wcc/806_append_place.c
Hojun-Cho ce3a25a0b4 test: contain sepwork scratch per-driver tmpdir, fix /tmp+in-repo leak (#8)
The wcc test drivers ran `ww build <bare-/tmp src>` with no -o, so the
compiler's <stem>.sepwork scratch landed beside the source and was never
cleaned: unbounded /tmp growth (2195 stale dirs observed) that fills tmpfs
and fabricates phantom test failures + silent harness aborts, and for
in-repo fixture builds leaked .sepwork into the tracked tree.

Each leaking build now writes its source + output inside a per-invocation
tmpdir, passes -o <tmpdir>/<stem> so the .sepwork lands inside it, and
rm -rf's the tmpdir on every exit path -- including fopen-fail and the
expected-fail reject builds (scratch is mkdir'd before the build can fail).
`ww run` and explicit-`-o`/byte-id helpers are left as-is; the 990/993
byte-id comparison logic is byte-for-byte unchanged.

Two items filed separately (this commit holds the no-Makefile / no-main.c
rail):
- #13: a stale <src>.s byte-id readback (749) silently no-ops since
  separate-compile emits .s to <ostem>.sepwork/__root.s; documented inline.
- #14: build-system Makefile recipes build selfhost/cmd/*/main.ww with no
  -o and leak main.sepwork in-tree (bounded, gitignored; own commit).

One concern -- sepwork leak hygiene -- across 228 drivers; uniform
transform applied per-file and two-round reviewed. make test: all 402
passed, zero net-new /tmp scratch, zero test-driven in-repo .sepwork.
2026-06-22 23:29:39 +09:00

968 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 tmpdir[64], src[128], outbin[128], errlog[160];
char rmcmd[160], cmd[1200];
snprintf(tmpdir, sizeof tmpdir, "/tmp/applp_%d_d_%d", getpid(), i);
mkdir(tmpdir, 0755);
snprintf(src, sizeof src, "%s/applp_%d_%d.ww", tmpdir, getpid(), i);
snprintf(outbin, sizeof outbin, "%s/applp_%d_%d", tmpdir, getpid(), i);
snprintf(errlog, sizeof errlog, "%s/err", tmpdir);
snprintf(rmcmd, sizeof rmcmd, "rm -rf %s", tmpdir);
FILE *f = fopen(src, "wb");
if (!f) { runwait(rmcmd); return -1; }
fputs(r->src, f);
fclose(f);
snprintf(cmd, sizeof cmd, "%s build -o %s %s 2>%s",
driver, outbin, 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 */
}
runwait(rmcmd);
return rc;
}
int got = runwait(outbin);
runwait(rmcmd);
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;
}