Files
ww/test/wcc/806_append_place.c
Hojun-Cho c34a48a81f wcc+w6c_ww: append() struct-element sources via split place-resolve (#49)
#49 (#35's single-element sibling, tranche-C pre-check PC2): the
struct-element append arm dispatched on SOURCE node kind — N_STRUCTLIT
(literal fill) and N_IDENT (local word-copy) only; every
place-resolvable chain died on the rule-7 fatal in BOTH stages,
including search()'s result-build line
`append(res, threads[best_idx].root_capture)` (regex.ha:819).

Wire those shapes with a SPLIT resolve around the grow (the #49
ruling): the chain's rvalues — deref-root pointer expr, index expr —
evaluate exactly once PRE-grow into @appendsroot/@appendsoff (an index
reading the slice header sees the pre-append len, Hare's argument
order), then only the BASE re-derives POST-grow from the live storage
and the stashed offsets land back on top, so a self-append source
re-roots in the post-realloc buffer. harec resolves an aggregate
source address wholly PRE-grow (gen.c: gen_load returns the address
for STORAGE_STRUCT, gen_store copies after rt.ensure) — a
use-after-free under a reclaiming allocator; per #263 we align to the
runtime-correct side, not the reference. A pointer ALIASING the grown
buffer keeps Hare's own stale-base hole (sound today only because
rt/malloc.ww never reclaims). Supported shapes are bounded: root
(local/global ident | deref) + at most one index + trailing direct
fields; all else stays on the #34 fatal, including CALL rvalues (the
#42-style bound, new reject row pins the text in both stages). The
N_STRUCTLIT/N_IDENT fast-paths keep their emission byte-identical.

806_append_place grows eight rows: indexed-field 56B capture (the
ha:819 shape, header readback), computed-index whole element,
deref-spine param pair, deref source, self-append ×33 crossing three
cap-doubling reallocs, the split-order semantics pin (a CALLED index
helper reading len must run once and see the PRE-grow len — the
pre-split emission failed exactly there), an element-kind ×
place-source matrix row (scalar/narrow/str/slice/tagged route via the
pre-existing arms — regression net), and the CALL-source reject. The
six fix rows verified FAILING against a pristine 796d41b build on
both drivers (loud #34 fatal, identically in cstage and w6c_ww —
there was no silent path at master); 70/70 fixtures green here
including per-row cs/ww asm byte-cmp.

Unblocks regex fold-2b tranche C (search) — PC2 was the lone
pre-check failure; PC1/PC3/PC4 passed at base.
2026-06-04 14:37:24 +09:00

811 lines
28 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) |
* reject_spread_src | non-ident spread SOURCE through a deref | BUILD_FAIL
* | target (the #35 designed boundary) |
* 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 },
/* 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",
"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 ys: []i64 = [];\n"
"\tlet hs: []holder = [];\n"
"\tdup(&ys, &hs, 0);\n"
"\treturn 0;\n"
"};\n",
BUILD_FAIL, "#34: 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;
}