cstage+test: variant-widen f64 arm accepts TY_UNTYPED_FLOAT (#40)

#30 (82be8b9) shipped the f64 variant-widen MOVSD path, but
cg_widen_tagged_store's float-arm gate `fld_isfloat` only accepted
declared f64/f32 — not TY_UNTYPED_FLOAT. cunop on TK_MINUS over an
N_FLOATLIT returns the operand's type (ty_untyped_float), and
cbinop on two untyped-floats returns ty_untyped_float too. So
`let a: (i64 | f64) = -2.5;` and `(2.5 + 1.0)` fell through to the
scalar fallback and stored AX residue at payload+8 (tag still set
correctly, payload = 0).

Wwstage post-#30 was already correct via exprfloatkind's AST walk.

Extend fld_isfloat to accept TY_UNTYPED_FLOAT (defaults to f64, no
TY_UNTYPED_F32 exists). Acceptance set now matches cg_isfloat
exactly. All 15 other fld_isfloat call sites pass declared field /
element / pointee types that never carry TY_UNTYPED_* post-check —
no over-trigger.

Test 715 grows from 7 → 10 rows: unary_neg_floatlit_direct (`-2.5`),
unary_neg_floatlit_paren (`-(2.5)`), binop_floatlit_sum (`2.5+1.0`).
All pin payload bits via hex-u64 punning through *u8 — direct/paren
land 0xC004000000000000 (sign=1, exp=0x400, mant=0x4000000000000);
sum lands 0x400C000000000000 (3.5). Hex literal use is documented
inline pointing at #41 (orthogonal comparison-ladder bug surfaced
during test development; decimal-u64 RHS of != miscompiles).

After this lands, lib/fmt/fmttest.ww's 3 routed-around rows (cited
at 7f320d3) can drop the `let nv: f64 = -2.5;` indirection and use
the direct literal — sibling cleanup.
This commit is contained in:
2026-05-16 14:54:11 +09:00
parent 7f320d3e75
commit 4d6a19fc8a
2 changed files with 57 additions and 6 deletions

View File

@@ -113,12 +113,24 @@ node_isf32(Node *n)
return n && type_isf32(n->type);
}
/* fld_isfloat — true iff f's underlying type is f32 or f64. The cgen
* passes float values in X0 (via MOVSD/MOVSS), integer/ptr values in
* AX (via MOVQ). Without this check, a field store/load on an f64 slot
* runs through AX and the bits never reach the SSE side — see the
* vfloat / L.curfval traps documented in examples/lisp/CLAUDE.md.
* Sets *isf32 to 1 for f32, 0 for f64. */
/* fld_isfloat — true iff f's underlying type is f32, f64, or
* untyped_float. The cgen passes float values in X0 (via MOVSD/MOVSS),
* integer/ptr values in AX (via MOVQ). Without this check, a field
* store/load on an f64 slot runs through AX and the bits never reach
* the SSE side — see the vfloat / L.curfval traps documented in
* examples/lisp/CLAUDE.md.
*
* TY_UNTYPED_FLOAT defaults to f64 (no TY_UNTYPED_F32 exists). Every
* field/element/pointee caller passes a declared type that is never
* UNTYPED — adding the case is a no-op for them. The variant-widen
* call site (cg_widen_tagged_store) is the only one passing an
* expression type, where `let _: (i64|f64) = -2.5;` arrives with
* src->type = ty_untyped_float (cunop returns the operand type for
* TK_MINUS, untyped_float for an untyped float literal). The earlier
* narrow predicate dropped the payload via the AX scalar fallback —
* matches cg_isfloat's acceptance set now.
*
* Sets *isf32 to 1 for f32, 0 for f64 / untyped_float. */
static int
fld_isfloat(Type *t, int *isf32)
{
@@ -127,6 +139,7 @@ fld_isfloat(Type *t, int *isf32)
if (t->kind == TY_NAMED) t = t->under;
if (t == NULL) return 0;
if (t->kind == TY_F64) return 1;
if (t->kind == TY_UNTYPED_FLOAT) return 1;
if (t->kind == TY_F32) { if (isf32) *isf32 = 1; return 1; }
return 0;
}

View File

@@ -117,6 +117,44 @@ static const struct row rows[] = {
/* -1.0 == 0xBFF0000000000000 == 13830554455654793216 */
"13830554455654793216", "1"),
0 },
/* #40: N_UN(MINUS, N_FLOATLIT) where the inner literal is untyped.
* cunop returns the operand type for TK_MINUS, so the expression's
* type is ty_untyped_float; the pre-fix fld_isfloat predicate
* accepted only TY_F32/TY_F64 and the variant-widen float arm
* silently fell through to MOVQ AX, dropping the X0 result onto
* a trashed AX. -2.5 == 0xC004000000000000.
*
* Hex u64 literal pins the bit pattern directly. Decimal literals
* above 2^63 round-trip cleanly through the lexer (cgexpr_int sees
* the right bits) but the surrounding comparison ladder has an
* unrelated latent miscompile on huge-decimal-u64 RHS — orthogonal
* to this fix; do not entangle. */
{ "unary_neg_floatlit_direct",
BITCHECK_F64(
"",
"let a: (i64 | f64) = -2.5;\n",
"0xC004000000000000", "1"),
0 },
/* Same expression shape with explicit parens around the literal.
* Parser builds N_UN(MINUS, N_FLOATLIT) for both forms; pinning
* both rows guards against a future parse change altering that. */
{ "unary_neg_floatlit_paren",
BITCHECK_F64(
"",
"let a: (i64 | f64) = -(2.5);\n",
"0xC004000000000000", "1"),
0 },
/* Binop of two untyped-float literals — N_BIN type is
* ty_untyped_float (untyped+untyped → untyped, see check.c cbinop).
* Same predicate gate as the unary case; pins that the extended
* fld_isfloat covers the binop arm too. 2.5 + 1.0 == 3.5 ==
* 0x400C000000000000. */
{ "binop_floatlit_sum",
BITCHECK_F64(
"",
"let a: (i64 | f64) = (2.5 + 1.0);\n",
"0x400C000000000000", "1"),
0 },
/* Concrete f64 ident — cgexpr emits MOVSD load to X0; AX is
* never touched. */
{ "ident_f64",