diff --git a/cmd/w6c/cgen.c b/cmd/w6c/cgen.c index a4cb5259..34d32823 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -39,11 +39,6 @@ static int *cg_frame; * semantics for synthetic scratches). 0 means "not yet allocated"; * negative offsets returned by local_alloc are the live value. */ static int cg_retscr; -/* The aggregate-unwrap receiver must survive its producer call without - * perturbing SP: an outstanding PUSHQ at CALL violates SysV alignment. - * One BP-relative address spill per function mirrors wwstage's @-prefix - * scratch reuse. */ -static int cg_unwrapdst; /* Per-fn @tupfscr offset (single-slot SSoT). A multi-float tuple return * (#164/#107) spills each float out of X0 to this scratch as the L→R * element walk clobbers X0, then reloads X0/X1 by SSE index after the @@ -5367,14 +5362,12 @@ cgexpr(Cg *c, Node *n, Local *locals) fatal("#16: global/chained aggregate " "unwrap field dest unresolved " "(cgplaceaddr)"); - int dstscr; - if (cg_unwrapdst != 0) { - dstscr = cg_unwrapdst; - } else { - dstscr = local_alloc(c, &locals, "@unwrapdst", - 8, cg_frame); - cg_unwrapdst = dstscr; - } + /* Fresh per lowering: cgexpr(rhs) may recursively + * emit another #16 assignment while this address is + * live. Sharing one per-fn slot would let the inner + * assignment overwrite the outer receiver. */ + int dstscr = local_alloc(c, &locals, "@unwrapdst", + 8, cg_frame); ins2(c, A_MOVQ, areg(D_BX), amem(D_BP, dstscr)); cgexpr(c, n->rhs, locals); ins2(c, A_MOVQ, amem(D_BP, dstscr), areg(D_BX)); @@ -15803,7 +15796,6 @@ cgfn(Cg *c, FILE *out, Node *fn) nloops = 0; cg_ret_type = fn->type ? fn->type->ret : NULL; cg_retscr = 0; - cg_unwrapdst = 0; cg_tupfscr = 0; cg_tupargscr = 0; cg_tupargscr_sz = 0; diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index 25a31164..d4b18d67 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -8780,7 +8780,11 @@ fn cgassign(c: *cgen, n: *syntax.node) void = { os.write(2, m16.ptr, m16.len: u64); os.exit(1); }; - let dstscr: i32 = localadd(c, "@unwrapdst", 8, nil); + // Fresh per lowering: cgexpr(rhs) may recursively + // emit another #16 assignment while this address is + // live. Sharing one per-fn slot would let the inner + // assignment overwrite the outer receiver. + let dstscr: i32 = localalloc(c, "@unwrapdst", 8, nil); emitline("\tMOVQ\tBX, "); emitoff(dstscr: i64); emitline("(BP)\n"); diff --git a/test/asm/chain_test.ww b/test/asm/chain_test.ww index 80064370..761959d7 100644 --- a/test/asm/chain_test.ww +++ b/test/asm/chain_test.ww @@ -299,6 +299,36 @@ fn unwrapaligncheck(label: str, stage: str, s: str) void = { }; }; +fn unwrapnestedcheck(label: str, stage: str, s: str) void = { + let w: str = probewindow(label, stage, s); + if (!testenv.has(w, "TEXT main.probe,$48")) { + fail(label, strings.concat(stage, + ": nested spill frame is not $48")); + }; + if (testenv.has(w, "\tPUSHQ\tBX\n")) { + fail(label, strings.concat(stage, + ": destination PUSHQ remains in nested window")); + }; + let osave: i32 = testenv.pos(w, "\tMOVQ\tBX, -8(BP)\n"); + let choice: i32 = posafter(w, osave + 1, + "\tCALL\tmain.choice(SB)\n"); + let isave: i32 = posafter(w, choice + 1, + "\tMOVQ\tBX, -48(BP)\n"); + let make: i32 = posafter(w, isave + 1, + "\tCALL\tmain.make(SB)\n"); + let iload: i32 = posafter(w, make + 1, + "\tMOVQ\t-48(BP), BX\n"); + let pass: i32 = posafter(w, iload + 1, + "\tCALL\tmain.pass(SB)\n"); + let oload: i32 = posafter(w, pass + 1, + "\tMOVQ\t-8(BP), BX\n"); + if (osave < 0 || choice < 0 || isave < 0 || make < 0 + || iload < 0 || pass < 0 || oload < 0) { + fail(label, strings.concat(stage, + ": nested destination spills are not distinct and scoped")); + }; +}; + fn commandexit(dir: str, name: str, av: []str) i32 = { let co: testenv.commandout; testenv.runcommand(dir, dir, name, av, tmo(), &co); @@ -349,3 +379,65 @@ fn commandexit(dir: str, name: str, av: []str) i32 = { }; testenv.clean(td); }; + +// The outer destination stays live while its call argument lowers a +// match arm containing another #16 assignment. A per-fn shared spill +// redirected both stores to inner.f and left outer.f unchanged. +@test fn unwrapnestedlive() void = { + let label: str = "nested_global_unwrap_destination"; + let td: str = testenv.fresh(); + let src: str = strings.concat( + "package main;\n", + "type e = !i32;\n", + "type pair = struct { a: i64, b: i64 };\n", + "type box = struct { f: pair, guard: i64 };\n", + "let outer: box = box {\n", + " f = pair { a = 1i64, b = 2i64 }, guard = 7i64\n", + "};\n", + "let inner: box = box {\n", + " f = pair { a = 3i64, b = 4i64 }, guard = 8i64\n", + "};\n", + "fn make() (pair | e) = {\n", + " return pair { a = 111i64, b = 222i64 };\n", + "};\n", + "fn choice() (i32 | str) = { return 1i32; };\n", + "fn pass(x: i64) (pair | e) = {\n", + " return pair { a = 111i64 + x, b = 222i64 };\n", + "};\n", + "fn probe() i32 = {\n", + " outer.f = pass(match (choice()) {\n", + " case i32 => {\n", + " inner.f = make()!;\n", + " yield 0i64;\n", + " };\n", + " case str => yield 0i64;\n", + " })!;\n", + " if (outer.f.a != 111i64 || outer.f.b != 222i64", + " || outer.guard != 7i64) { return 91; };\n", + " if (inner.f.a != 111i64 || inner.f.b != 222i64", + " || inner.guard != 8i64) { return 92; };\n", + " return 37;\n", + "};\n", + "export fn main() i32 = { return probe(); };\n"); + 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")); + unwrapnestedcheck(label, "cstage", cs); + unwrapnestedcheck(label, "wwstage", ws); + if (!testenv.same(cs, ws)) { + fail(label, "cstage vs wwstage asm differs"); + }; + let out: str = strings.concat(td, "/unwrapnested"); + let bav: []str = [testenv.driver("ww"), "build", "-o", out, + "src.ww"]; + if (commandexit(td, "build", bav) != 0) { + fail(label, "nested runtime pin build failed"); + }; + let rav: []str = [out]; + if (commandexit(td, "run", rav) != 37) { + fail(label, "nested payload/neighbour runtime exit != 37"); + }; + testenv.clean(td); +};