From ef7c0c167538b120c6c6d1290ef1d64755587608 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Fri, 12 Jun 2026 21:14:57 +0900 Subject: [PATCH] wcc: widen-push spills the float payload from X0, both stages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Widening a runtime f64 into a tagged slot pushed a stale AX as the payload while the value sat in X0 — both stages shared the push bug (float literals dodged it because TK_FLOAT loads AX too); the divergent pop sides then produced different garbage. Spill the payload from X0 (MOVSD) with the variant tag. Review item #49. Both stages move in one commit: one emission contract; splitting would leave the byte-id gates red between the halves. --- cmd/w6c/cgen.c | 15 ++++++++++++++- selfhost/cmd/w6c/main.combined.ww | 19 ++++++++++++++++++- selfhost/cmd/wcc/cgenutil.ww | 19 ++++++++++++++++++- selfhost/cmd/wwdump/main.combined.ww | 19 ++++++++++++++++++- test/wcc/949_f9_float_run.c | 26 ++++++++++++++++++++++++++ 5 files changed, 94 insertions(+), 4 deletions(-) diff --git a/cmd/w6c/cgen.c b/cmd/w6c/cgen.c index 7e29af92..4a50e816 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -3108,7 +3108,20 @@ cg_widen_tagged_push(Cg *c, Local **locals_p, Type *dst, Node *src, int sz) ins2(c, A_XORQ, areg(D_DX), areg(D_DX)); ins1(c, A_PUSHQ, areg(D_DX)); } - ins1(c, A_PUSHQ, areg(D_AX)); /* value at +8 */ + /* #49: an f64/f32 payload sits in X0 (cgexpr left it there), + * not AX — spill it through the stack so the callee reads the + * real bits. A plain PUSHQ AX pushed whatever AX last held + * (stale for a runtime float producer; only a const folder + * leaves the bits in AX, which is why #48's no-payload-read arm + * passed but #49's `d == 2.5` read did not). Both stages + * (#263); wwstage cgenutil.ww scalar-variant twin. */ + if (node_isfloat(src)) { + int fmov = node_isf32(src) ? A_MOVSS : A_MOVSD; + ins2(c, A_SUBQ, aimm(8), areg(D_SP)); + ins2(c, fmov, areg(D_X0), amem(D_SP, 0)); + } else { + ins1(c, A_PUSHQ, areg(D_AX)); /* value at +8 */ + } ins2(c, A_MOVQ, aimm(tag), areg(D_AX)); ins1(c, A_PUSHQ, areg(D_AX)); /* tag at +0 */ return; diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index dde0d15d..dd8658fd 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -18215,7 +18215,24 @@ fn pushargsrev(c: *cgen, arg: *node, param: *node, memphase: bool) i32 = { emitline("\tPUSHQ\tDX\n"); pp -= 8; }; - emitline("\tPUSHQ\tAX\n"); + // #49: an f64/f32 payload sits in X0 (cgexpr left it + // there), not AX — spill it through the stack so the + // callee reads the real bits. A plain PUSHQ AX pushed + // whatever AX last held (stale for a runtime float + // producer; only a const folder leaves the bits in AX + // — why #48 with a no-payload-read arm passed but #49 + // reading `d == 2.5` did not). Both stages (#263); + // cstage cg_widen_tagged_push twin. + if (isfloattype(c, arg)) { + emitline("\tSUBQ\t$8, SP\n"); + let fmov: str = "MOVSD"; + if (isf32type(c, arg)) { fmov = "MOVSS"; }; + emitline("\t"); + emitline(fmov); + emitline("\tX0, (SP)\n"); + } else { + emitline("\tPUSHQ\tAX\n"); + }; emitline("\tMOVQ\t$"); emitint(widentag: i64); emitline(", AX\n"); diff --git a/selfhost/cmd/wcc/cgenutil.ww b/selfhost/cmd/wcc/cgenutil.ww index bc635d5a..542e12f0 100644 --- a/selfhost/cmd/wcc/cgenutil.ww +++ b/selfhost/cmd/wcc/cgenutil.ww @@ -501,7 +501,24 @@ fn pushargsrev(c: *cgen, arg: *node, param: *node, memphase: bool) i32 = { emitline("\tPUSHQ\tDX\n"); pp -= 8; }; - emitline("\tPUSHQ\tAX\n"); + // #49: an f64/f32 payload sits in X0 (cgexpr left it + // there), not AX — spill it through the stack so the + // callee reads the real bits. A plain PUSHQ AX pushed + // whatever AX last held (stale for a runtime float + // producer; only a const folder leaves the bits in AX + // — why #48 with a no-payload-read arm passed but #49 + // reading `d == 2.5` did not). Both stages (#263); + // cstage cg_widen_tagged_push twin. + if (isfloattype(c, arg)) { + emitline("\tSUBQ\t$8, SP\n"); + let fmov: str = "MOVSD"; + if (isf32type(c, arg)) { fmov = "MOVSS"; }; + emitline("\t"); + emitline(fmov); + emitline("\tX0, (SP)\n"); + } else { + emitline("\tPUSHQ\tAX\n"); + }; emitline("\tMOVQ\t$"); emitint(widentag: i64); emitline(", AX\n"); diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index ce54df58..08785573 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -18215,7 +18215,24 @@ fn pushargsrev(c: *cgen, arg: *node, param: *node, memphase: bool) i32 = { emitline("\tPUSHQ\tDX\n"); pp -= 8; }; - emitline("\tPUSHQ\tAX\n"); + // #49: an f64/f32 payload sits in X0 (cgexpr left it + // there), not AX — spill it through the stack so the + // callee reads the real bits. A plain PUSHQ AX pushed + // whatever AX last held (stale for a runtime float + // producer; only a const folder leaves the bits in AX + // — why #48 with a no-payload-read arm passed but #49 + // reading `d == 2.5` did not). Both stages (#263); + // cstage cg_widen_tagged_push twin. + if (isfloattype(c, arg)) { + emitline("\tSUBQ\t$8, SP\n"); + let fmov: str = "MOVSD"; + if (isf32type(c, arg)) { fmov = "MOVSS"; }; + emitline("\t"); + emitline(fmov); + emitline("\tX0, (SP)\n"); + } else { + emitline("\tPUSHQ\tAX\n"); + }; emitline("\tMOVQ\t$"); emitint(widentag: i64); emitline(", AX\n"); diff --git a/test/wcc/949_f9_float_run.c b/test/wcc/949_f9_float_run.c index a88c5735..44235d66 100644 --- a/test/wcc/949_f9_float_run.c +++ b/test/wcc/949_f9_float_run.c @@ -97,6 +97,32 @@ static const struct row rows[] = { " let d: f64 = 3.5;\n" " return g(d);\n" "};\n", 2, K_RUN, NULL }, + /* #49 (#263 both-stages): a RUNTIME f64 producer (mk) feeds a value + * widened into (f64|void), and the f64 arm READS the payload + * (d == 2.5). Pre-fix the widen-PUSH did `PUSHQ AX` for the payload + * while the f64 sat in X0, so d read stale bits → 1 (cs) / 3 (ww, + * the #48 pop divergence compounding). #48's arm dodged this by not + * reading the payload; this one catches the stale-AX push. Both + * stages now spill X0 → 0. (After the c4 drain fix the two stages + * already agree on the pop; this push fix closes the shared bug.) */ + { "runtime_float_widen_payload", + "package main;\n" + "type fv = (f64 | void);\n" + "fn mk(x: f64) f64 = { return x + 1.5; };\n" + "fn take(v: fv) i32 = {\n" + " match (v) {\n" + " case let d: f64 => {\n" + " if (d == 2.5) { return 0; };\n" + " return 1;\n" + " };\n" + " case void => { return 2; };\n" + " };\n" + " return 3;\n" + "};\n" + "export fn main() i32 = {\n" + " let d: f64 = mk(1.0);\n" + " return take(d);\n" + "};\n", 0, K_RUN, NULL }, }; static int