wcc: stamp un-suffixed f32-context float literals (#104 fold-2)

fold-1 narrows a float literal at materialisation only when its node
already carries an f32 type — the `f32` suffix. The common un-suffixed
case `let x: f32 = 1.0` stays ty_untyped_float through the checker, so
the node is never f32-typed: the literal materialises as a 64-bit double
and the f32 consumer reads the low 4 bytes (0.0f for clean values).

Stamp such a literal f32 when an f32 target type is in context, the way
harec's lower_implicit_cast does (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 type. Scoped to
untyped_float -> f32 only (f64 already works via cgen's double default).

coerce_floatlit (cstage clet + cstmt N_RETURN) / coercefloatlit (wwstage
resolvewalk's post-order N_LET / N_RETURN handler) are logically
identical. The wwstage stamp is placed AFTER the child re-walk: the
post-order exprtype dispatch re-stamps a bare N_FLOATLIT back to
untyped_float, so coercing earlier (checkletassign) would be undone.

Scope is let-init and return ONLY, aligned down to the leaner wwstage
(rule 10). The wwstage cgen's exprfloatkind hardcodes a float literal to
f64 and cgbin / the unary negate pick f32 off the operands, not the node
stamp — so a stamped literal in an arith-binop / behind a unary minus
narrows in cstage (ADDSS) but not wwstage (ADDSD), a byte-id break. The
wwstage checker also has no assign / param-typed call-arg / per-field
struct-lit site. binop, unary-minus, assign, call-arg, struct-field wait
on #120 (wwstage cgen + checker build-out).

965_f32stamp_run: cstage run + cs==ww byte-id over un-suffixed let-init
and return literals, the hole 964 left open. Regen w6c/wwdump
combined.ww embeds.
This commit is contained in:
2026-05-26 12:06:47 +09:00
parent acaf0152da
commit a1dff13ec1
6 changed files with 374 additions and 0 deletions

View File

@@ -786,6 +786,46 @@ 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.
*
* 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). */
static void
coerce_floatlit(Node *n, Type *target)
{
if (n == NULL || target == NULL)
return;
/* Chase the full alias chain (wwstage resolvealias does the same), so
* a doubly-aliased f32 target stamps in both stages or neither. */
Type *u = target;
while (u && u->kind == TY_NAMED)
u = u->under;
if (u == NULL || u->kind != TY_F32)
return;
if (n->kind == N_FLOATLIT && n->type == ty_untyped_float)
n->type = ty_f32;
}
static Type *
cbinop(Checker *c, Node *n)
{
@@ -1757,6 +1797,8 @@ clet(Checker *c, Node *n)
!type_assignable(declared, initt))
err(c, n->pos, "init %s not assignable to declared %s",
type_name(c->a, initt), type_name(c->a, declared));
/* #104 fold-2: `let x: f32 = 1.0` — narrow the init literal to f32. */
coerce_floatlit(n->rhs, declared);
n->type = t;
if (n->str && n->str[0]) {
check_module_shadow(c, n->str, n->pos, "let");
@@ -1795,6 +1837,8 @@ cstmt(Checker *c, Node *n)
&& !type_assignable(c->ret, rt))
err(c, n->pos, "return %s not assignable to %s",
type_name(c->a, rt), type_name(c->a, c->ret));
/* #104 fold-2: `fn g() f32 = { return 1.0; }` — narrow to f32. */
coerce_floatlit(n->lhs, c->ret);
break;
}
case N_IF: {