check: narrow untyped float literals to f32 in f32 context (#120)

An untyped float literal defaults to f64, so in an f32 context it was
materialized as f64 then bit-truncated by a raw MOVSS (low-32 reinterpret)
rather than narrowed -- e.g. `let x: f32 = 2.0f32; x * 3.0` multiplied by
0.0f. Twelve byte-id-gate-blind both-wrong miscompiles, all this one cause
(compare, binop, call-arg, struct-field, array-elem against an untyped
literal).

Broaden coerce_floatlit to stamp the untyped fconst type_=f32 across the
f32-context sites (assign rhs, call-arg, struct-field, array-elem) and to
descend the implicit-cast shapes (peel unary +/-/cast, recurse binop
operands AND the binop node, recurse arrlit elems), mirroring harec's
lower_implicit_cast. The existing CVTSD2SS gate then fires; cgen is
unchanged. f64 contexts are untouched -- the stamp is gated on TY_F32.

Surfaced by the float codegen sub-hunt (= the deferred #120). Pinned by
test/lang/f32_untyped_narrow_test.ww (22 value-asserting rows incl. f64
controls; reddens on revert).
This commit is contained in:
2026-06-27 12:34:58 +09:00
parent 98b84986b7
commit e7effe5f57
3 changed files with 299 additions and 41 deletions

View File

@@ -0,0 +1,138 @@
// f32_untyped_narrow_test — #120: an UN-suffixed float literal (defaults to
// untyped_float/f64) used in an f32 context must narrow to single precision,
// exactly as the f32-suffixed literal already does (#104 fold-1). Pre-#120 the
// untyped literal stayed untyped_float through the checker, so cgen's CVTSD2SS
// narrow (gated on the node's f32 stamp) never fired: the literal materialised
// as a 64-bit double in X0 and the downstream f32 consumer MOVSS-read the low 4
// bytes (0.0f for clean values; garbage for others). The fix stamps the literal
// (and, for an arith binop, its operands AND result node) f32 in the checker at
// every harec lower_implicit_cast site — binop operands, comparison sibling,
// unary ±, assign rhs, param-typed call-arg, struct-field init, array element.
//
// Byte-id is BLIND to this class (both stages were wrong-but-identical), so the
// asserts below are VALUE-asserting: each redden on pre-fix HEAD (0.0f / false);
// the cstage `ww test` run is the live net, the T2 byte-id gate rides along.
//
// Test-design: ARITHMETIC rows use exactly-representable values (2/3/4/5/6) so a
// rounded product can't false-redden; pure COERCION round-trips use a rounding
// witness (0.1/0.2/1.3, whose f32 and f64 roundings differ) to also catch a
// low-32 reinterpret and a wrong-rounding regression.
package f32_untyped_narrow_test;
type S = struct { f: f32 };
fn ca(a: f32) f32 = { return a; };
// --- binop ×4 (exact) + operand-order ---
@test fn binop_mul() void = {
let x: f32 = 2.0f32;
assert(x * 3.0 == 6.0f32);
};
@test fn binop_add() void = {
let x: f32 = 2.0f32;
assert(x + 4.0 == 6.0f32);
};
@test fn binop_sub() void = {
let x: f32 = 5.0f32;
assert(x - 1.0 == 4.0f32);
};
@test fn binop_div() void = {
let x: f32 = 8.0f32;
assert(x / 2.0 == 4.0f32);
};
@test fn binop_lit_left() void = {
let x: f32 = 2.0f32;
assert(3.0 * x == 6.0f32);
};
// --- compare ×6 (exact); the !=, >, >= rows assert a FALSE result (their
// pre-fix low-32 garbage flips it true), so they need the negation. ---
@test fn cmp_eq() void = {
let y: f32 = 4.0f32;
assert(y == 4.0);
};
@test fn cmp_ne() void = {
let y: f32 = 4.0f32;
assert(!(y != 4.0));
};
@test fn cmp_lt() void = {
let y: f32 = 4.0f32;
assert(y < 5.0);
};
@test fn cmp_gt() void = {
let y: f32 = 4.0f32;
assert(!(y > 5.0));
};
@test fn cmp_le() void = {
let y: f32 = 4.0f32;
assert(y <= 4.0);
};
@test fn cmp_ge() void = {
let y: f32 = 4.0f32;
assert(!(y >= 5.0));
};
// --- unary neg (exact) ---
@test fn unary_neg() void = {
let z: f32 = -2.0;
assert(z == -2.0f32);
};
// --- assign rhs (witness) ---
@test fn assign_rhs() void = {
let w: f32 = 0.0f32;
w = 0.1;
assert(w == 0.1f32);
};
// --- param-typed call-arg (witness) ---
@test fn call_arg() void = {
assert(ca(0.1) == 0.1f32);
};
// --- struct-field init (witness) ---
@test fn struct_field() void = {
let s: S = S { f = 1.3 };
assert(s.f == 1.3f32);
};
// --- array element (witness, multi-leaf: each untyped leaf coerces alone) ---
@test fn array_elem() void = {
let a: [2]f32 = [0.1, 0.2];
assert(a[0] == 0.1f32);
assert(a[1] == 0.2f32);
};
// --- let-store (witness) ---
@test fn let_store() void = {
let v: f32 = 1.3;
assert(v == 1.3f32);
};
// --- literal-on-BOTH-sides under an f32 target (exact, must-green): the
// recursion narrows per-leaf AND stamps the binop result f32, so it computes
// 5.0f directly (no f64 intermediate). reddens with garbage if either half
// of the narrow fails. ---
@test fn both_sides() void = {
let q: f32 = 2.0 + 3.0;
assert(q == 5.0f32);
};
// --- CONTROL rows: green pre AND post-fix (no regression / no over-stamp). ---
@test fn ctrl_suffixed() void = {
let s: f32 = 0.1f32;
assert(s == 0.1f32);
};
@test fn ctrl_explicit_cast() void = {
assert((4.0: f32) == 4.0f32);
};
@test fn ctrl_no_f32_context() void = {
// no f32 anywhere → both literals default to f64; stays f64, false.
let b: bool = (4.0 == 5.0);
assert(b == false);
};
@test fn ctrl_f64_untouched() void = {
let d: f64 = 1.3;
assert(d == 1.3f64);
};