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

@@ -1087,30 +1087,30 @@ unify_arith(Checker *c, Pos p, Type *a, Type *b)
type_name(c->a, a), type_name(c->a, b));
}
/* #104 fold-2: an un-suffixed float literal stays ty_untyped_float through
* the checker, so fold-1's cgen narrow (gated on the node's f32-ness) never
* fires for `let x: f32 = 1.0` — the literal materialises as a double whose
* low 4 bytes (0.0f for clean values) are what the f32 consumer reads. Stamp
* such a literal f32 when an f32 target type is in context, mirroring harec's
* lower_implicit_cast sites (ref/harec/src/check.c:148). A float literal's
* bit pattern is target-dependent (unlike a width-agnostic int immediate),
* so the value-producing node must carry the f32 type.
/* #104 fold-2 / #120: an un-suffixed float literal stays ty_untyped_float
* through the checker, so fold-1's cgen narrow (gated on the node's f32-ness,
* cgen.c:4163) never fires — the literal materialises as a double whose low 4
* bytes (0.0f for clean values) are what the f32 consumer reads. Stamp such a
* literal f32 when an f32 target type is in context, mirroring harec's
* lower_implicit_cast (ref/harec/src/check.c:148): a flexible fconst adapts to
* the hinted type exactly as a flexible iconst does. A float literal's bit
* pattern is target-dependent (unlike a width-agnostic int immediate), so the
* value-producing node must carry the f32 type.
*
* SCOPED to untyped_float -> f32 ONLY: untyped_float -> f64 already works via
* cgen's double default, so stamping it would broaden the surface for no gain.
*
* Symmetric subset (rule 10), DIRECT N_FLOATLIT at let-init / return only
* because the wwstage cgen (selfhost/cmd/wcc/cgenutil.ww exprfloatkind) does
* NOT read node.type_ for a float literal: it hardcodes N_FLOATLIT -> f64 and
* cgbin picks f32 off the OPERANDS' float-kind, not the node stamp. So the
* wwstage materialiser (fold-1 isf32type, the one path that does read the
* stamp) narrows a let-init / return literal correctly, but a stamped literal
* inside an arith-binop or behind a unary minus is NOT narrowed by cgbin /
* the negate — cstage would emit ADDSS/SUBSS while wwstage emits ADDSD/SUBSD,
* breaking the cs==ww byte-id gate. Likewise the wwstage checker has no
* N_ASSIGN check, no param-typed call-arg loop, and a head-only struct
* literal. binop / unary-minus / assign / call-arg / struct-field therefore
* wait on the wwstage cgen + checker gaining those (#120). */
* #120 broadens the reach (was let-init / return only): descend the
* lower_implicit_cast operand shapes so every untyped float LEAF in an f32
* context gets the stamp — a unary ± / paren-cast wrapper, both operands of an
* arith binop (harec lowers a binop's operands to its result type,
* ref/harec/src/check.c:1347-1348; this is the only path that reaches a
* literal-on-BOTH-sides `2.0 + 3.0` under an f32 target — narrowing per-leaf,
* never via an f64 intermediate that would double-round), and each element of
* an array literal against the array's element type. f64-only targets recurse
* harmlessly (the leaf gate stays TY_F32). The (B) sibling-lowering of a
* comparison's untyped operand — which has no f32 target above (its result is
* bool) — lives in cbinop. */
static void
coerce_floatlit(Node *n, Type *target)
{
@@ -1119,10 +1119,48 @@ coerce_floatlit(Node *n, Type *target)
/* Chase the full alias chain (wwstage resolvealias does the same), so
* a doubly-aliased f32 target stamps in both stages or neither. */
Type *u = type_chase_named(target);
if (u == NULL || u->kind != TY_F32)
if (u == NULL)
return;
if (n->kind == N_FLOATLIT && n->type == ty_untyped_float)
n->type = ty_f32;
switch (n->kind) {
case N_UN:
if (n->op == TK_MINUS || n->op == TK_PLUS)
coerce_floatlit(n->lhs, target);
return;
case N_CAST:
coerce_floatlit(n->lhs, target);
return;
case N_BIN:
switch (n->op) {
case TK_PLUS: case TK_MINUS: case TK_STAR: case TK_SLASH:
coerce_floatlit(n->lhs, target);
coerce_floatlit(n->rhs, target);
/* harec lowers the binop's RESULT to the hint too, not
* only its operands. A literal-on-both-sides binop's node
* stays ty_untyped_float (unify_arith of two untyped), and
* cstage's arith cgen keys the op/spill width on the BINOP
* node (cgen.c:4944 node_isf32(n)) — so without stamping the
* node f32 it emits ADDSD over the f32-narrowed operands
* (garbage), diverging from wwstage (which keys on operands).
* Stamping the node converges both stages on ADDSS. */
if (u->kind == TY_F32 && n->type == ty_untyped_float)
n->type = ty_f32;
break;
default:
break;
}
return;
case N_ARRLIT:
if (u->kind == TY_ARRAY)
for (Node *e = n->list; e; e = e->next)
coerce_floatlit(e, u->sub);
return;
case N_FLOATLIT:
if (u->kind == TY_F32 && n->type == ty_untyped_float)
n->type = ty_f32;
return;
default:
return;
}
}
/* desugar_arrayslice — #258. The single shared injection point for the
@@ -1189,6 +1227,16 @@ cbinop(Checker *c, Node *n)
{
Type *l = cexpr(c, n->lhs);
Type *r = cexpr(c, n->rhs);
/* #120 (B): a binop/compare with one f32 operand lowers an untyped-
* float peer to f32 — harec unifies both operands to the operand type
* (ref/harec/src/check.c:1347-1348). A comparison's result is bool, so
* no f32 target is above its operands and this sibling is their only
* lowering path. Each coerce is inert unless the PEER type resolves f32
* and this operand carries an untyped float leaf, so a both-untyped pair
* (`4.0 == 5.0`, no f32 context) stays f64. Held byte-id-symmetric with
* wwstage binoptype (two unconditional, internally-gated coerce calls). */
coerce_floatlit(n->rhs, l);
coerce_floatlit(n->lhs, r);
switch (n->op) {
case TK_PLUS: case TK_MINUS: case TK_STAR: case TK_SLASH:
case TK_PERCENT:
@@ -1892,6 +1940,8 @@ cexpr(Checker *c, Node *n)
&& !assignable_addrfn(c, p->type, a))
err(c, a->pos, "argument type %s not assignable to %s",
type_name(c->a, at), type_name(c->a, p->type));
/* #120: `f(1.0)` narrows the arg literal to the param's f32. */
coerce_floatlit(a, p->type);
/* #258: `f(arr)` borrows the array as a full slice.
* #31/#33: a bare array LITERAL arg has no backing —
* loud-reject (supported only at a `let`). */
@@ -1924,6 +1974,8 @@ cexpr(Checker *c, Node *n)
!assignable_addrfn(c, l, n->rhs))
err(c, n->pos, "cannot assign %s to %s",
type_name(c->a, r), type_name(c->a, l));
/* #120: `w = 1.0` narrows the rhs literal to the lvalue's f32. */
coerce_floatlit(n->rhs, l);
/* #258: `s = arr` borrows the array as a full slice.
* #31/#33: a bare array LITERAL rhs has no backing —
* loud-reject (supported only at a `let`). */
@@ -1966,6 +2018,9 @@ cexpr(Checker *c, Node *n)
err(c, f->pos, "field %s: %s not assignable to %s",
f->str, type_name(c->a, vt),
type_name(c->a, match->type));
/* #120: `S{ f: 1.0 }` narrows the init to the field's f32. */
if (match != NULL)
coerce_floatlit(f->lhs, match->type);
}
}
return n->type = t;