diff --git a/cmd/w6c/cgen.c b/cmd/w6c/cgen.c index c29ce03f..0e65c974 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -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; } diff --git a/test/wcc/715_tagged_widen_f64.c b/test/wcc/715_tagged_widen_f64.c index c4c3471d..d4d8b88f 100644 --- a/test/wcc/715_tagged_widen_f64.c +++ b/test/wcc/715_tagged_widen_f64.c @@ -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",