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"); };