test: port the sret/struct-return asm observers to ww

718/721/730 -> test/asm/sret_test.ww. Frame-size and max--K(BP)
stomp sentinels, line-adjacency sret triangle (hidden-RDI, return-
the-pointer, no-AX-capture, forwarding reload) and narrow-trailing-
field windows preserved, including 718's #15 byte-id gate on the
three-return row. 718's whole-suite ww_ww-executable gate dropped
with the other skip gates (the Make target declares the tools).
This commit is contained in:
2026-08-08 14:38:08 +09:00
parent a50429138c
commit 3cca8cd249
5 changed files with 558 additions and 1051 deletions

557
test/asm/sret_test.ww Normal file
View File

@@ -0,0 +1,557 @@
package sret_test;
// Direct-w6c asm-window gate over struct-return lowering. Port of the
// retired native carriers test/wcc/718_struct_multi_return_scratch.c,
// 721_sret_struct_return.c and 730_sret_narrow_field.c; every
// assertion preserved. Runtime rows are owned elsewhere
// (test/wcc/data/r718_struct_return_*, test/lang/
// sret_struct_return_test.ww, test/lang/sret_narrow_field_test.ww) —
// the asm windows here are the unowned remainder byte-id cannot
// subsume by design (both stages could stomp or widen identically).
//
// 718 (#14 single-slot @retscr): frame value parsed off the first
// `TEXT f,$N` line must be 64 and no `-K(BP)` operand anywhere in the
// .s may exceed 64 (the below-SP stomp sentinel). Row
// three_returns_16B carries no byte-id leg — pre-existing #15
// label-counter skew for nested-if shapes, exactly the C's gate; the
// tagged row pins byte-id only (@tagscr family, frame unchecked).
// The C additionally gated ALL rows on an executable ww_ww — a skip
// gate, dropped like the w6c_ww ones (the Make target declares the
// tools it launches).
//
// 721 (#23 sret ABI + #9 forwarding): line-adjacency sentinels — the
// line before CALL main.mk(SB) must LEAQ the hidden dest into DI, one
// of the last 8 lines of mk before RET must reload @sretarg into AX,
// no MOVQ AX,(BP) capture within 3 lines after the CALL, and fwd rows
// reload (not LEAQ) DI before CALL main.inner(SB).
//
// 730 (#33 narrow trailing field): the field copy at offset 32 in mk
// must use the row's narrow MOV on both the (BP) load and (BX) store
// sides, and the pre-fix 8-byte `MOVQ AX, 32(BX)` must be absent. The
// trailing-field offset 32 is structurally fixed by the shared
// `{ a: i32, s: []u8, r: NARROW }` row shape.
//
// The `package main;` source prefix reproduces the carriers'
// wwtest_fputs injection.
import os;
import os.exec;
import strings;
import testenv;
import time;
fn fail(label: str, why: str) void = {
let m: str = strings.concat("sret FAIL: ", label, " -- ", why, "\n");
os.write(2, m.ptr, m.len: u64);
assert(false);
};
fn tmo() time.duration = {
return (180i64 * (time.second: i64)): time.duration;
};
fn emitstage(td: str, label: str, stage: str, drv: str,
outname: str) void = {
let av: []str = [];
append(av, drv);
append(av, "-o");
append(av, outname);
append(av, "src.ww");
let co: testenv.commandout;
testenv.runcommand(td, td, stage, av, tmo(), &co);
let ok: bool = co.termination == exec.termination.EXIT && co.code == 0;
if (!ok) { fail(label, strings.concat(stage, " compile failed")); };
};
// End index of the line holding `i`, INCLUDING its newline (fgets
// framing, so `\tRET\n` needles only hit whole RET lines).
fn lineend(s: str, i: i32) i32 = {
let rest: str = strings.sub(s, i, s.len);
let e: i32 = testenv.pos(rest, "\n");
if (e < 0) { return s.len; };
return i + e + 1;
};
fn digits(s: str, i: i32) i32 = {
let j: i32 = i;
let k: i32 = 0;
for (j < s.len) {
let c: u8 = s[j];
if (c < '0' || c > '9') { break; };
k = k * 10 + ((c - '0'): i32);
j += 1;
};
return k;
};
// $N off the first `TEXT f,$N` line (718 read_frame).
fn readframe(label: str, stage: str, s: str) i32 = {
let i: i32 = 0;
for (i < s.len) {
let le: i32 = lineend(s, i);
let line: str = strings.sub(s, i, le);
if (strings.hasprefix(line, "TEXT ")) {
let dp: i32 = testenv.pos(line, "$");
if (dp >= 0) { return digits(line, dp + 1); };
};
i = le;
};
fail(label, strings.concat(stage, ": no TEXT frame line in .s"));
return -1;
};
// Largest K over every `-K(BP)` operand in the .s (718
// read_max_neg_off): the below-SP stomp sentinel.
fn maxnegoff(s: str) i32 = {
let maxk: i32 = 0;
let i: i32 = 0;
for (i < s.len) {
let rest: str = strings.sub(s, i, s.len);
let p: i32 = testenv.pos(rest, "(BP)");
if (p < 0) { break; };
let ap: i32 = i + p;
let q: i32 = ap;
for (q > 0) {
let c: u8 = s[q - 1];
if (c < '0' || c > '9') { break; };
q -= 1;
};
if (q > 0 && s[q - 1] == '-') {
let k: i32 = digits(s, q);
if (k > maxk) { maxk = k; };
};
i = ap + 4;
};
return maxk;
};
// One 718 row: byte-id (when not #15-gated), frame == want on both
// stages, deepest -K(BP) <= bound on both stages.
fn framerow(label: str, src: str, frame: i32, maxoff: i32,
byteid: bool) void = {
let td: str = testenv.fresh();
testenv.writefile(strings.concat(td, "/src.ww"), src);
emitstage(td, label, "cstage", testenv.driver("w6c"), "cs.s");
emitstage(td, label, "wwstage", testenv.driver("w6c_ww"), "ws.s");
let cs: str = testenv.readfile(strings.concat(td, "/cs.s"));
let ws: str = testenv.readfile(strings.concat(td, "/ws.s"));
if (byteid) {
if (!testenv.same(cs, ws)) {
fail(label, "cstage vs wwstage asm differs");
};
};
if (frame > 0) {
if (readframe(label, "cstage", cs) != frame
|| readframe(label, "wwstage", ws) != frame) {
fail(label, "frame size mismatch (single-slot dedup)");
};
};
if (maxoff > 0) {
if (maxnegoff(cs) > maxoff || maxnegoff(ws) > maxoff) {
fail(label, "stomp regression: -K(BP) deeper than frame");
};
};
testenv.clean(td);
};
// 718 rows 1-3: frame = i/x/k args + r(16) + retscr(24) = $64; row 3
// pins that the one-return base case still allocates retscr once.
// Row 4 pins the orthogonal @tagscr family byte-id only.
@test fn retscratch() void = {
framerow("two_returns_16B", strings.concat(
"package main;\n",
"type inst = struct { sec: i64, nsec: i64 };\n",
"fn f(i: inst, x: i64) inst = {\n",
" let r: inst;\n",
" if (x > 0i64) {\n",
" r.sec = i.sec + x;\n",
" r.nsec = i.nsec + x;\n",
" return r;\n",
" };\n",
" r.sec = i.sec - x;\n",
" r.nsec = i.nsec - x;\n",
" return r;\n",
"};\n",
"fn main() i32 = {\n",
" let i: inst = inst { sec = 10i64, nsec = 20i64 };\n",
" let p: inst = f(i, 5i64);\n",
" if (p.sec != 15i64) { return 1; };\n",
" if (p.nsec != 25i64) { return 2; };\n",
" let q: inst = f(i, -3i64);\n",
" if (q.sec != 13i64) { return 3; };\n",
" if (q.nsec != 23i64) { return 4; };\n",
" return 0;\n",
"};\n"), 64, 64, true);
// Byte-id disabled: pre-existing #15 label-counter skew between
// stages for nested-if shapes; frame+stomp asserts still pin #14.
framerow("three_returns_16B", strings.concat(
"package main;\n",
"type inst = struct { sec: i64, nsec: i64 };\n",
"fn f(i: inst, k: i32) inst = {\n",
" let r: inst;\n",
" if (k == 1i32) {\n",
" r.sec = i.sec + 1i64;\n",
" r.nsec = i.nsec + 1i64;\n",
" return r;\n",
" };\n",
" if (k == 2i32) {\n",
" r.sec = i.sec * 2i64;\n",
" r.nsec = i.nsec * 2i64;\n",
" return r;\n",
" };\n",
" r.sec = i.sec;\n",
" r.nsec = i.nsec;\n",
" return r;\n",
"};\n",
"fn main() i32 = {\n",
" let i: inst = inst { sec = 10i64, nsec = 20i64 };\n",
" let a: inst = f(i, 1i32);\n",
" if (a.sec != 11i64) { return 1; };\n",
" if (a.nsec != 21i64) { return 2; };\n",
" let b: inst = f(i, 2i32);\n",
" if (b.sec != 20i64) { return 3; };\n",
" if (b.nsec != 40i64) { return 4; };\n",
" let c: inst = f(i, 9i32);\n",
" if (c.sec != 10i64) { return 5; };\n",
" if (c.nsec != 20i64) { return 6; };\n",
" return 0;\n",
"};\n"), 64, 64, false);
framerow("single_return_16B", strings.concat(
"package main;\n",
"type inst = struct { sec: i64, nsec: i64 };\n",
"fn f(i: inst) inst = {\n",
" let r: inst;\n",
" r.sec = i.sec + 1i64;\n",
" r.nsec = i.nsec + 1i64;\n",
" return r;\n",
"};\n",
"fn main() i32 = {\n",
" let i: inst = inst { sec = 100i64, nsec = 200i64 };\n",
" let r: inst = f(i);\n",
" if (r.sec != 101i64) { return 1; };\n",
" if (r.nsec != 201i64) { return 2; };\n",
" return 0;\n",
"};\n"), 64, 64, true);
framerow("tagged_multi_return", strings.concat(
"package main;\n",
"fn f(k: i32) (i64 | i32) = {\n",
" if (k > 0i32) { return 1i64; };\n",
" return 2i32;\n",
"};\n",
"fn main() i32 = {\n",
" let r: (i64 | i32) = f(5i32);\n",
" match (r) {\n",
" case let v: i64 => if (v != 1i64) { return 1; };\n",
" case let v: i32 => return 2;\n",
" };\n",
" let s: (i64 | i32) = f(-1i32);\n",
" match (s) {\n",
" case let v: i64 => return 3;\n",
" case let v: i32 => if (v != 2i32) { return 4; };\n",
" };\n",
" return 0;\n",
"};\n"), 0, 0, true);
};
// 721 (a): the line immediately before CALL main.mk(SB) passes the
// hidden sret dest pointer in RDI.
fn leaqdicheck(label: str, stage: str, s: str) void = {
let prev: str = "";
let i: i32 = 0;
for (i < s.len) {
let le: i32 = lineend(s, i);
let line: str = strings.sub(s, i, le);
if (testenv.has(line, "CALL\tmain.mk(SB)")) {
if (testenv.has(prev, "LEAQ\t")
&& testenv.has(prev, "(BP), DI")) {
return;
};
break;
};
prev = line;
i = le;
};
fail(label, strings.concat(stage,
": LEAQ -K(BP), DI before CALL mk(SB) missing"));
};
// 721 (b): the @sretarg reload (SysV return-the-pointer) must sit in
// the last 8 lines of mk before its first RET.
fn sretretloadcheck(label: str, stage: str, s: str) void = {
let tp: i32 = testenv.pos(s, "TEXT main.mk,");
if (tp < 0) {
fail(label, strings.concat(stage, ": no TEXT main.mk in .s"));
};
let win: []str = alloc([], 8u64)!;
win.len = 8;
let k: i32 = 0;
for (k < 8) { win[k] = ""; k += 1; };
let wi: i32 = 0;
let i: i32 = lineend(s, tp);
for (i < s.len) {
let le: i32 = lineend(s, i);
let line: str = strings.sub(s, i, le);
if (testenv.has(line, "\tRET\n")) {
k = 0;
for (k < 8) {
if (testenv.has(win[k], "MOVQ\t")
&& testenv.has(win[k], "(BP), AX")) {
return;
};
k += 1;
};
break;
};
win[wi] = line;
wi += 1;
if (wi == 8) { wi = 0; };
i = le;
};
fail(label, strings.concat(stage,
": MOVQ -K(BP), AX before RET in mk missing"));
};
// 721 (c) NEGATIVE: no MOVQ AX, -K(BP) capture within 3 lines after
// the CALL — the pre-#23 wwstage truncation pattern. A missing CALL
// passes here exactly as in the C; (a) already fails on it.
fn noaxcapturecheck(label: str, stage: str, s: str) void = {
let cp: i32 = testenv.pos(s, "CALL\tmain.mk(SB)");
if (cp < 0) { return; };
let i: i32 = lineend(s, cp);
let peek: i32 = 0;
for (i < s.len && peek < 3) {
let le: i32 = lineend(s, i);
let line: str = strings.sub(s, i, le);
if (testenv.has(line, "MOVQ\tAX,")
&& testenv.has(line, "(BP)")) {
fail(label, strings.concat(stage,
": MOVQ AX, -K(BP) after CALL mk(SB) -- ",
"pre-#23 truncation pattern"));
};
peek += 1;
i = le;
};
};
// 721 (d), fwd rows only: mk reloads its own @sretarg (MOVQ, not
// LEAQ of a local dest) into DI before forwarding to inner.
fn fwdreloadcheck(label: str, stage: str, s: str) void = {
let tp: i32 = testenv.pos(s, "TEXT main.mk,");
if (tp < 0) {
fail(label, strings.concat(stage, ": no TEXT main.mk in .s"));
};
let prev: str = strings.sub(s, tp, lineend(s, tp));
let i: i32 = lineend(s, tp);
for (i < s.len) {
let le: i32 = lineend(s, i);
let line: str = strings.sub(s, i, le);
if (testenv.has(line, "CALL\tmain.inner(SB)")) {
if (testenv.has(prev, "MOVQ\t")
&& testenv.has(prev, "(BP), DI")
&& !testenv.has(prev, "LEAQ")) {
return;
};
break;
};
prev = line;
i = le;
};
fail(label, strings.concat(stage,
": MOVQ -K(BP), DI (sret-forward) before CALL inner(SB) ",
"missing"));
};
fn sretrow(label: str, src: str, fwd: bool) void = {
let td: str = testenv.fresh();
testenv.writefile(strings.concat(td, "/src.ww"), src);
emitstage(td, label, "cstage", testenv.driver("w6c"), "cs.s");
emitstage(td, label, "wwstage", testenv.driver("w6c_ww"), "ws.s");
let cs: str = testenv.readfile(strings.concat(td, "/cs.s"));
let ws: str = testenv.readfile(strings.concat(td, "/ws.s"));
leaqdicheck(label, "cstage", cs);
sretretloadcheck(label, "cstage", cs);
noaxcapturecheck(label, "cstage", cs);
leaqdicheck(label, "wwstage", ws);
sretretloadcheck(label, "wwstage", ws);
noaxcapturecheck(label, "wwstage", ws);
if (fwd) {
fwdreloadcheck(label, "cstage", cs);
fwdreloadcheck(label, "wwstage", ws);
};
if (!testenv.same(cs, ws)) {
fail(label, "cstage vs wwstage asm differs");
};
testenv.clean(td);
};
@test fn sretsentinels() void = {
sretrow("quad_i64", strings.concat(
"package main;\n",
"type quad = struct { a: i64, b: i64, c: i64, d: i64 };\n",
"fn mk() quad = {\n",
" return quad { a = 1i64, b = 2i64, c = 3i64, d = 4i64 };\n",
"};\n",
"fn main() i32 = { let q: quad = mk(); return 0; };\n"), false);
// The utf8 decoder shape that surfaced #23: the []u8 slice tail
// crosses the AX/DX/CX boundary the truncation bug dropped.
sretrow("decoder", strings.concat(
"package main;\n",
"type decoder = struct { offs: i64, src: []u8 };\n",
"fn mk(s: []u8) decoder = {\n",
" let r: decoder;\n",
" r.offs = 0i64;\n",
" r.src = s;\n",
" return r;\n",
"};\n",
"fn main() i32 = {\n",
" let b: [1]u8;\n",
" let d: decoder = mk(b[0:1]);\n",
" return 0;\n",
"};\n"), false);
sretrow("five_i64", strings.concat(
"package main;\n",
"type five = struct { a: i64, b: i64, c: i64, d: i64, e: i64 };\n",
"fn mk() five = {\n",
" return five { a = 1i64, b = 2i64, c = 3i64, d = 4i64, e = 5i64 };\n",
"};\n",
"fn main() i32 = { let f: five = mk(); return 0; };\n"), false);
sretrow("forward_quad", strings.concat(
"package main;\n",
"type quad = struct { a: i64, b: i64, c: i64, d: i64 };\n",
"fn inner(x: i64) quad = {\n",
" return quad { a = x, b = x + 1i64, c = x + 2i64, d = x + 3i64 };\n",
"};\n",
"fn mk(x: i64) quad = {\n",
" return inner(x);\n",
"};\n",
"fn main() i32 = { let q: quad = mk(10i64); return 0; };\n"), true);
sretrow("forward_decoder", strings.concat(
"package main;\n",
"type decoder = struct { offs: i64, src: []u8 };\n",
"fn inner(s: []u8) decoder = {\n",
" let r: decoder;\n",
" r.offs = 0i64;\n",
" r.src = s;\n",
" return r;\n",
"};\n",
"fn mk(s: []u8) decoder = {\n",
" return inner(s);\n",
"};\n",
"fn main() i32 = {\n",
" let b: [1]u8;\n",
" let d: decoder = mk(b[0:1]);\n",
" return 0;\n",
"};\n"), true);
};
// 730: mk's window (TEXT main.mk, .. first RET line) must copy the
// trailing narrow field with the row's MOV width on both the (BP)
// load and the 32(BX) store, and never with an 8-byte MOVQ.
fn narrowcheck(label: str, stage: str, s: str, mov: str) void = {
let tp: i32 = testenv.pos(s, "TEXT main.mk,");
if (tp < 0) {
fail(label, strings.concat(stage, ": no TEXT main.mk in .s"));
};
let store: str = strings.concat(mov, "\tAX, 32(BX)\n");
let loadkey: str = strings.concat(mov, "\t");
let sawstore: bool = false;
let sawload: bool = false;
let i: i32 = lineend(s, tp);
for (i < s.len) {
let le: i32 = lineend(s, i);
let line: str = strings.sub(s, i, le);
if (testenv.has(line, "\tRET\n")) { break; };
if (testenv.has(line, store)) { sawstore = true; };
if (testenv.has(line, loadkey)
&& testenv.has(line, "(BP), AX")) {
sawload = true;
};
if (testenv.has(line, "MOVQ\tAX, 32(BX)\n")) {
fail(label, strings.concat(stage,
": MOVQ AX, 32(BX) in mk -- pre-#33 over-wide ",
"pattern"));
};
i = le;
};
if (!sawstore) {
fail(label, strings.concat(stage, ": expected `", mov,
" AX, 32(BX)` store in mk body"));
};
if (!sawload) {
fail(label, strings.concat(stage, ": expected narrow `", mov,
" -K(BP), AX` load in mk"));
};
};
fn narrowrow(label: str, src: str, mov: str) void = {
let td: str = testenv.fresh();
testenv.writefile(strings.concat(td, "/src.ww"), src);
emitstage(td, label, "cstage", testenv.driver("w6c"), "cs.s");
emitstage(td, label, "wwstage", testenv.driver("w6c_ww"), "ws.s");
let cs: str = testenv.readfile(strings.concat(td, "/cs.s"));
let ws: str = testenv.readfile(strings.concat(td, "/ws.s"));
narrowcheck(label, "cstage", cs, mov);
narrowcheck(label, "wwstage", ws, mov);
if (!testenv.same(cs, ws)) {
fail(label, "cstage vs wwstage asm differs");
};
testenv.clean(td);
};
// The i16 tail falls to 2x MOVB per the shared MOVQ/MOVL/MOVB chain
// (no MOVW arm in either stage) — narrow + byte-id is the pin, the
// bug-check is the no-MOVQ-at-32 negative.
@test fn sretnarrowfield() void = {
narrowrow("trailing_bool", strings.concat(
"package main;\n",
"type t = struct { a: i32, s: []u8, r: bool };\n",
"export fn mk() t = {\n",
" let v: t; let z: []u8;\n",
" v.s = z; v.a = 0; v.r = false;\n",
" return v;\n",
"};\n",
"export fn main() i32 = {\n",
" let x: t = mk();\n",
" if (x.r) { return 1; };\n",
" return 0;\n",
"};\n"), "MOVB");
narrowrow("trailing_u8", strings.concat(
"package main;\n",
"type t = struct { a: i32, s: []u8, r: u8 };\n",
"export fn mk() t = {\n",
" let v: t; let z: []u8;\n",
" v.s = z; v.a = 0; v.r = 0u8;\n",
" return v;\n",
"};\n",
"export fn main() i32 = {\n",
" let x: t = mk();\n",
" if (x.r != 0u8) { return 1; };\n",
" return 0;\n",
"};\n"), "MOVB");
narrowrow("trailing_i16", strings.concat(
"package main;\n",
"type t = struct { a: i32, s: []u8, r: i16 };\n",
"export fn mk() t = {\n",
" let v: t; let z: []u8;\n",
" v.s = z; v.a = 0; v.r = 0i16;\n",
" return v;\n",
"};\n",
"export fn main() i32 = {\n",
" let x: t = mk();\n",
" if (x.r != 0i16) { return 1; };\n",
" return 0;\n",
"};\n"), "MOVB");
narrowrow("trailing_i32", strings.concat(
"package main;\n",
"type t = struct { a: i32, s: []u8, r: i32 };\n",
"export fn mk() t = {\n",
" let v: t; let z: []u8;\n",
" v.s = z; v.a = 0; v.r = 0;\n",
" return v;\n",
"};\n",
"export fn main() i32 = {\n",
" let x: t = mk();\n",
" if (x.r != 0) { return 1; };\n",
" return 0;\n",
"};\n"), "MOVL");
};