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:
101
cmd/wcc/check.c
101
cmd/wcc/check.c
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user