cstage+selfhost+test: f64 variant-widen via MOVSD from X0 (#30)

Initializing a tagged-union variant slot with a runtime f64 source
(let, cast, fn call, unary, struct field, etc.) stored the i64 bit
pattern in the payload, not the float bit pattern. cgexpr leaves f64
in X0; the existing scalar-fallback MOVQ-from-AX wrote whatever was
last in AX (typically pre-conversion integer or stale residue).

Worker-fmtfloat surfaced this during #17 pre-flight (probe at
.ai/probe_f64_union_widen.ww). Blocks #17 fmt.float dispatch arm.
TK_FLOAT literals were coincidentally correct because the lowering
loads bits into AX before passing through X0 — the literal_1_0 test
row pins that as the principled MOVSD path now.

cstage cg_widen_tagged_store: add fld_isfloat arm between the slice
and scalar fallbacks. Emit MOVSD (f64) / MOVSS (f32) from X0 to the
payload offset, then the tag MOVQ. Mirrors existing str/slice/
structlit field-flow dispatchers.

Wwstage cgwidentaggedstorebp: mirror via exprfloatkind. Resolves a
secondary gap by looking up the variant tag directly via
flatvariantidx(c, dt, "f64"/"f32") — rhstargetname has no N_FLOATLIT
/ N_CALL / N_DOT branch and would fall through to str-fallback
returning tag 0.

No in-tree consumer triggered this pre-fix (no f64 in any tagged
union yet) — hence latent silence. arr[i]= and append() have the
same class gap but no in-tree exerciser today; same shape if/when
[N]f64 / []f64 land.

Test 715 (tagged_widen_f64): 7 rows × 2 stages = 14 fixtures with
bit-pinning via *u8 punning. literal_1_0 (regression lock-in),
cast_1_f64, call_makeone, unary_neg_f64, ident_f64, field_f64
(rob's extra row), i64_rhs_still_integer (negative control).
Diagnosable 0/1/2 return codes distinguish pass / wrong-tag /
wrong-payload.

ww2 == ww3 == ww4 byte-identical post-fix.
This commit is contained in:
2026-05-16 14:15:45 +09:00
parent 09ce249226
commit 82be8b9b4b
6 changed files with 356 additions and 0 deletions

View File

@@ -9110,6 +9110,37 @@ fn cgwidentaggedstorebp(c: *cgen, dst: *node, src: *node, slot_off: i32, slot_sz
emitline("(BP)\n");
return;
};
// Float arm: cgexpr on an f64/f32 source leaves the bit pattern in
// X0 only — the AX-store fallback below would silently write whatever
// was loaded into AX before the SSE conversion. Literal `1.0` works
// by coincidence (TK_FLOAT lowering loads the f64 bit pattern into AX
// before MOVSD'ing into X0); every runtime f64 shape (cast, call,
// unary, ident, struct-field load) needs the explicit MOVSD path.
// Mirror of cstage cg_widen_tagged_store's float arm. Wwstage has no
// checker so we classify via exprfloatkind (same shape used by cgcast)
// and resolve the variant tag by name directly — rhstargetname has no
// N_FLOATLIT / N_CALL / N_DOT branch and would fall through to the
// str-shape fallback that picks tag 0 for an `(i64 | f64)` union.
let fkind: i32 = exprfloatkind(c, src);
if (fkind != 0) {
let fmov: str = "MOVSD";
let fname: str = "f64";
if (fkind == 1) { fmov = "MOVSS"; fname = "f32"; };
cgexpr(c, src);
emitline("\t");
emitline(fmov);
emitline("\tX0, ");
emitoff((slot_off + 8): i64);
emitline("(BP)\n");
let ftag: i32 = flatvariantidx(c, dt, fname);
if (ftag < 0) { ftag = 0; };
emitline("\tMOVQ\t$");
emitint(ftag: i64);
emitline(", ");
emitoff(slot_off: i64);
emitline("(BP)\n");
return;
};
// Scalar payload.
cgexpr(c, src);
emitline("\tMOVQ\tAX, ");