w6c+w6c_ww: tagged sret for slot>32B returns (fix #38)

A tagged-union RETURN rides a fixed AX(tag)+DX/CX/R8 cursor (TUPLE_GPCAP
eightbytes = 32B slot); wider slots were silently truncated at the
return crossing — payload word 4+ built in the callee frame and died
there, byte-identical on both stages (gate-blind). Blocks regex fold-2a
((regex | error | nomem) = 64B slot).

Classifier: cg_sret_retsize / sretretsize gain a TY_TAGGED arm
(<= TUPLE_GPCAP*8 stays register-ABI — the (str|nomem)/(s3|bool) 32B
boundary class is pinned unchanged byte-for-byte vs master). Callee:
cgreturn writes the slot through *(@sretarg) via the existing widener
non-BP base (bare return stores the void tag); exact-type 'return f();'
rides the #9 sret-forward. Receive: let/assign/discard reuse the
generic #23/#10 sret protocol; the match scrutinee passes its spill
slot as the sret dest (tagged-specific, no tuple precedent).

This could NOT land as a gate-first interim loud-stop (the planned
#38a): lib/errors/errors.ww errno() already returns a 40B
(errors.error) slot in-tree — the cgenstmt.ww-documented #222 latent —
so a bare gate breaks the build. errno graduates to sret here instead;
errnotest pins it at runtime (its cstage run; the wwstage run was
already failing at master via an unrelated pre-existing indirect-call
arg-classification divergence, reported separately) and test/926's
errno-shaped row reads the previously-dropped tail word on both stages.

The unwired cursor consumers of an sret-class call result loud-stop
(rule 7) rather than read a cursor the callee no longer fills:
widening forward/receive ((A|B)->(A|B|C) mem-to-mem tag-remap, filed
#40), ?/!/is/as operands, argument position, and the >48B tagged-arg
class both stages previously mishandled silently. One-class-one-commit
per the #133 carve-out: post-flip those consumers would read AX (now
the dest pointer) as the tag — a gates-trailing commit would leave a
silently-wrong bisect point, so the flip and its gates are not
separable.

test/926: 15 rows — 56B regex-shaped round-trips (literal/local/
assign/match-scrutinee/forward/str-variant/multi-call), 40B repro +
bare-return-void, the errno-shaped tail-read graduation row, 32B
boundary rows pinned register-ABI by asm sentinel, and 3 loud-stop
rows pinned as build failures on both stages.
This commit is contained in:
2026-06-04 03:43:44 +09:00
parent 5f15eb3d09
commit 4f3967835e
8 changed files with 1660 additions and 22 deletions

View File

@@ -0,0 +1,516 @@
/*
* 926_tagged_sret_run — tagged-union sret returns (#38b): a tagged
* RETURN whose slot exceeds the AX/DX/CX/R8 register cursor
* (TUPLE_GPCAP eightbytes = 32B; tag + 3 payload words) routes
* through the SysV sret discipline instead of silently truncating
* payload word 4+ in the callee frame.
*
* Pre-#38 both stages emitted the SAME truncating cursor loads
* (gate-blind byte-id): the regex-shaped 56B payload (64B slot)
* died at the return crossing while locals, args, and the widener's
* memory stores were all correct. errors.errno's 40B (errors.error)
* slot was the latent in-tree instance — benign only because no
* consumer read opaque_data word 2.
*
* Three checks per row:
* - byte-id: w6c vs w6c_ww .s must be identical (rule 10).
* - sret-presence: rows flagged `sret` must emit the hidden-RDI
* `LEAQ <K>(BP), DI` caller dest; boundary rows (slot EXACTLY
* 32B — the (str|nomem)/(s3|bool) class) must NOT — an
* off-by-one in the classifier flips every (T|nomem) consumer
* in the tree (ken's top #38 risk).
* - runtime: build via the ww / ww_ww drivers and run; the exit
* code pins every payload word INCLUDING the last (the
* truncation signature).
* Rows flagged `buildfail` must be REJECTED by both stages (the
* rule-7 loud-stops over the unwired #40-family shapes: widening
* sret forward, `?`/arg-position consumption of an sret-class call
* result).
*/
#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;
}
struct row {
const char *label;
const char *src;
int want; /* expected exit code (run rows) */
int sret; /* 1: .s must contain LEAQ..DI; 0: must not */
int buildfail; /* 1: both stages must reject (loud-stop) */
};
/* Shared regex-shaped prelude: 56B payload (two slices + i64) inside
* a 3-variant union = 64B slot, the lib/regex compile() return shape
* that surfaced #38. */
#define WIDE_TYPES \
"type oops = !str;\n" \
"type nomem = !void;\n" \
"type wide = struct { xs: []u8, ys: []u8, n: i64 };\n"
#define WIDE_MAIN_CHECK \
" case let e: oops => return 13;\n" \
" case nomem => return 14;\n" \
" };\n" \
" return 0;\n" \
"};\n"
static const struct row rows[] = {
/* Literal source; every field checked, n (the LAST payload
* word, dead pre-#38) checked FIRST so its loss is exit 1. */
{ "wide_lit_roundtrip",
WIDE_TYPES
"fn mk(b: []u8, n: i64) (wide | oops | nomem) = {\n"
" return wide { xs = b, ys = b, n = n };\n"
"};\n"
"export fn main() i32 = {\n"
" let buf: [4]u8 = [9u8, 8u8, 7u8, 6u8];\n"
" match (mk(buf[0:4], 42)) {\n"
" case let w: wide => {\n"
" if (w.n != 42) { return 1; };\n"
" if (w.xs.len != 4) { return 2; };\n"
" if (w.ys.len != 4) { return 3; };\n"
" if (w.xs[3] != 6u8) { return 4; };\n"
" };\n"
WIDE_MAIN_CHECK,
0, 1, 0 },
/* Local-ident source (the widener's mem-to-mem struct arm). */
{ "wide_local_roundtrip",
WIDE_TYPES
"fn mk(b: []u8, n: i64) (wide | oops | nomem) = {\n"
" let w: wide = wide { xs = b, ys = b, n = n };\n"
" return w;\n"
"};\n"
"export fn main() i32 = {\n"
" let buf: [4]u8 = [1u8, 2u8, 3u8, 4u8];\n"
" let r: (wide | oops | nomem) = mk(buf[0:4], 7);\n"
" match (r) {\n"
" case let w: wide => {\n"
" if (w.n != 7) { return 1; };\n"
" if (w.ys[0] != 1u8) { return 2; };\n"
" };\n"
WIDE_MAIN_CHECK,
0, 1, 0 },
/* let-receive then ASSIGN-receive into the same slot, with
* frame canaries (locals around the receives must survive). */
{ "wide_assign_receive",
WIDE_TYPES
"fn mk(b: []u8, n: i64) (wide | oops | nomem) = {\n"
" return wide { xs = b, ys = b, n = n };\n"
"};\n"
"export fn main() i32 = {\n"
" let canary1: i64 = 111;\n"
" let buf: [4]u8 = [1u8, 2u8, 3u8, 4u8];\n"
" let r: (wide | oops | nomem) = mk(buf[0:4], 5);\n"
" let canary2: i64 = 222;\n"
" r = mk(buf[0:4], 6);\n"
" if (canary1 != 111) { return 21; };\n"
" if (canary2 != 222) { return 22; };\n"
" match (r) {\n"
" case let w: wide => { if (w.n != 6) { return 1; }; };\n"
WIDE_MAIN_CHECK,
0, 1, 0 },
/* match-scrutinee receive — the tagged-specific arm with no
* tuple precedent: the scrut slot itself is the sret dest. */
{ "wide_match_scrutinee",
WIDE_TYPES
"fn mk(b: []u8, n: i64) (wide | oops | nomem) = {\n"
" return wide { xs = b, ys = b, n = n };\n"
"};\n"
"export fn main() i32 = {\n"
" let buf: [4]u8 = [1u8, 2u8, 3u8, 4u8];\n"
" match (mk(buf[0:4], 9)) {\n"
" case let w: wide => { if (w.n != 9) { return 1; }; };\n"
WIDE_MAIN_CHECK,
0, 1, 0 },
/* Exact-type return-forward: inner sret's straight into
* outer's caller dest (cg_sret_forward / c.sretforward). */
{ "wide_forward_exact",
WIDE_TYPES
"fn inner(b: []u8, n: i64) (wide | oops | nomem) = {\n"
" return wide { xs = b, ys = b, n = n };\n"
"};\n"
"fn outer(b: []u8, n: i64) (wide | oops | nomem) = {\n"
" return inner(b, n);\n"
"};\n"
"export fn main() i32 = {\n"
" let buf: [4]u8 = [1u8, 2u8, 3u8, 4u8];\n"
" match (outer(buf[0:4], 11)) {\n"
" case let w: wide => { if (w.n != 11) { return 1; }; };\n"
WIDE_MAIN_CHECK,
0, 1, 0 },
/* str-payload error variant through the 64B slot (the widener
* str arm writing *(@sretarg)). */
{ "wide_str_error_variant",
WIDE_TYPES
"fn mk(b: []u8, n: i64) (wide | oops | nomem) = {\n"
" if (n < 0) { return \"neg\": oops; };\n"
" return wide { xs = b, ys = b, n = n };\n"
"};\n"
"export fn main() i32 = {\n"
" let buf: [4]u8 = [1u8, 2u8, 3u8, 4u8];\n"
" match (mk(buf[0:4], -1)) {\n"
" case let w: wide => return 1;\n"
" case let e: oops => {\n"
" if ((e: str).len != 3) { return 2; };\n"
" };\n"
" case nomem => return 3;\n"
" };\n"
" return 0;\n"
"};\n",
0, 1, 0 },
/* Two wide results live at once: distinct @match-independent
* receive slots, no shared-scratch clobber. */
{ "wide_multicall",
WIDE_TYPES
"fn mk(b: []u8, n: i64) (wide | oops | nomem) = {\n"
" return wide { xs = b, ys = b, n = n };\n"
"};\n"
"export fn main() i32 = {\n"
" let buf: [4]u8 = [1u8, 2u8, 3u8, 4u8];\n"
" let r1: (wide | oops | nomem) = mk(buf[0:4], 100);\n"
" let r2: (wide | oops | nomem) = mk(buf[0:4], 200);\n"
" match (r1) {\n"
" case let w: wide => { if (w.n != 100) { return 1; }; };\n"
" case let e: oops => return 13;\n"
" case nomem => return 14;\n"
" };\n"
" match (r2) {\n"
" case let w: wide => { if (w.n != 200) { return 2; }; };\n"
WIDE_MAIN_CHECK,
0, 1, 0 },
/* Bare `return;` (void variant) through a 40B slot: the tag
* must land in *(@sretarg), not AX. */
{ "wide_bare_return_void",
"type s4 = struct { a: i64, b: i64, c: i64, d: i64 };\n"
"fn maybe(n: i64) (s4 | void) = {\n"
" if (n == 0) { return; };\n"
" return s4 { a = 1, b = 2, c = 3, d = n };\n"
"};\n"
"export fn main() i32 = {\n"
" match (maybe(0)) {\n"
" case let v: s4 => return 1;\n"
" case void => { };\n"
" };\n"
" match (maybe(5)) {\n"
" case let v: s4 => { if (v.d != 5) { return 2; }; };\n"
" case void => return 3;\n"
" };\n"
" return 0;\n"
"};\n",
0, 1, 0 },
/* The #38 repro shape: 32B struct payload = 40B slot, literal
* AND local sources (was exit 2 / silent d-drop at master). */
{ "wide_s4_repro",
"type s4 = struct { a: i64, b: i64, c: i64, d: i64 };\n"
"fn taglit4() (s4 | bool) = {\n"
" return s4 { a = 1, b = 2, c = 3, d = 4 };\n"
"};\n"
"fn taglocal4() (s4 | bool) = {\n"
" let v: s4 = s4 { a = 1, b = 2, c = 3, d = 4 };\n"
" return v;\n"
"};\n"
"export fn main() i32 = {\n"
" match (taglit4()) {\n"
" case let r: s4 => { if (r.d != 4) { return 2; }; };\n"
" case let b: bool => return 3;\n"
" };\n"
" match (taglocal4()) {\n"
" case let r: s4 => { if (r.d != 4) { return 4; }; };\n"
" case let b: bool => return 5;\n"
" };\n"
" return 0;\n"
"};\n",
0, 1, 0 },
/* errno-shaped graduation row: errors.error's opaque_ variant is
* struct { strerror *fn (8B), data [3]u64 (24B) } = 32B payload =
* 40B slot — the LIVE in-tree #38 instance that truncated
* benignly-by-luck at master (data[2] was never read). This row
* READS the previously-dropped tail word first, pinning the
* errors.errno sret graduation. Local-ident return source, like
* errno's `return err;`. */
{ "errno_shaped_tail_read",
"type opq = struct { h: i64, data: [3]u64 };\n"
"fn wrap(e: i64) (opq | bool) = {\n"
" let o: opq;\n"
" o.h = 7;\n"
" o.data[0] = e: u64;\n"
" o.data[1] = 1111u64;\n"
" o.data[2] = 2222u64;\n"
" return o;\n"
"};\n"
"export fn main() i32 = {\n"
" match (wrap(5)) {\n"
" case let o: opq => {\n"
" if (o.data[2] != 2222u64) { return 1; };\n"
" if (o.data[1] != 1111u64) { return 2; };\n"
" if (o.data[0] != 5u64) { return 3; };\n"
" if (o.h != 7) { return 4; };\n"
" };\n"
" case let b: bool => return 5;\n"
" };\n"
" return 0;\n"
"};\n",
0, 1, 0 },
/* BOUNDARY: 24B struct payload = EXACTLY 32B slot — must stay
* register-ABI (sret==0 pins no hidden-RDI LEAQ in the .s). */
{ "boundary_s3_register",
"type s3 = struct { a: i64, b: i64, c: i64 };\n"
"fn mk(ok: bool) (s3 | bool) = {\n"
" if (!ok) { return false; };\n"
" return s3 { a = 7, b = 8, c = 9 };\n"
"};\n"
"export fn main() i32 = {\n"
" match (mk(true)) {\n"
" case let v: s3 => { if (v.c != 9) { return 1; }; };\n"
" case let b: bool => return 2;\n"
" };\n"
" match (mk(false)) {\n"
" case let v: s3 => return 3;\n"
" case let b: bool => { if (b) { return 4; }; };\n"
" };\n"
" return 0;\n"
"};\n",
0, 0, 0 },
/* BOUNDARY: (str|nomem) — the 32B-slot class fmt/io/strconv
* return everywhere; flipping it to sret breaks the tree. */
{ "boundary_str_nomem_register",
"type nomem = !void;\n"
"fn pick(ok: bool) (str | nomem) = {\n"
" if (ok) { return \"hello\"; };\n"
" let nm: nomem;\n"
" return nm;\n"
"};\n"
"export fn main() i32 = {\n"
" match (pick(true)) {\n"
" case let s: str => { if (s.len != 5) { return 1; }; };\n"
" case nomem => return 2;\n"
" };\n"
" match (pick(false)) {\n"
" case let s: str => return 3;\n"
" case nomem => { };\n"
" };\n"
" return 0;\n"
"};\n",
0, 0, 0 },
/* LOUD-STOP: widening return-forward of an sret-class source
* ((wide|oops) -> (wide|oops|nomem)) needs the mem-to-mem
* tag-remap — unwired, filed #40. Must NOT compile. */
{ "fail_widening_forward",
WIDE_TYPES
"fn inner(b: []u8) (wide | oops) = {\n"
" return wide { xs = b, ys = b, n = 1 };\n"
"};\n"
"fn outer(b: []u8) (wide | oops | nomem) = {\n"
" return inner(b);\n"
"};\n"
"export fn main() i32 = {\n"
" let buf: [4]u8 = [1u8, 2u8, 3u8, 4u8];\n"
" match (outer(buf[0:4])) {\n"
" case let w: wide => return 0;\n"
WIDE_MAIN_CHECK,
0, 0, 1 },
/* LOUD-STOP: `!` consuming an sret-class call result reads the
* cursor the callee never filled — #40-family follow-up. */
{ "fail_tryunw_wide",
WIDE_TYPES
"fn mk(b: []u8) (wide | oops | nomem) = {\n"
" return wide { xs = b, ys = b, n = 1 };\n"
"};\n"
"export fn main() i32 = {\n"
" let buf: [4]u8 = [1u8, 2u8, 3u8, 4u8];\n"
" let w: wide = mk(buf[0:4])!;\n"
" if (w.n != 1) { return 1; };\n"
" return 0;\n"
"};\n",
0, 0, 1 },
/* LOUD-STOP: an sret-class tagged call result in argument
* position (cursor push of a memory result). */
{ "fail_wide_call_arg",
WIDE_TYPES
"fn mk(b: []u8) (wide | oops | nomem) = {\n"
" return wide { xs = b, ys = b, n = 1 };\n"
"};\n"
"fn use(r: (wide | oops | nomem)) i64 = {\n"
" match (r) {\n"
" case let w: wide => return w.n;\n"
" case let e: oops => return -1;\n"
" case nomem => return -2;\n"
" };\n"
" return -3;\n"
"};\n"
"export fn main() i32 = {\n"
" let buf: [4]u8 = [1u8, 2u8, 3u8, 4u8];\n"
" if (use(mk(buf[0:4])) != 1) { return 1; };\n"
" return 0;\n"
"};\n",
0, 0, 1 },
};
static const char *g_bin;
/* compile one row with `tool` (w6c or w6c_ww) into outpath; returns
* the tool's exit code. */
static int
compile_s(const char *tool, const char *src, const char *outpath)
{
char cmd[1024];
snprintf(cmd, sizeof cmd, "%s/%s %s > %s 2>/dev/null",
g_bin, tool, src, outpath);
return runwait(cmd);
}
static int
file_eq(const char *a, const char *b)
{
char cmd[1024];
snprintf(cmd, sizeof cmd, "cmp -s %s %s", a, b);
return runwait(cmd) == 0;
}
static int
file_has(const char *path, const char *needle)
{
FILE *f = fopen(path, "rb");
if (!f) return 0;
static char buf[1 << 20];
size_t n = fread(buf, 1, sizeof buf - 1, f);
fclose(f);
buf[n] = '\0';
return strstr(buf, needle) != NULL;
}
static int
run_driver(const char *driver, const char *src, const char *label)
{
char tmpdir[128], cmd[1024];
snprintf(tmpdir, sizeof tmpdir, "/tmp/tsret_%d_d", getpid());
mkdir(tmpdir, 0755);
snprintf(cmd, sizeof cmd, "cd %s && %s/%s build %s >/dev/null 2>&1",
tmpdir, g_bin, driver, src);
if (runwait(cmd) != 0) {
fprintf(stderr, "row[%s]: build via %s failed\n",
label, driver);
return -1;
}
const char *base = strrchr(src, '/');
base = base ? base + 1 : src;
char outbin[256];
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(outbin);
rmdir(tmpdir);
return got;
}
int
main(void)
{
const char *bin = getenv("BIN");
if (!bin) bin = "out/bin";
static char absbin[512];
if (bin[0] != '/') {
char cwd[256];
if (getcwd(cwd, sizeof cwd) == NULL) return 1;
snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin);
bin = absbin;
}
g_bin = bin;
int n = (int)(sizeof rows / sizeof rows[0]);
int total = 0, fail = 0;
for (int i = 0; i < n; i++) {
const struct row *r = &rows[i];
char src[128], cs_s[128], ww_s[128];
snprintf(src, sizeof src, "/tmp/tsret_%d_%d.ww",
getpid(), i);
snprintf(cs_s, sizeof cs_s, "/tmp/tsret_%d_%d_cs.s",
getpid(), i);
snprintf(ww_s, sizeof ww_s, "/tmp/tsret_%d_%d_ww.s",
getpid(), i);
FILE *f = fopen(src, "wb");
if (!f) return 1;
fputs("package main;\n\n", f);
fputs(r->src, f);
fclose(f);
int cs_rc = compile_s("w6c", src, cs_s);
int ww_rc = compile_s("w6c_ww", src, ww_s);
if (r->buildfail) {
total++;
if (cs_rc == 0 || ww_rc == 0) {
fprintf(stderr, "FAIL row[%s]: loud-stop "
"expected, cstage rc=%d wwstage rc=%d\n",
r->label, cs_rc, ww_rc);
fail++;
}
unlink(src); unlink(cs_s); unlink(ww_s);
continue;
}
total++;
if (cs_rc != 0 || ww_rc != 0) {
fprintf(stderr, "FAIL row[%s]: compile rc cs=%d "
"ww=%d\n", r->label, cs_rc, ww_rc);
fail++;
unlink(src); unlink(cs_s); unlink(ww_s);
continue;
}
if (!file_eq(cs_s, ww_s)) {
fprintf(stderr, "FAIL row[%s]: cs != ww .s\n",
r->label);
fail++;
}
/* hidden-RDI dest: present iff the row is sret-class.
* The boundary rows pin the 32B class stays register. */
int has_di = file_has(cs_s, "(BP), DI\n");
if (r->sret && !has_di) {
fprintf(stderr, "FAIL row[%s]: expected sret "
"hidden-RDI LEAQ, none emitted\n", r->label);
fail++;
}
if (!r->sret && has_di) {
fprintf(stderr, "FAIL row[%s]: 32B-slot row "
"flipped to sret (classifier boundary "
"off-by-one)\n", r->label);
fail++;
}
int got_cs = run_driver("ww", src, r->label);
if (got_cs != r->want) {
fprintf(stderr, "FAIL row[%s] cstage: want %d "
"got %d\n", r->label, r->want, got_cs);
fail++;
}
char wwdrv[600];
snprintf(wwdrv, sizeof wwdrv, "%s/ww_ww", g_bin);
if (access(wwdrv, X_OK) == 0) {
int got_ww = run_driver("ww_ww", src, r->label);
if (got_ww != r->want) {
fprintf(stderr, "FAIL row[%s] wwstage: "
"want %d got %d\n",
r->label, r->want, got_ww);
fail++;
}
}
unlink(src); unlink(cs_s); unlink(ww_s);
}
if (fail) {
fprintf(stderr, "tagged_sret_run: %d/%d rows failed\n",
fail, total);
return 1;
}
printf("tagged_sret_run: %d rows ok\n", total);
return 0;
}