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

@@ -1295,6 +1295,25 @@ cg_widen_tagged_store(Cg *c, Local **locals_p, Type *dst, Node *src,
if (via_outer) goto copy_out;
return;
}
/* Float arm: cgexpr on an f64/f32 source leaves the bit pattern in
* X0 only — the AX-store 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.
* Same kind-specific dispatch as the str/slice branches above and
* the structlit field-flow at the top of this function. */
int wid_isf32 = 0;
if (fld_isfloat(st, &wid_isf32)) {
int mov = wid_isf32 ? A_MOVSS : A_MOVSD;
cgexpr(c, src, *locals_p);
ins2(c, mov, areg(D_X0), amem(D_BP, write_off + 8));
int tag = cg_tag_for_variant(du, st);
ins2(c, A_MOVQ, aimm(tag < 0 ? 0 : tag),
amem(D_BP, write_off + 0));
if (via_outer) goto copy_out;
return;
}
/* Scalar / pointer / etc. The high slot word (when sz > 16) is
* left untouched here — match dispatches on the tag word first
* and only the str branch reads slot+16, so leaving the pad