getopt's deleted appendoption helper fed rt_ensure a hardcoded
membsz=24, stale since str went 24B (option {rune,str} = 32B): the
8-slot first grow allocated 192B while writes strode 32 — OOB past 6
options. The builtin derives 32 from the type table (MOVQ $32, SI);
this row appends 8 option literals and reads element 7 back full-width
so the class stays pinned. No in-tree test parsed >3 options.
497 lines
16 KiB
C
497 lines
16 KiB
C
/*
|
|
* 800_append_wide_elem — cstage and wwstage agree, byte-for-byte and at
|
|
* runtime, that `append(s, v)` stores the FULL element width for every
|
|
* element kind (task #34; the regex fold-2a blocker).
|
|
*
|
|
* The bug: both stages lowered the append element store as ONE sized
|
|
* mov from AX (`MOV* AX, (BX)`) — correct only for scalars <= 8B. A
|
|
* str/slice element (24B {ptr,len,cap}, cgexpr -> AX/BX/CX) kept only
|
|
* .ptr (byte-id-BLIND: both stages identical and identically wrong); a
|
|
* tagged element got the raw payload written into the tag slot (no
|
|
* boxing — the #12 pathology); a struct element kept only its first
|
|
* qword. wwstage ADDITIONALLY fed rt_ensure membsz from bare
|
|
* elemsizeof, whose 8-sentinel under-allocated AND mis-strided named
|
|
* tagged/struct elements (the #8 family; cs!=ww on the `MOVQ $N, SI`
|
|
* line and the stride IMUL).
|
|
*
|
|
* The fix (BOTH stages, converged byte-identical), keyed on the
|
|
* DECLARED slice local's element type (cstage su->sub; wwstage stamped
|
|
* tinfo via elemsizeofc — NEVER the value node, the #25/#31 esz=0
|
|
* trap):
|
|
* - scalar 1/2/4/8: UNTOUCHED (the u8 row's asm is unchanged vs
|
|
* pre-fix master — regression-pinned by byte-id + runtime).
|
|
* - str/slice: push AX/BX/CX across rt_ensure, dst in DX (BX holds
|
|
* the element .len after the pops — the #24 register discipline),
|
|
* 3-word store at (DX)/8(DX)/16(DX).
|
|
* - tagged: grow FIRST, dst -> BX, then the #12 widen choke-point
|
|
* (cg_widen_tagged_store / cgwidentaggedstore) boxes {tag,payload}
|
|
* through @tagbase/@tagscr (via_outer mode).
|
|
* - struct: grow first; literal -> dst spilled to @appendscr +
|
|
* structlit fill (DST_PTR_LOCAL); local ident -> word-copy; ANY
|
|
* other source shape is a rule-7 loud-stop (build fails), never a
|
|
* silent scalar fall-through.
|
|
* - spread `append(s, items...)`: the source element is already a
|
|
* fully-formed T (tag included), so the wide arm grows first and
|
|
* whole-width word-copies &items[i] -> dst, recomputing both
|
|
* addresses from the slice headers after the (possibly
|
|
* reallocating) rt_ensure.
|
|
*
|
|
* row | shape | want
|
|
* ---------------------+--------------------------------------+------
|
|
* u8_baseline | []u8, two appends, sum | 8
|
|
* enum_alias_esz | []ek (enum i32 alias) — pins the | 5
|
|
* | elemsizeofc swap: wwstage fed SI=$8 |
|
|
* | where cstage fed $4 (cs!=ww growth). |
|
|
* tagged_box | [](i64|bool), append 100i64, match | 100
|
|
* | readback. Pre-fix: raw 100 landed in |
|
|
* | the tag slot, no arm matched. |
|
|
* tagged_two_append | two appends — element 0 survives the | 42
|
|
* | realloc full-width. |
|
|
* str_len_bytes | []str, append, len + byte readback | 11
|
|
* str_two_append | two str appends, len0*10 + len1 | 32
|
|
* slice_elem | [][]u8, append, len + byte readback | 12
|
|
* struct_first8 | []pt (16B), x+y (first qword — ken's | 8
|
|
* | characterization row) |
|
|
* struct_tail_word | []pt, z at +8 (the word the 1-word | 8
|
|
* | store dropped) |
|
|
* struct_lit_two | two struct-LITERAL appends (pins the | 7
|
|
* | @appendscr per-fn dedup: frame + |
|
|
* | byte-id diverge if cstage allocs two |
|
|
* | scratch slots where wwstage dedups) |
|
|
* spread_str | append(ys, xs...) of []str + single | 7
|
|
* spread_tagged | append(ys, xs...) of [](i64|bool) | 42
|
|
* tagged_ident_src | append of ALREADY-TAGGED locals | 42
|
|
* | (i64 + bool members) — the widener's |
|
|
* | already-tagged source path. |
|
|
* struct_eight_appends | 8 struct-lit appends of the getopt | 15
|
|
* | option shape ({rune,str} = 32B), |
|
|
* | full-width readback of element 7 — |
|
|
* | the >6-element OOB regression pin |
|
|
* | (getopt's deleted helper fed a stale |
|
|
* | hardcoded membsz=24: 8-slot grow was |
|
|
* | 192B while writes strode 32; the |
|
|
* | builtin derives 32 from the type |
|
|
* | table, MOVQ $32, SI). |
|
|
* struct_call_loudstop | append(xs, f()) struct-from-call is | BUILD_FAIL
|
|
* | the deferred source shape — must |
|
|
* | fail LOUD on both stages (rule-7), |
|
|
* | never silently store one word. |
|
|
* spread_call_loudstop | append(ys, f()...) non-ident spread | BUILD_FAIL
|
|
* | source — pre-review this fell PAST |
|
|
* | the spread arm (cstage garbage store |
|
|
* | vs wwstage silent skip, divergent); |
|
|
* | now a rule-7 loud-stop (task #37). |
|
|
*
|
|
* Every non-BUILD_FAIL row also asserts cstage/wwstage asm byte-id,
|
|
* which subsumes the frame-size canary (TEXT main,$N) — the @tagbase/
|
|
* @tagscr/@appendscr scratch allocations must land in the same order
|
|
* and size on both stages (#25/#31 lesson).
|
|
*/
|
|
#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 (the
|
|
* rule-7 loud-stop for a deferred source shape). run_driver returns -1
|
|
* exactly when the build fails, so `got == -1` is the pass. */
|
|
#define BUILD_FAIL (-2147483647 - 1)
|
|
|
|
struct row { const char *label; const char *src; int want; };
|
|
|
|
static const struct row rows[] = {
|
|
{ "u8_baseline",
|
|
"package main;\n"
|
|
"export fn main() i32 = {\n"
|
|
"\tlet xs: []u8 = [];\n"
|
|
"\tappend(xs, 5u8);\n"
|
|
"\tappend(xs, 3u8);\n"
|
|
"\treturn (xs[0] + xs[1]): i32;\n"
|
|
"};\n",
|
|
8 },
|
|
|
|
{ "enum_alias_esz",
|
|
"package main;\n"
|
|
"type ek = enum { A = 5, B = 9 };\n"
|
|
"export fn main() i32 = {\n"
|
|
"\tlet xs: []ek = [];\n"
|
|
"\tappend(xs, ek.A);\n"
|
|
"\treturn xs[0]: i32;\n"
|
|
"};\n",
|
|
5 },
|
|
|
|
{ "tagged_box",
|
|
"package main;\n"
|
|
"type cell = (i64 | bool);\n"
|
|
"export fn main() i32 = {\n"
|
|
"\tlet xs: []cell = [];\n"
|
|
"\tappend(xs, 100i64);\n"
|
|
"\tlet r: i32 = 1;\n"
|
|
"\tmatch (xs[0]) {\n"
|
|
"\tcase let v: i64 => r = v: i32;\n"
|
|
"\tcase bool => r = 2;\n"
|
|
"\t};\n"
|
|
"\treturn r;\n"
|
|
"};\n",
|
|
100 },
|
|
|
|
{ "tagged_two_append",
|
|
"package main;\n"
|
|
"type cell = (i64 | bool);\n"
|
|
"export fn main() i32 = {\n"
|
|
"\tlet xs: []cell = [];\n"
|
|
"\tappend(xs, 40i64);\n"
|
|
"\tappend(xs, 2i64);\n"
|
|
"\tlet r: i32 = 0;\n"
|
|
"\tmatch (xs[0]) {\n"
|
|
"\tcase let v: i64 => r += v: i32;\n"
|
|
"\tcase bool => r = 99;\n"
|
|
"\t};\n"
|
|
"\tmatch (xs[1]) {\n"
|
|
"\tcase let v: i64 => r += v: i32;\n"
|
|
"\tcase bool => r = 98;\n"
|
|
"\t};\n"
|
|
"\treturn r;\n"
|
|
"};\n",
|
|
42 },
|
|
|
|
{ "str_len_bytes",
|
|
"package main;\n"
|
|
"export fn main() i32 = {\n"
|
|
"\tlet xs: []str = [];\n"
|
|
"\tlet s: str = \"abcdef\";\n"
|
|
"\tappend(xs, s);\n"
|
|
"\treturn (xs[0].len: i32) + (xs[0][5]: i32) - (xs[0][0]: i32);\n"
|
|
"};\n",
|
|
11 },
|
|
|
|
{ "str_two_append",
|
|
"package main;\n"
|
|
"export fn main() i32 = {\n"
|
|
"\tlet xs: []str = [];\n"
|
|
"\tlet a: str = \"abc\";\n"
|
|
"\tlet b: str = \"fg\";\n"
|
|
"\tappend(xs, a);\n"
|
|
"\tappend(xs, b);\n"
|
|
"\treturn (xs[0].len * 10 + xs[1].len): i32;\n"
|
|
"};\n",
|
|
32 },
|
|
|
|
{ "slice_elem",
|
|
"package main;\n"
|
|
"export fn main() i32 = {\n"
|
|
"\tlet hb: [4]u8; hb[0] = 9u8; hb[1] = 4u8;\n"
|
|
"\tlet a: []u8; a.ptr = &hb[0]; a.len = 3; a.cap = 4;\n"
|
|
"\tlet ys: [][]u8 = [];\n"
|
|
"\tappend(ys, a);\n"
|
|
"\treturn (ys[0].len: i32) + (ys[0][0]: i32);\n"
|
|
"};\n",
|
|
12 },
|
|
|
|
{ "struct_first8",
|
|
"package main;\n"
|
|
"type pt = struct { x: i32, y: i32, z: i64 };\n"
|
|
"export fn main() i32 = {\n"
|
|
"\tlet xs: []pt = [];\n"
|
|
"\tlet p: pt = pt { x = 3, y = 5, z = 9 };\n"
|
|
"\tappend(xs, p);\n"
|
|
"\treturn (xs[0].x + xs[0].y): i32;\n"
|
|
"};\n",
|
|
8 },
|
|
|
|
{ "struct_tail_word",
|
|
"package main;\n"
|
|
"type pt = struct { x: i32, y: i32, z: i64 };\n"
|
|
"export fn main() i32 = {\n"
|
|
"\tlet xs: []pt = [];\n"
|
|
"\tlet p: pt = pt { x = 3, y = 5, z = 9 };\n"
|
|
"\tappend(xs, p);\n"
|
|
"\treturn (xs[0].z - 1): i32;\n"
|
|
"};\n",
|
|
8 },
|
|
|
|
{ "struct_lit_two",
|
|
"package main;\n"
|
|
"type pt = struct { x: i32, y: i32, z: i64 };\n"
|
|
"export fn main() i32 = {\n"
|
|
"\tlet xs: []pt = [];\n"
|
|
"\tappend(xs, pt { x = 1, y = 2, z = 5 });\n"
|
|
"\tappend(xs, pt { x = 3, y = 4, z = 7 });\n"
|
|
"\treturn (xs[0].z + xs[1].z): i32 - xs[0].x - xs[1].y;\n"
|
|
"};\n",
|
|
7 },
|
|
|
|
{ "spread_str",
|
|
"package main;\n"
|
|
"export fn main() i32 = {\n"
|
|
"\tlet xs: []str = [];\n"
|
|
"\tlet a: str = \"abc\";\n"
|
|
"\tlet b: str = \"fg\";\n"
|
|
"\tappend(xs, a);\n"
|
|
"\tlet ys: []str = [];\n"
|
|
"\tappend(ys, xs...);\n"
|
|
"\tappend(ys, b);\n"
|
|
"\treturn (ys[0].len + ys[1].len + ys.len): i32;\n"
|
|
"};\n",
|
|
7 },
|
|
|
|
{ "spread_tagged",
|
|
"package main;\n"
|
|
"type cell = (i64 | bool);\n"
|
|
"export fn main() i32 = {\n"
|
|
"\tlet xs: []cell = [];\n"
|
|
"\tappend(xs, 40i64);\n"
|
|
"\tappend(xs, 2i64);\n"
|
|
"\tlet ys: []cell = [];\n"
|
|
"\tappend(ys, xs...);\n"
|
|
"\tlet r: i32 = 0;\n"
|
|
"\tmatch (ys[0]) {\n"
|
|
"\tcase let v: i64 => r += v: i32;\n"
|
|
"\tcase bool => r = 99;\n"
|
|
"\t};\n"
|
|
"\tmatch (ys[1]) {\n"
|
|
"\tcase let v: i64 => r += v: i32;\n"
|
|
"\tcase bool => r = 98;\n"
|
|
"\t};\n"
|
|
"\treturn r;\n"
|
|
"};\n",
|
|
42 },
|
|
|
|
{ "tagged_ident_src",
|
|
"package main;\n"
|
|
"type cell = (i64 | bool);\n"
|
|
"export fn main() i32 = {\n"
|
|
"\tlet xs: []cell = [];\n"
|
|
"\tlet c: cell = 40i64;\n"
|
|
"\tlet b: cell = true;\n"
|
|
"\tappend(xs, c);\n"
|
|
"\tappend(xs, b);\n"
|
|
"\tlet r: i32 = 0;\n"
|
|
"\tmatch (xs[0]) {\n"
|
|
"\tcase let v: i64 => r += v: i32;\n"
|
|
"\tcase bool => r = 99;\n"
|
|
"\t};\n"
|
|
"\tmatch (xs[1]) {\n"
|
|
"\tcase i64 => r = 98;\n"
|
|
"\tcase let w: bool => { if (w) { r += 2; }; };\n"
|
|
"\t};\n"
|
|
"\treturn r;\n"
|
|
"};\n",
|
|
42 },
|
|
|
|
{ "struct_eight_appends",
|
|
"package main;\n"
|
|
"type option = struct { flag: rune, value: str };\n"
|
|
"export fn main() i32 = {\n"
|
|
"\tlet opts: []option = [];\n"
|
|
"\tappend(opts, option { flag = 'a', value = \"v0\" });\n"
|
|
"\tappend(opts, option { flag = 'b', value = \"v1\" });\n"
|
|
"\tappend(opts, option { flag = 'c', value = \"v2\" });\n"
|
|
"\tappend(opts, option { flag = 'd', value = \"v3\" });\n"
|
|
"\tappend(opts, option { flag = 'e', value = \"v4\" });\n"
|
|
"\tappend(opts, option { flag = 'f', value = \"v5\" });\n"
|
|
"\tappend(opts, option { flag = 'g', value = \"v6\" });\n"
|
|
"\tappend(opts, option { flag = 'h', value = \"seventh\" });\n"
|
|
"\treturn (opts[7].value.len + opts.len): i32;\n"
|
|
"};\n",
|
|
15 },
|
|
|
|
{ "struct_call_loudstop",
|
|
"package main;\n"
|
|
"type pt = struct { x: i32, y: i32, z: i64 };\n"
|
|
"fn mk() pt = {\n"
|
|
"\treturn pt { x = 1, y = 2, z = 3 };\n"
|
|
"};\n"
|
|
"export fn main() i32 = {\n"
|
|
"\tlet xs: []pt = [];\n"
|
|
"\tappend(xs, mk());\n"
|
|
"\treturn 0;\n"
|
|
"};\n",
|
|
BUILD_FAIL },
|
|
|
|
{ "spread_call_loudstop",
|
|
"package main;\n"
|
|
"fn mks() []str = {\n"
|
|
"\tlet xs: []str = [];\n"
|
|
"\tlet a: str = \"abc\";\n"
|
|
"\tappend(xs, a);\n"
|
|
"\treturn xs;\n"
|
|
"};\n"
|
|
"export fn main() i32 = {\n"
|
|
"\tlet ys: []str = [];\n"
|
|
"\tappend(ys, mks()...);\n"
|
|
"\treturn ys[0].len: i32;\n"
|
|
"};\n",
|
|
BUILD_FAIL },
|
|
};
|
|
|
|
static int
|
|
run_driver(const char *driver, const struct row *r, int i)
|
|
{
|
|
char src[64], tmpdir[64], cmd[1024];
|
|
snprintf(src, sizeof src, "/tmp/apwe_%d_%d.ww", getpid(), i);
|
|
snprintf(tmpdir, sizeof tmpdir, "/tmp/apwe_%d_d_%d", getpid(), i);
|
|
|
|
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>/dev/null",
|
|
tmpdir, driver, src);
|
|
if (runwait(cmd) != 0) {
|
|
if (r->want != BUILD_FAIL)
|
|
fprintf(stderr, "row[%s]: build via %s failed\n",
|
|
r->label, driver);
|
|
unlink(src); rmdir(tmpdir);
|
|
return -1;
|
|
}
|
|
|
|
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(outbin); rmdir(tmpdir);
|
|
return got;
|
|
}
|
|
|
|
/* asm_byte_identical — generate .s via cstage's w6c and wwstage's
|
|
* w6c_ww and diff. Subsumes the frame-size canary: a scratch-order or
|
|
* esz divergence shows up on the TEXT main,$N line or the MOVQ $N, SI
|
|
* line. Pre-fix the tagged/struct rows differed (wwstage SI=$8 vs $16);
|
|
* the str rows were byte-id-blind (both wrong identically), which the
|
|
* runtime rows above catch. */
|
|
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/apwe_asm_%d_%d.ww", getpid(), i);
|
|
snprintf(cs, sizeof cs, "/tmp/apwe_asm_%d_%d_c.s", getpid(), i);
|
|
snprintf(ws, sizeof ws, "/tmp/apwe_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[1024];
|
|
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[1024];
|
|
snprintf(cdrv, sizeof cdrv, "%s/ww", bin);
|
|
char wdrv[1024];
|
|
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_wide_elem: 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_wide_elem[%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_wide_elem: %d/%d fixtures failed\n", fail, total);
|
|
return 1;
|
|
}
|
|
printf("append_wide_elem: %d fixtures passed\n", total);
|
|
return 0;
|
|
}
|