diff --git a/cmd/w6c/cgen.c b/cmd/w6c/cgen.c index 3eb99cfc..02752dfc 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -39,6 +39,11 @@ 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 @@ -5362,9 +5367,17 @@ cgexpr(Cg *c, Node *n, Local *locals) fatal("#16: global/chained aggregate " "unwrap field dest unresolved " "(cgplaceaddr)"); - ins1(c, A_PUSHQ, areg(D_BX)); + int dstscr; + if (cg_unwrapdst != 0) { + dstscr = cg_unwrapdst; + } else { + dstscr = local_alloc(c, &locals, "@unwrapdst", + 8, cg_frame); + cg_unwrapdst = dstscr; + } + ins2(c, A_MOVQ, areg(D_BX), amem(D_BP, dstscr)); cgexpr(c, n->rhs, locals); - ins1(c, A_POPQ, areg(D_BX)); + ins2(c, A_MOVQ, amem(D_BP, dstscr), areg(D_BX)); cg_agg_reg_store(c, &locals, D_BX, 0, ssz, 0); break; } @@ -5377,9 +5390,8 @@ cgexpr(Cg *c, Node *n, Local *locals) * explicit-deref form `(*p).f = ...` where the parser emits * N_UN(STAR, IDENT(p)). For (*p).f, retarget base to the * inner IDENT so the via_ptr branch fires identically to - * `p.f = v`. v1 scope: bare-IDENT inner only; (*expr).f - * (non-IDENT inner) falls through to the existing drop - * behaviour pending follow-up task. */ + * `p.f = v`. Other addressable bases fall through to the + * shared place resolver. */ if (!global_ptr_field_decline(n->lhs, n->op, locals) && n->lhs && n->lhs->kind == N_DOT && n->lhs->lhs && (n->lhs->lhs->kind == N_IDENT || @@ -5452,20 +5464,8 @@ cgexpr(Cg *c, Node *n, Local *locals) if (strcmp(fl->name, n->lhs->str) == 0) { f = fl; break; } if (f == NULL) break; - /* Tagged-union field: synthesise tag and store - * value bytes. Compound ops on tagged fields are - * not meaningful, so only plain `=` is wired. - * Three base shapes: - * - via_ptr: base is *struct local; address - * pre-loaded into BX. Buggy with a str - * variant since cgexpr will overwrite BX, - * but matches the existing pre-global - * behaviour. - * - is_global: struct global. LEAQ after - * cgexpr drops the slot address into CX - * without touching AX/BX, so str variants - * work cleanly. - * - else: struct local, BP-relative. */ + /* Compound ops on tagged fields are not meaningful, + * so only plain `=` is wired. */ Type *ft = f->type; /* Transitive chase (#5-F1 fold): 2-level alias * slice/str field skipped the 3-word arm — ptr @@ -11876,10 +11876,10 @@ cgexpr(Cg *c, Node *n, Local *locals) * IDENT(p)) with type T (post-deref struct). Pull the inner * IDENT in as dot_lhs so bt resolves to *T and the pointer- * auto-deref branch below fires (mirror of the N_ASSIGN - * N_DOT lhs retarget). v1 scope: N_IDENT inner only; - * (*expr).f follow-up task pending. Branches that gate on + * N_DOT lhs retarget). Branches that gate on * `n->lhs->kind == N_DOT/N_INDEX/...` keep checking the raw - * n->lhs since (*p) isn't either of those shapes. */ + * n->lhs since (*p) isn't either of those shapes; remaining + * addressable bases use the shared place resolver. */ Node *dot_lhs = n->lhs; if (dot_lhs && dot_lhs->kind == N_UN && dot_lhs->op == TK_STAR && dot_lhs->lhs && dot_lhs->lhs->kind == N_IDENT) @@ -15803,6 +15803,7 @@ 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 38a041ad..25a31164 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -3623,10 +3623,10 @@ fn cgdot(c: *cgen, n: *syntax.node) void = { // IDENT(p)). Substitute the inner IDENT as dotlhs so the // pointer-auto-deref branch (lhs.kind == N_IDENT && N_TPTR // tnode) fires the same as `p.f`. Mirror of the N_ASSIGN N_DOT - // lhs retarget in cgassign. v1 scope: N_IDENT inner only; - // (*expr).f follow-up task pending. Enum-leaf lookup above and + // lhs retarget in cgassign. Enum-leaf lookup above and // chained-N_DOT branches below keep checking raw lhs since - // (*p) is neither shape. + // (*p) is neither shape; remaining addressable bases use the + // shared place resolver. let dotlhs: *syntax.node = lhs; if (dotlhs != nil) { if (dotlhs.kind == syntax.nkind.N_UN) { @@ -8780,9 +8780,14 @@ fn cgassign(c: *cgen, n: *syntax.node) void = { os.write(2, m16.ptr, m16.len: u64); os.exit(1); }; - emitline("\tPUSHQ\tBX\n"); + let dstscr: i32 = localadd(c, "@unwrapdst", 8, nil); + emitline("\tMOVQ\tBX, "); + emitoff(dstscr: i64); + emitline("(BP)\n"); cgexpr(c, n.rhs); - emitline("\tPOPQ\tBX\n"); + emitline("\tMOVQ\t"); + emitoff(dstscr: i64); + emitline("(BP), BX\n"); cgaggregstore(c, "BX", 0, ssz, false); return; }; @@ -10876,7 +10881,8 @@ fn cgassign(c: *cgen, n: *syntax.node) void = { // is rare and not yet needed by our fixtures). Base accepts the // explicit-deref form `(*p).f = ...` (parser N_UN(STAR, IDENT)) // by retargeting to the inner IDENT so the via_ptr branch fires - // the same as auto-deref `p.f = v`. v1 scope: bare-IDENT inner. + // the same as auto-deref `p.f = v`. Other addressable bases use + // the shared place resolver below. if (lhs != nil) { if (lhs.kind == syntax.nkind.N_DOT) { let base: *syntax.node = lhs.lhs; diff --git a/test/asm/chain_test.ww b/test/asm/chain_test.ww index c99c47d2..80064370 100644 --- a/test/asm/chain_test.ww +++ b/test/asm/chain_test.ww @@ -271,3 +271,81 @@ fn dotbaserow(label: str, src: str, needle: str) void = { "export fn main() i32 = { return 0; };\n"), "\tMOVQ\t$32, CX\n"); }; + +fn unwrapaligncheck(label: str, stage: str, s: str) void = { + let w: str = probewindow(label, stage, s); + let call: i32 = testenv.pos(w, "\tCALL\tmain.mkpair(SB)\n"); + if (call < 0) { + fail(label, strings.concat(stage, ": no CALL main.mkpair")); + }; + let pre: str = strings.sub(w, 0, call); + let post: str = strings.sub(w, call, w.len); + if (testenv.has(pre, "\tPUSHQ\tBX\n")) { + fail(label, strings.concat(stage, + ": destination PUSHQ remains outstanding at CALL")); + }; + if (!testenv.has(pre, "\tMOVQ\tBX, -")) { + fail(label, strings.concat(stage, + ": destination address is not spilled BP-relative")); + }; + if (!testenv.has(post, "\tMOVQ\t-") + || !testenv.has(post, "(BP), BX\n")) { + fail(label, strings.concat(stage, + ": destination address is not reloaded after CALL")); + }; + if (!testenv.has(w, "TEXT main.probe,$16")) { + fail(label, strings.concat(stage, + ": dedicated address spill is absent from the frame")); + }; +}; + +fn commandexit(dir: str, name: str, av: []str) i32 = { + let co: testenv.commandout; + testenv.runcommand(dir, dir, name, av, tmo(), &co); + if (co.termination != exec.termination.EXIT) { return -1; }; + return co.code; +}; + +@test fn unwrapcallalign() void = { + let label: str = "global_unwrap_call_alignment"; + 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", + "fn mkpair(x: f64) (pair | e) = {\n", + " return pair { a = 111i64, b = 222i64 };\n", + "};\n", + "let gb: box = box {\n", + " f = pair { a = 9i64, b = 9i64 }, guard = 7i64\n", + "};\n", + "fn probe() i32 = {\n", + " gb.f = mkpair(1.25f64)!;\n", + " if (gb.f.a != 111i64 || gb.f.b != 222i64", + " || gb.guard != 7i64) { return 91; };\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")); + unwrapaligncheck(label, "cstage", cs); + unwrapaligncheck(label, "wwstage", ws); + if (!testenv.same(cs, ws)) { + fail(label, "cstage vs wwstage asm differs"); + }; + let out: str = strings.concat(td, "/unwrapalign"); + let bav: []str = [testenv.driver("ww"), "build", "-o", out, + "src.ww"]; + if (commandexit(td, "build", bav) != 0) { + fail(label, "runtime pin build failed"); + }; + let rav: []str = [out]; + if (commandexit(td, "run", rav) != 37) { + fail(label, "payload/neighbor runtime exit != 37"); + }; + testenv.clean(td); +};