diff --git a/Makefile b/Makefile index aa914cfa..da030aa1 100644 --- a/Makefile +++ b/Makefile @@ -384,7 +384,7 @@ XMOD_WW_TARGETS = $(XMOD_WW_TESTS:%=wwtest/%) # byte-id legs). They observe compiler BEHAVIOR by driving w6c and # w6c_ww per row — not identity gates — so they run under # test-compiler beside the surviving residual carriers. -ASM_WW_TESTS = test/asm/modshadow_test.ww +ASM_WW_TESTS = test/asm/modshadow_test.ww test/asm/sret_test.ww ASM_WW_TARGETS = $(ASM_WW_TESTS:%=wwtest/%) BOOTSTRAP_WRAPPER_SOURCES = test/wcc/950_selfcheck.c \ test/wcc/991_w6a_ww.c \ diff --git a/test/asm/sret_test.ww b/test/asm/sret_test.ww new file mode 100644 index 00000000..671b8a02 --- /dev/null +++ b/test/asm/sret_test.ww @@ -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"); +}; diff --git a/test/wcc/718_struct_multi_return_scratch.c b/test/wcc/718_struct_multi_return_scratch.c deleted file mode 100644 index c0142915..00000000 --- a/test/wcc/718_struct_multi_return_scratch.c +++ /dev/null @@ -1,381 +0,0 @@ -/* - * 718_struct_multi_return_scratch — single-slot @retscr SSoT across - * both stages for struct-by-value return scratch (task #14). - * - * Pre-fix: - * - cstage cgreturn struct arm called local_alloc per return site - * (mklabel("retscr") + local_alloc grows cg_frame). N return sites - * reserved N×scratch_size bytes — over-allocation, but safe. - * - wwstage scanlocals already deduped via scanseenmark("@retscr") - * → frame reserved a single 24B slot. Emit-time localadd("@retscr") - * was supposed to dedup via the `@`-prefix path that walks - * c.locals, but cgblock save/restore (post-#27) unwound the - * @retscr stub on block exit. The second `return r` (outside the - * if-body's block) hit localadd with c.locals lacking @retscr → - * fell to localalloc → fresh slot, growing c.frame past the - * scan-reserved bound. Frame size (taken from scan) was correct - * for ONE slot but the emit code referenced TWO slots — the - * second site's stores landed BELOW SP. - * - * That under-allocation was a silent stomp-on-OS-stack: signal - * delivery / interrupt in the second-return window would clobber - * the scratch writes. Bootstrap byte-id survived only because - * nothing fired in those windows during self-compile. - * - * Fix (#14): - * - cstage: cg_retscr static (per-fn, reset in cgfn). First retscr - * allocation runs the existing local_alloc path AND stores the - * offset; subsequent uses reuse cg_retscr. Returns are terminal, - * so all retscr uses in a fn share one slot — single-slot is - * structurally correct, not "best-effort merge". - * - wwstage: c.retscroff i32 field. localadd's `@`-prefix dedup - * special-cases "@retscr" to consult c.retscroff (set on first - * emit, reused after). c.retscroff survives cgblock save/restore. - * - * Polarity catalog: - * - #9 wwstage OVER (tagged return slot sized 16B for 1-word - * payload; fixed by sizing match-spill to scrutinee). - * - #11 wwstage UNDER (struct-by-value param decompose missed - * user-defined TY_STRUCT branch; fixed by adding it). - * - #14 wwstage UNDER (struct multi-return @retscr stomp post-#27; - * emit/scan disagreement on `@`-prefix dedup across blocks; - * fixed by retscroff SSoT). cstage was per-site-fresh — - * wasteful-but-safe, aligned UP to single-slot for ABI - * consistency with wwstage's now-correct enforcement, not - * for correctness. - * - * What this test pins: - * 1. Asm byte-identity between cstage and wwstage on multi-return - * and single-return struct-return fns (rows 1-3). - * 2. **Negative**: no `.s` instruction references `-N(BP)` with - * N > frame-size on any multi-struct-return row. This is the - * stomp regression sentinel — a future emit/scan disagreement - * that re-introduces below-SP writes would slip past byte-id - * alone (both stages could stomp the same way and stay - * byte-identical). - * 3. Runtime: each row's main returns 0 only when both return - * arms of the fn-under-test produce the correct value. - * 4. Regression guard (row 4): a tagged-return multi-return shape - * uses a different scratch family (@tagscr, NOT @retscr) and - * must stay byte-identical pre- and post-fix. - */ -#include -#include -#include -#include -#include -#include "wwtestpkg.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 { - /* Runtime ownership: test/wcc/data/r718_struct_return_*. */ - const char *label; - const char *src; - int frame; /* expected `TEXT f,$N` — 0 = don't check */ - int max_off; /* assert no `-K(BP)` with K > max_off — 0 = don't check */ - int check_byte_id; /* 1 = assert cstage-vs-wwstage asm byte-identical */ -}; - -static const struct row rows[] = { - /* 1. Smallest divergent shape: two return sites returning a - * 16B struct. Pre-fix cstage $96, wwstage $64 (stomp). - * Post-fix: both $64, no offset deeper than -64. */ - { "two_returns_16B", - "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", - /* frame: i(16) + x(8) + r(16) + retscr(24) = 64 */ - 64, - 64, - 1 }, - /* 2. Three return sites returning a 16B struct. Pre-fix cstage - * would have grown the frame by another 24B (to $112 or so); - * wwstage would have stomped TWO additional 24B slots below SP. - * Post-fix: still $64 — single slot across three sites. */ - { "three_returns_16B", - "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", - /* frame: i(16) + k(8) + r(16) + retscr(24) = 64 */ - 64, - 64, - /* Byte-id disabled: pre-existing label-counter skew between - * stages for nested-if shapes (cstage's _ct_/_ce_/_end_ seq - * leads wwstage by one). Filed as #15 (sister of #14); not - * introduced by #14, so the disable is scoped to byte-id - * only — frame+stomp asserts still pin the #14 invariant. */ - 0 }, - /* 3. Single-return regression guard. Frame should match the - * multi-return shape — i.e. the fix doesn't perturb the - * one-return base case (still allocates the retscr slot - * exactly once). */ - { "single_return_16B", - "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", - /* frame: i(16) + r(16) + retscr(24) = 56 -> aligned to 64 */ - 64, - 64, - 1 }, - /* 4. Tagged-return multi-return — orthogonal scratch family - * (@tagscr, not @retscr). Pin that byte-identity holds; the - * #14 fix does NOT alter this shape's frame or body. If a - * future change accidentally routes tagged-return through - * @retscr, this row catches the divergence. */ - { "tagged_multi_return", - "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", - /* Frame/max_off depend on the tagged ABI shape; pin byte-id - * only, not specific values. The negative-offset bound here - * still catches a stomp regression because byte-identity - * forces both stages to agree, and if a future emit/scan - * disagreement appears it'll surface as either a frame - * mismatch OR a different offset pattern. */ - 0, - 0, - 1 }, -}; - -/* Scan asm output for the `TEXT f,$N` line; return N (or -1 if not - * found). Tolerates an optional MODULE-mangled prefix. */ -static int -read_frame(const char *path) -{ - FILE *f = fopen(path, "rb"); - if (!f) return -1; - char line[1024]; - int frame = -1; - while (fgets(line, sizeof line, f)) { - if (strncmp(line, "TEXT ", 5) != 0) continue; - const char *dollar = strchr(line, '$'); - if (!dollar) continue; - /* TEXT f,$96 — frame after `$` up to whitespace */ - frame = atoi(dollar + 1); - break; - } - fclose(f); - return frame; -} - -/* Scan asm output for max -K(BP) offset (where K > 0). Returns the - * largest K seen, or 0 if no negative-BP reference appears. */ -static int -read_max_neg_off(const char *path) -{ - FILE *f = fopen(path, "rb"); - if (!f) return -1; - int maxk = 0; - char buf[16384]; - size_t n = fread(buf, 1, sizeof buf - 1, f); - fclose(f); - buf[n] = '\0'; - /* Look for "-N(BP)" patterns. */ - const char *p = buf; - while ((p = strstr(p, "(BP)")) != NULL) { - /* Walk backward to find the start of the operand. */ - const char *q = p; - while (q > buf && (q[-1] >= '0' && q[-1] <= '9')) q--; - if (q > buf && q[-1] == '-') { - int k = atoi(q); - if (k > maxk) maxk = k; - } - p += 4; - } - return maxk; -} - -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/wcsmr_asm_%d_%d.ww", getpid(), i); - snprintf(cs, sizeof cs, "/tmp/wcsmr_asm_%d_%d_c.s", getpid(), i); - snprintf(ws, sizeof ws, "/tmp/wcsmr_asm_%d_%d_w.s", getpid(), i); - - FILE *f = fopen(src, "wb"); - if (!f) return -1; - wwtest_fputs(r->src, f); - fclose(f); - - int rc = 0; - - 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; - } - - /* Positive: byte-identity between stages. Gated per row — some - * shapes have pre-existing label-counter skew unrelated to #14. */ - if (r->check_byte_id) { - FILE *fc = fopen(cs, "rb"); - FILE *fw = fopen(ws, "rb"); - 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); - } - - /* Positive: frame size matches expected (single-slot dedup). */ - if (rc == 0 && r->frame > 0) { - int cframe = read_frame(cs); - int wframe = read_frame(ws); - if (cframe != r->frame || wframe != r->frame) { - fprintf(stderr, - "row[%s]: frame size mismatch — cstage=$%d wwstage=$%d want=$%d\n", - r->label, cframe, wframe, r->frame); - rc = -1; - } - } - - /* Negative (stomp sentinel): no -K(BP) with K > max_off. */ - if (rc == 0 && r->max_off > 0) { - int cmax = read_max_neg_off(cs); - int wmax = read_max_neg_off(ws); - if (cmax > r->max_off || wmax > r->max_off) { - fprintf(stderr, - "row[%s]: stomp regression — deepest -BP offset cstage=%d wwstage=%d bound=%d\n", - r->label, cmax, wmax, r->max_off); - rc = -1; - } - } - - 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 wdrv[1024]; - snprintf(wdrv, sizeof wdrv, "%s/ww_ww", bin); - - int n = (int)(sizeof rows / sizeof rows[0]); - int total = 0, fail = 0; - - if (access(wdrv, X_OK) == 0) { - for (int i = 0; i < n; i++) { - total++; - if (asm_byte_identical(bin, &rows[i], i) != 0) - fail++; - } - } - - if (fail) { - fprintf(stderr, - "struct_multi_return_scratch: %d/%d fixtures failed\n", - fail, total); - return 1; - } - printf("struct_multi_return_scratch: %d/%d ok\n", total, total); - return 0; -} diff --git a/test/wcc/721_sret_struct_return.c b/test/wcc/721_sret_struct_return.c deleted file mode 100644 index 79e2d0d0..00000000 --- a/test/wcc/721_sret_struct_return.c +++ /dev/null @@ -1,392 +0,0 @@ -/* - * 721_sret_struct_return — Class A asm-presence sentinels for the - * System V AMD64 sret ABI lowering (task #23). - * - * #23 wires both stages to lower a plain TY_STRUCT return > 24B - * through the standard SysV sret discipline: caller pre-allocates - * dest, passes &dest in RDI as a hidden first arg (shifting all - * declared args right by one — SI/DX/CX/R8/R9/+stack), callee saves - * RDI to @sretarg in the prologue, writes the return value through - * the saved pointer, then `MOVQ @sretarg(BP), AX; RET` (the SysV - * "return the pointer" discipline). 4 lowering sites: caller arg- - * shift+receive, callee prologue, callee return — receive collapses - * into the caller-prealloc because the named LHS slot IS the - * prealloc dest. - * - * Pre-#23: cstage skipped the CALL emit entirely at the receive - * site (frame layout collapsed; exit 11). wwstage emitted the CALL - * but truncated the 32B return to RAX only (slice payload garbage; - * segfault). Surfaced by lib/encoding/utf8 pre-flight when the - * Hoehrmann decoder `struct { offs: size, src: []u8 }` (32B) hit - * the documented OUT-OF-SCOPE marker at 698. - * - * Three sentinels per row pinned here (rob-pike's triangle): - * (a) caller emits `LEAQ (BP), DI` immediately before `CALL` — - * the hidden RDI dest pointer (asm-presence positive). - * (b) callee emits `MOVQ (BP), AX` BEFORE the final `RET` — - * the sret return-the-pointer load (asm-presence positive). - * The K is the @sretarg offset; we don't pin it, but we pin - * that an `(BP), AX` load lives in the last three lines - * before RET in any fn whose return type is > 24B struct. - * (c) caller emits NO `MOVQ AX, (BP)` capture of the call - * result for a return type > 24B (asm-presence negative). - * Pre-#23 wwstage emitted such a capture and truncated. - * - * Plus byte-id between stages per row (a future Class A divergence - * via either site catches here). - */ -#include -#include -#include -#include -#include -#include -#include "wwtestpkg.h" - -static int -runwait(const char *cmd) -{ - int rc = system(cmd); - if (rc == -1) return -1; - if (WIFEXITED(rc)) return WEXITSTATUS(rc); - return -1; -} - -/* fwd: row's mk body is `return inner(...)` — sret return-forwarding - * (task #9 follow-up). Additional sentinel: inside mk, the CALL - * inner(SB) must be preceded by `MOVQ -K(BP), DI` (the @sretarg - * reload), NOT `LEAQ -K(BP), DI` (which would point at a local). */ -struct row { const char *label; const char *src; int fwd; }; - -/* Each row's mk fn returns a >24B struct; main does a `let r: T = mk(...)` - * so the receive site is wired and the sret discipline fires. */ -static const struct row rows[] = { - /* 32B four-i64: the smallest-padded sret return shape. */ - { "quad_i64", - "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", 0 }, - /* utf8 decoder shape (surfacing case for #23): i64 + []u8. The - * []u8 field's slice layout (ptr/len/cap) crosses the AX/DX/CX - * boundary — the wwstage truncation bug dropped the slice tail. */ - { "decoder", - "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", 0 }, - /* 40B five-i64: second size past 24B, exercises @sretscr sizing. */ - { "five_i64", - "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", 0 }, - /* Forwarding (task #9 follow-up to #23): `return inner(...);` from - * an sret callee. mk reloads its own @sretarg into RDI and tail- - * shapes the call into inner; no @sretscr/local materialised, no - * struct copy in mk's frame. */ - { "forward_quad", - "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", 1 }, - /* Forwarding decoder: argument-bearing inner (slice param) routes - * through the same forwarding shape as utf8 iterators. */ - { "forward_decoder", - "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", 1 }, -}; - -static int -slurp(const char *path, char *buf, size_t cap) -{ - FILE *f = fopen(path, "rb"); - if (!f) return -1; - size_t n = fread(buf, 1, cap - 1, f); - fclose(f); - buf[n] = '\0'; - return (int)n; -} - -static int -emit_s(const char *w6c, const struct row *r, int i, char *out_s, size_t cap) -{ - char src[96], cmd[1024]; - snprintf(src, sizeof src, "/tmp/sret_asm_%d_%d.ww", getpid(), i); - snprintf(out_s, cap, "/tmp/sret_asm_%d_%d_%s.s", - getpid(), i, w6c[strlen(w6c) - 1] == 'w' ? "ww" : "c"); - - FILE *f = fopen(src, "wb"); - if (!f) return -1; - wwtest_fputs(r->src, f); - fclose(f); - - snprintf(cmd, sizeof cmd, "%s -o %s %s 2>/dev/null", w6c, out_s, src); - int rc = runwait(cmd); - unlink(src); - return rc; -} - -/* (a) caller-side prealloc sentinel: `LEAQ -K(BP), DI` must appear - * on the line immediately preceding `CALL mk(SB)`. */ -static int -check_leaq_di_before_call(const char *path, const struct row *r) -{ - FILE *f = fopen(path, "rb"); - if (!f) return -1; - char prev[256] = {0}; - char line[1024]; - int ok = -1; - while (fgets(line, sizeof line, f)) { - if (strstr(line, "CALL\tmain.mk(SB)") - || strstr(line, "CALL main.mk(SB)")) { - if (strstr(prev, "LEAQ\t") - && strstr(prev, "(BP), DI")) { - ok = 0; - } - break; - } - strncpy(prev, line, sizeof prev - 1); - prev[sizeof prev - 1] = '\0'; - } - fclose(f); - if (ok != 0) - fprintf(stderr, - "row[%s]: LEAQ -K(BP), DI before CALL mk(SB) missing\n", - r->label); - return ok; -} - -/* (b) callee-side return-the-pointer sentinel: in the mk fn body - * (between `TEXT mk,` and the FIRST `RET` after it), assert a - * `MOVQ -K(BP), AX` appears within the last few lines before that - * RET — the @sretarg reload. */ -static int -check_movq_bp_ax_before_ret(const char *path, const struct row *r) -{ - FILE *f = fopen(path, "rb"); - if (!f) return -1; - char line[1024]; - int in_mk = 0; - char window[8][256] = {{0}}; - int wi = 0; - int ok = -1; - while (fgets(line, sizeof line, f)) { - if (!in_mk) { - if (strstr(line, "TEXT main.mk,") || strstr(line, "TEXT\tmain.mk,")) - in_mk = 1; - continue; - } - if (strstr(line, "\tRET\n")) { - for (int k = 0; k < 8; k++) { - if (strstr(window[k], "MOVQ\t") - && strstr(window[k], "(BP), AX")) { - ok = 0; - break; - } - } - break; - } - strncpy(window[wi % 8], line, sizeof window[0] - 1); - window[wi % 8][sizeof window[0] - 1] = '\0'; - wi++; - } - fclose(f); - if (ok != 0) - fprintf(stderr, - "row[%s]: MOVQ -K(BP), AX before RET in mk missing\n", - r->label); - return ok; -} - -/* (d) forwarding-specific sentinel (task #9 follow-up): inside mk's - * body (between `TEXT mk,` and the first `CALL inner(SB)` after it), - * assert the prior line is `MOVQ -K(BP), DI` — the @sretarg reload - * pattern — and NOT `LEAQ -K(BP), DI` (which would mean mk allocated - * a local dest for the forwarded call, defeating the elision). */ -static int -check_movq_bp_di_before_inner_call(const char *path, const struct row *r) -{ - FILE *f = fopen(path, "rb"); - if (!f) return -1; - char line[1024]; - char prev[256] = {0}; - int in_mk = 0; - int ok = -1; - while (fgets(line, sizeof line, f)) { - if (!in_mk) { - if (strstr(line, "TEXT main.mk,") - || strstr(line, "TEXT\tmain.mk,")) - in_mk = 1; - strncpy(prev, line, sizeof prev - 1); - prev[sizeof prev - 1] = '\0'; - continue; - } - if (strstr(line, "CALL\tmain.inner(SB)") - || strstr(line, "CALL main.inner(SB)")) { - if (strstr(prev, "MOVQ\t") - && strstr(prev, "(BP), DI") - && !strstr(prev, "LEAQ")) - ok = 0; - break; - } - strncpy(prev, line, sizeof prev - 1); - prev[sizeof prev - 1] = '\0'; - } - fclose(f); - if (ok != 0) - fprintf(stderr, - "row[%s]: MOVQ -K(BP), DI (sret-forward) before" - " CALL inner(SB) in mk missing\n", r->label); - return ok; -} - -/* (c) caller-side negative-assert: between `CALL mk(SB)` and the - * NEXT instruction line, there must be NO `MOVQ AX, -K(BP)` (the - * pre-#23 wwstage truncation pattern). The natural sret receive - * leaves the value in the slot already; AX holds the dest ptr but - * we don't store it back. */ -static int -check_no_movq_ax_bp_after_call(const char *path, const struct row *r) -{ - FILE *f = fopen(path, "rb"); - if (!f) return -1; - char line[1024]; - int seen_call = 0; - int peek = 0; - int ok = 0; - while (fgets(line, sizeof line, f)) { - if (!seen_call) { - if (strstr(line, "CALL\tmain.mk(SB)") - || strstr(line, "CALL main.mk(SB)")) { - seen_call = 1; - } - continue; - } - /* Inspect the next few instruction lines. A `MOVQ AX, -N(BP)` - * within ~3 lines after CALL would be the truncation - * pattern. */ - if (peek++ >= 3) break; - if (strstr(line, "MOVQ\tAX,") && strstr(line, "(BP)")) { - ok = -1; - break; - } - } - fclose(f); - if (ok != 0) - fprintf(stderr, - "row[%s]: unexpected MOVQ AX, -K(BP) after CALL mk(SB)" - " (pre-#23 truncation pattern)\n", r->label); - return ok; -} - -int -main(void) -{ - const char *bin = getenv("BIN"); - if (!bin) bin = "out/bin"; - 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; - } - - char w6c[640], w6c_ww[640]; - snprintf(w6c, sizeof w6c, "%s/w6c", bin); - snprintf(w6c_ww, sizeof w6c_ww, "%s/w6c_ww", bin); - - int have_ww = (access(w6c_ww, X_OK) == 0); - - int n = (int)(sizeof rows / sizeof rows[0]); - int total = 0, fail = 0; - - for (int i = 0; i < n; i++) { - char cs_path[128], ws_path[128]; - - /* cstage asm + three (or four, fwd) sentinels. */ - if (emit_s(w6c, &rows[i], i, cs_path, sizeof cs_path) != 0) { - fprintf(stderr, "row[%s]: w6c failed\n", rows[i].label); - fail++; total++; continue; - } - total += 3; - if (check_leaq_di_before_call(cs_path, &rows[i]) != 0) fail++; - if (check_movq_bp_ax_before_ret(cs_path, &rows[i]) != 0) fail++; - if (check_no_movq_ax_bp_after_call(cs_path, &rows[i]) != 0) fail++; - if (rows[i].fwd) { - total++; - if (check_movq_bp_di_before_inner_call(cs_path, - &rows[i]) != 0) fail++; - } - - if (!have_ww) { unlink(cs_path); continue; } - - /* wwstage asm + three (or four, fwd) sentinels. */ - if (emit_s(w6c_ww, &rows[i], i, ws_path, sizeof ws_path) != 0) { - fprintf(stderr, - "row[%s]: w6c_ww failed\n", rows[i].label); - fail++; total++; - unlink(cs_path); - continue; - } - total += 3; - if (check_leaq_di_before_call(ws_path, &rows[i]) != 0) fail++; - if (check_movq_bp_ax_before_ret(ws_path, &rows[i]) != 0) fail++; - if (check_no_movq_ax_bp_after_call(ws_path, &rows[i]) != 0) fail++; - if (rows[i].fwd) { - total++; - if (check_movq_bp_di_before_inner_call(ws_path, - &rows[i]) != 0) fail++; - } - - /* Byte-id diff between stages. */ - total++; - char cmd[512]; - snprintf(cmd, sizeof cmd, "cmp -s %s %s", cs_path, ws_path); - if (runwait(cmd) != 0) { - fprintf(stderr, - "row[%s]: cstage vs wwstage asm differs\n", - rows[i].label); - fail++; - } - - unlink(cs_path); unlink(ws_path); - } - - if (fail) { - fprintf(stderr, - "sret_struct_return: %d/%d fixtures failed\n", - fail, total); - return 1; - } - printf("sret_struct_return: %d/%d ok\n", total, total); - return 0; -} diff --git a/test/wcc/730_sret_narrow_field.c b/test/wcc/730_sret_narrow_field.c deleted file mode 100644 index 0ced1b29..00000000 --- a/test/wcc/730_sret_narrow_field.c +++ /dev/null @@ -1,277 +0,0 @@ -/* - * 730_sret_narrow_field — Class A asm-presence + byte-id for the - * sret callee's N_IDENT word-copy tail when the returned struct - * ends in a narrow primitive (bool / u8 / i8 / i16 / i32). - * - * Task #33. Cstage pre-fix used the slot-padded `rt->size` to drive - * the field-copy loop's MOVQ/MOVL/MOVB tail; for a struct whose - * natural size is unaligned (e.g. `{ i32, []u8, bool }` natural=33, - * padded=40), the trailing MOVQ at offset 32 widened a 1-byte bool - * into an 8-byte load+store — diverged from wwstage's correct MOVB - * (which used natural size via `sretretsize` / `structnaturalsize`). - * Corpus-coverage-blind: no in-tree stdlib struct had a bool field - * at any offset until lib/strings.iterator landed (Hare's - * `iterator { dec, reverse }` shape). - * - * Sentinel per row: in the `mk` body, between the final pre-RET - * "RAX = @sretarg load" pair (the SysV return-the-pointer - * discipline) and the first MOVQ word-store into (BX), assert - * the last copy instruction targets the narrow MOV width derived - * from the final field's type — NOT MOVQ. The trailing-field offset - * is hard-coded per row (structurally fixed) and we pin both the - * read-from-(BP) and store-to-(BX) sides. - * - * Plus byte-id between stages per row. - */ -#include -#include -#include -#include -#include -#include "wwtestpkg.h" - -static int -runwait(const char *cmd) -{ - int rc = system(cmd); - if (rc == -1) return -1; - if (WIFEXITED(rc)) return WEXITSTATUS(rc); - return -1; -} - -/* `off` = byte offset of the trailing narrow field inside the - * struct (= natural offset of that field). `mov` = expected mnemonic - * for the trailing field-copy at that offset (MOVB / MOVW / MOVL). - * Rows all share the shape `{ i32 a, []u8 s, NARROW r }` so a sits - * at 0..4, s at 8..32, r at 32..32+sz(r). */ -struct row { const char *label; const char *src; int off; const char *mov; }; - -static const struct row rows[] = { - { "trailing_bool", - "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", 32, "MOVB" }, - { "trailing_u8", - "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", 32, "MOVB" }, - /* Trailing i16: natural size 34. The chained loop has only - * MOVQ/MOVL/MOVB arms (matching wwstage's identical chain), - * so a 2-byte tail falls through to 2× MOVB at offsets 32, 33. - * That's still narrow + byte-id; the bug-check is the negative - * "no MOVQ at offset 32". Promoting both stages to a MOVW arm - * is a separate refactor (preserves byte-id but out of #33). */ - { "trailing_i16", - "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", 32, "MOVB" }, - { "trailing_i32", - "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", 32, "MOVL" }, -}; - -static int -emit_s(const char *w6c, const struct row *r, int i, char *out_s, size_t cap) -{ - char src[96], cmd[1024]; - snprintf(src, sizeof src, "/tmp/sret_narrow_%d_%d.ww", getpid(), i); - snprintf(out_s, cap, "/tmp/sret_narrow_%d_%d_%s.s", - getpid(), i, w6c[strlen(w6c) - 1] == 'w' ? "ww" : "c"); - - FILE *f = fopen(src, "wb"); - if (!f) return -1; - wwtest_fputs(r->src, f); - fclose(f); - - snprintf(cmd, sizeof cmd, "%s -o %s %s 2>/dev/null", w6c, out_s, src); - int rc = runwait(cmd); - unlink(src); - return rc; -} - -/* Asm-presence sentinel: inside mk's body (between `TEXT mk,` and - * the first `RET`), the trailing field-copy at offset `r->off` must - * use the narrow MOV mnemonic — both the read-from-(BP) and the - * store-to-(BX) lines. Pre-fix the cstage emitted MOVQ at that - * offset (8-byte over-wide) because the loop drove off slot-padded - * size; wwstage emitted the narrow MOV via natural size. */ -static int -check_narrow_mov(const char *path, const struct row *r) -{ - FILE *f = fopen(path, "rb"); - if (!f) return -1; - char line[1024]; - int in_mk = 0; - int saw_store = 0; - int saw_load = 0; - char store_needle[64], load_needle[64]; - snprintf(store_needle, sizeof store_needle, "%s\tAX, %d(BX)\n", - r->mov, r->off); - snprintf(load_needle, sizeof load_needle, "%d(BP), AX", - -16); /* not used as exact-match — see contains below */ - (void)load_needle; - while (fgets(line, sizeof line, f)) { - if (!in_mk) { - if (strstr(line, "TEXT main.mk,") - || strstr(line, "TEXT\tmain.mk,")) - in_mk = 1; - continue; - } - if (strstr(line, "\tRET\n")) break; - if (strstr(line, store_needle)) saw_store = 1; - /* load: `\tMOV?\t-K(BP), AX\n` with the matching width. */ - char loadkey[16]; - snprintf(loadkey, sizeof loadkey, "%s\t", r->mov); - if (strstr(line, loadkey) && strstr(line, "(BP), AX")) - saw_load = 1; - } - fclose(f); - if (!saw_store) { - fprintf(stderr, - "row[%s]: expected `%s AX, %d(BX)` in mk body\n", - r->label, r->mov, r->off); - return -1; - } - if (!saw_load) { - fprintf(stderr, - "row[%s]: expected narrow `%s -K(BP), AX` load in mk\n", - r->label, r->mov); - return -1; - } - return 0; -} - -/* Negative sentinel: pre-fix bug pattern. cstage emitted - * `MOVQ AX, off>(BX)` for the narrow trailing field — assert - * absence in mk body. */ -static int -check_no_movq_at_off(const char *path, const struct row *r) -{ - FILE *f = fopen(path, "rb"); - if (!f) return -1; - char line[1024]; - int in_mk = 0; - char needle[64]; - snprintf(needle, sizeof needle, "MOVQ\tAX, %d(BX)\n", r->off); - int bad = 0; - while (fgets(line, sizeof line, f)) { - if (!in_mk) { - if (strstr(line, "TEXT main.mk,") - || strstr(line, "TEXT\tmain.mk,")) - in_mk = 1; - continue; - } - if (strstr(line, "\tRET\n")) break; - if (strstr(line, needle)) { bad = 1; break; } - } - fclose(f); - if (bad) { - fprintf(stderr, - "row[%s]: unexpected `MOVQ AX, %d(BX)` (pre-#33" - " over-wide pattern) in mk\n", r->label, r->off); - return -1; - } - return 0; -} - -int -main(void) -{ - const char *bin = getenv("BIN"); - if (!bin) bin = "out/bin"; - 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; - } - - char w6c[640], w6c_ww[640]; - snprintf(w6c, sizeof w6c, "%s/w6c", bin); - snprintf(w6c_ww, sizeof w6c_ww, "%s/w6c_ww", bin); - - int have_ww = (access(w6c_ww, X_OK) == 0); - - int n = (int)(sizeof rows / sizeof rows[0]); - int total = 0, fail = 0; - - for (int i = 0; i < n; i++) { - char cs_path[128], ws_path[128]; - - if (emit_s(w6c, &rows[i], i, cs_path, sizeof cs_path) != 0) { - fprintf(stderr, "row[%s]: w6c failed\n", rows[i].label); - fail++; total++; continue; - } - total += 2; - if (check_narrow_mov(cs_path, &rows[i]) != 0) fail++; - if (check_no_movq_at_off(cs_path, &rows[i]) != 0) fail++; - - if (!have_ww) { unlink(cs_path); continue; } - - if (emit_s(w6c_ww, &rows[i], i, ws_path, sizeof ws_path) != 0) { - fprintf(stderr, - "row[%s]: w6c_ww failed\n", rows[i].label); - fail++; total++; - unlink(cs_path); - continue; - } - total += 2; - if (check_narrow_mov(ws_path, &rows[i]) != 0) fail++; - if (check_no_movq_at_off(ws_path, &rows[i]) != 0) fail++; - - total++; - char cmd[512]; - snprintf(cmd, sizeof cmd, "cmp -s %s %s", cs_path, ws_path); - if (runwait(cmd) != 0) { - fprintf(stderr, - "row[%s]: cstage vs wwstage asm differs\n", - rows[i].label); - fail++; - } - - unlink(cs_path); unlink(ws_path); - } - - if (fail) { - fprintf(stderr, - "sret_narrow_field: %d/%d fixtures failed\n", - fail, total); - return 1; - } - printf("sret_narrow_field: %d/%d ok\n", total, total); - return 0; -}