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:
@@ -1087,30 +1087,30 @@ unify_arith(Checker *c, Pos p, Type *a, Type *b)
|
|||||||
type_name(c->a, a), type_name(c->a, b));
|
type_name(c->a, a), type_name(c->a, b));
|
||||||
}
|
}
|
||||||
|
|
||||||
/* #104 fold-2: an un-suffixed float literal stays ty_untyped_float through
|
/* #104 fold-2 / #120: an un-suffixed float literal stays ty_untyped_float
|
||||||
* the checker, so fold-1's cgen narrow (gated on the node's f32-ness) never
|
* through the checker, so fold-1's cgen narrow (gated on the node's f32-ness,
|
||||||
* fires for `let x: f32 = 1.0` — the literal materialises as a double whose
|
* cgen.c:4163) never fires — the literal materialises as a double whose low 4
|
||||||
* low 4 bytes (0.0f for clean values) are what the f32 consumer reads. Stamp
|
* bytes (0.0f for clean values) are what the f32 consumer reads. Stamp such a
|
||||||
* such a literal f32 when an f32 target type is in context, mirroring harec's
|
* 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
|
* lower_implicit_cast (ref/harec/src/check.c:148): a flexible fconst adapts to
|
||||||
* bit pattern is target-dependent (unlike a width-agnostic int immediate),
|
* the hinted type exactly as a flexible iconst does. A float literal's bit
|
||||||
* so the value-producing node must carry the f32 type.
|
* 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
|
* 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.
|
* 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 —
|
* #120 broadens the reach (was let-init / return only): descend the
|
||||||
* because the wwstage cgen (selfhost/cmd/wcc/cgenutil.ww exprfloatkind) does
|
* lower_implicit_cast operand shapes so every untyped float LEAF in an f32
|
||||||
* NOT read node.type_ for a float literal: it hardcodes N_FLOATLIT -> f64 and
|
* context gets the stamp — a unary ± / paren-cast wrapper, both operands of an
|
||||||
* cgbin picks f32 off the OPERANDS' float-kind, not the node stamp. So the
|
* arith binop (harec lowers a binop's operands to its result type,
|
||||||
* wwstage materialiser (fold-1 isf32type, the one path that does read the
|
* ref/harec/src/check.c:1347-1348; this is the only path that reaches a
|
||||||
* stamp) narrows a let-init / return literal correctly, but a stamped literal
|
* literal-on-BOTH-sides `2.0 + 3.0` under an f32 target — narrowing per-leaf,
|
||||||
* inside an arith-binop or behind a unary minus is NOT narrowed by cgbin /
|
* never via an f64 intermediate that would double-round), and each element of
|
||||||
* the negate — cstage would emit ADDSS/SUBSS while wwstage emits ADDSD/SUBSD,
|
* an array literal against the array's element type. f64-only targets recurse
|
||||||
* breaking the cs==ww byte-id gate. Likewise the wwstage checker has no
|
* harmlessly (the leaf gate stays TY_F32). The (B) sibling-lowering of a
|
||||||
* N_ASSIGN check, no param-typed call-arg loop, and a head-only struct
|
* comparison's untyped operand — which has no f32 target above (its result is
|
||||||
* literal. binop / unary-minus / assign / call-arg / struct-field therefore
|
* bool) — lives in cbinop. */
|
||||||
* wait on the wwstage cgen + checker gaining those (#120). */
|
|
||||||
static void
|
static void
|
||||||
coerce_floatlit(Node *n, Type *target)
|
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
|
/* Chase the full alias chain (wwstage resolvealias does the same), so
|
||||||
* a doubly-aliased f32 target stamps in both stages or neither. */
|
* a doubly-aliased f32 target stamps in both stages or neither. */
|
||||||
Type *u = type_chase_named(target);
|
Type *u = type_chase_named(target);
|
||||||
if (u == NULL || u->kind != TY_F32)
|
if (u == NULL)
|
||||||
return;
|
return;
|
||||||
if (n->kind == N_FLOATLIT && n->type == ty_untyped_float)
|
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;
|
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
|
/* 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 *l = cexpr(c, n->lhs);
|
||||||
Type *r = cexpr(c, n->rhs);
|
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) {
|
switch (n->op) {
|
||||||
case TK_PLUS: case TK_MINUS: case TK_STAR: case TK_SLASH:
|
case TK_PLUS: case TK_MINUS: case TK_STAR: case TK_SLASH:
|
||||||
case TK_PERCENT:
|
case TK_PERCENT:
|
||||||
@@ -1892,6 +1940,8 @@ cexpr(Checker *c, Node *n)
|
|||||||
&& !assignable_addrfn(c, p->type, a))
|
&& !assignable_addrfn(c, p->type, a))
|
||||||
err(c, a->pos, "argument type %s not assignable to %s",
|
err(c, a->pos, "argument type %s not assignable to %s",
|
||||||
type_name(c->a, at), type_name(c->a, p->type));
|
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.
|
/* #258: `f(arr)` borrows the array as a full slice.
|
||||||
* #31/#33: a bare array LITERAL arg has no backing —
|
* #31/#33: a bare array LITERAL arg has no backing —
|
||||||
* loud-reject (supported only at a `let`). */
|
* loud-reject (supported only at a `let`). */
|
||||||
@@ -1924,6 +1974,8 @@ cexpr(Checker *c, Node *n)
|
|||||||
!assignable_addrfn(c, l, n->rhs))
|
!assignable_addrfn(c, l, n->rhs))
|
||||||
err(c, n->pos, "cannot assign %s to %s",
|
err(c, n->pos, "cannot assign %s to %s",
|
||||||
type_name(c->a, r), type_name(c->a, l));
|
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.
|
/* #258: `s = arr` borrows the array as a full slice.
|
||||||
* #31/#33: a bare array LITERAL rhs has no backing —
|
* #31/#33: a bare array LITERAL rhs has no backing —
|
||||||
* loud-reject (supported only at a `let`). */
|
* 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",
|
err(c, f->pos, "field %s: %s not assignable to %s",
|
||||||
f->str, type_name(c->a, vt),
|
f->str, type_name(c->a, vt),
|
||||||
type_name(c->a, match->type));
|
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;
|
return n->type = t;
|
||||||
|
|||||||
@@ -2696,28 +2696,71 @@ fn unifyarith(c: *checker, e: *syntax.node, ltn: *syntax.node, rtn: *syntax.node
|
|||||||
};
|
};
|
||||||
|
|
||||||
// coercefloatlit — twin of cstage cmd/wcc/check.c coerce_floatlit (see there
|
// coercefloatlit — twin of cstage cmd/wcc/check.c coerce_floatlit (see there
|
||||||
// for the full rationale + the rule-10 scope note). Stamp an un-suffixed
|
// for the full rationale). Stamp an un-suffixed float literal (whose type_ is
|
||||||
// float literal (whose type_ is the untyped_float singleton) as f32 when the
|
// the untyped_float singleton) as f32 when the target type resolves to f32, so
|
||||||
// target type resolves to f32, so fold-1's cgen narrow (isf32type, cgenexpr.
|
// fold-1's cgen narrow (isf32type, cgenexpr.ww) fires off the now-f32
|
||||||
// ww) fires off the now-f32 node.type_. SCOPED to a DIRECT untyped_float
|
// node.type_. SCOPED to untyped_float -> f32. #120 broadens the reach (was
|
||||||
// N_FLOATLIT at let-init / return only: the wwstage cgen's exprfloatkind
|
// let-init / return only): descend the harec lower_implicit_cast operand
|
||||||
// (cgenutil.ww) hardcodes N_FLOATLIT -> f64 and cgbin / the unary negate pick
|
// shapes so every untyped float LEAF in an f32 context gets the stamp — a
|
||||||
// f32 off the operands' float-kind, not the node stamp, so a stamped literal
|
// unary ± / paren-cast wrapper, both arith-binop operands (the only path that
|
||||||
// inside an arith-binop / behind a unary minus does NOT narrow there —
|
// reaches a literal-on-both-sides under an f32 target, per-leaf so no f64
|
||||||
// binop / unary-minus / assign / call-arg / struct-field wait on #120.
|
// intermediate double-rounds), and each array-literal element against the
|
||||||
|
// array's element type. The comparison sibling (no f32 target above) is in
|
||||||
|
// binoptype.
|
||||||
fn coercefloatlit(c: *checker, e: *syntax.node, target: *syntax.node) void = {
|
fn coercefloatlit(c: *checker, e: *syntax.node, target: *syntax.node) void = {
|
||||||
if (e == nil) { return; };
|
if (e == nil) { return; };
|
||||||
if (target == nil) { return; };
|
if (target == nil) { return; };
|
||||||
let tu: *syntax.node = resolvealias(c, unwrapbang(target));
|
let tu: *syntax.node = resolvealias(c, unwrapbang(target));
|
||||||
if (tu == nil) { return; };
|
if (tu == nil) { return; };
|
||||||
if (tu.kind != syntax.nkind.N_TNAME) { return; };
|
let k: syntax.nkind = e.kind;
|
||||||
if (!syntax.streq(tu.str, "f32")) { return; };
|
if (k == syntax.nkind.N_UN) {
|
||||||
if (e.kind == syntax.nkind.N_FLOATLIT) {
|
if (e.op == syntax.tkind.TK_MINUS || e.op == syntax.tkind.TK_PLUS) {
|
||||||
|
coercefloatlit(c, e.lhs, target);
|
||||||
|
};
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if (k == syntax.nkind.N_CAST) {
|
||||||
|
coercefloatlit(c, e.lhs, target);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if (k == syntax.nkind.N_BIN) {
|
||||||
|
if (e.op == syntax.tkind.TK_PLUS || e.op == syntax.tkind.TK_MINUS ||
|
||||||
|
e.op == syntax.tkind.TK_STAR || e.op == syntax.tkind.TK_SLASH) {
|
||||||
|
coercefloatlit(c, e.lhs, target);
|
||||||
|
coercefloatlit(c, e.rhs, target);
|
||||||
|
// harec lowers the binop's RESULT to the hint too, not only
|
||||||
|
// its operands. A literal-on-both-sides binop node stays
|
||||||
|
// untyped_float; cstage's arith cgen keys op-width on the
|
||||||
|
// BINOP node (cgen.c:4944), so the node must carry f32 to keep
|
||||||
|
// both stages on ADDSS. Mirror cstage coerce_floatlit N_BIN.
|
||||||
|
if (tu.kind == syntax.nkind.N_TNAME && syntax.streq(tu.str, "f32")) {
|
||||||
|
if ((e.type_: *syntax.tinfo) == c.tc.tyuntypedfloat) {
|
||||||
|
let f32b: *syntax.node = mktname(c, "f32");
|
||||||
|
e.type_ = tinfofornode(c, f32b): *void;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if (k == syntax.nkind.N_ARRLIT) {
|
||||||
|
if (tu.kind == syntax.nkind.N_TARRAY) {
|
||||||
|
let it: *syntax.node = e.list;
|
||||||
|
for (it != nil) {
|
||||||
|
coercefloatlit(c, it, tu.lhs);
|
||||||
|
it = it.next;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if (k == syntax.nkind.N_FLOATLIT) {
|
||||||
|
if (tu.kind == syntax.nkind.N_TNAME && syntax.streq(tu.str, "f32")) {
|
||||||
if ((e.type_: *syntax.tinfo) == c.tc.tyuntypedfloat) {
|
if ((e.type_: *syntax.tinfo) == c.tc.tyuntypedfloat) {
|
||||||
let f32t: *syntax.node = mktname(c, "f32");
|
let f32t: *syntax.node = mktname(c, "f32");
|
||||||
e.type_ = tinfofornode(c, f32t): *void;
|
e.type_ = tinfofornode(c, f32t): *void;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
return;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// coercerunelit — #29: an un-suffixed rune literal narrowing into an
|
// coercerunelit — #29: an un-suffixed rune literal narrowing into an
|
||||||
@@ -2771,6 +2814,16 @@ fn binoptype(c: *checker, e: *syntax.node) *syntax.node = {
|
|||||||
let op: syntax.tkind = e.op;
|
let op: syntax.tkind = e.op;
|
||||||
let ltn: *syntax.node = exprtype(c, e.lhs, nil);
|
let ltn: *syntax.node = exprtype(c, e.lhs, nil);
|
||||||
let rtn: *syntax.node = exprtype(c, e.rhs, nil);
|
let rtn: *syntax.node = exprtype(c, e.rhs, nil);
|
||||||
|
// #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. coercefloatlit's internal f32-target + untyped-leaf
|
||||||
|
// gates make each call inert unless the OTHER operand resolves f32 and
|
||||||
|
// this one carries an untyped float leaf; a both-untyped pair stays f64.
|
||||||
|
// Mirror cstage cbinop (cmd/wcc/check.c:1190).
|
||||||
|
coercefloatlit(c, e.rhs, ltn);
|
||||||
|
coercefloatlit(c, e.lhs, rtn);
|
||||||
// #38/F2: ptr ± int / int + ptr use intkindast (the type_isint mirror,
|
// #38/F2: ptr ± int / int + ptr use intkindast (the type_isint mirror,
|
||||||
// incl untyped_int / untyped_rune / alias-chased) — NOT isinttypeast,
|
// incl untyped_int / untyped_rune / alias-chased) — NOT isinttypeast,
|
||||||
// which misses untyped_int. cstage's ptr-arith arm gates on type_isint
|
// which misses untyped_int. cstage's ptr-arith arm gates on type_isint
|
||||||
@@ -3949,8 +4002,7 @@ fn exprtype(c: *checker, e: *syntax.node, hint: *syntax.node) *syntax.node = {
|
|||||||
let fi: *syntax.node = e.list;
|
let fi: *syntax.node = e.list;
|
||||||
for (fi != nil) {
|
for (fi != nil) {
|
||||||
if (fi.kind == syntax.nkind.N_FIELD
|
if (fi.kind == syntax.nkind.N_FIELD
|
||||||
&& fi.lhs != nil
|
&& fi.lhs != nil) {
|
||||||
&& fi.lhs.kind == syntax.nkind.N_ARRLIT) {
|
|
||||||
let ftn: *syntax.node = nil;
|
let ftn: *syntax.node = nil;
|
||||||
let tf: *syntax.node = stn.list;
|
let tf: *syntax.node = stn.list;
|
||||||
for (tf != nil) {
|
for (tf != nil) {
|
||||||
@@ -3960,9 +4012,15 @@ fn exprtype(c: *checker, e: *syntax.node, hint: *syntax.node) *syntax.node = {
|
|||||||
tf = tf.next;
|
tf = tf.next;
|
||||||
};
|
};
|
||||||
if (ftn != nil) {
|
if (ftn != nil) {
|
||||||
|
// #120: `S{ f: 1.0 }` narrows the
|
||||||
|
// field init to the field's f32.
|
||||||
|
// Mirror cstage N_STRUCTLIT site.
|
||||||
|
coercefloatlit(c, fi.lhs, ftn);
|
||||||
|
if (fi.lhs.kind == syntax.nkind.N_ARRLIT) {
|
||||||
checkarrlitfits(c, ftn, fi.lhs);
|
checkarrlitfits(c, ftn, fi.lhs);
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
};
|
||||||
fi = fi.next;
|
fi = fi.next;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -5666,6 +5724,10 @@ fn desugarcallargs(c: *checker, n: *syntax.node) void = {
|
|||||||
};
|
};
|
||||||
if (param.op != syntax.tkind.TK_ELLIPSIS) {
|
if (param.op != syntax.tkind.TK_ELLIPSIS) {
|
||||||
let atype: *syntax.node = exprtype(c, a, nil);
|
let atype: *syntax.node = exprtype(c, a, nil);
|
||||||
|
// #120: `f(1.0)` narrows the arg literal to the
|
||||||
|
// param's f32. Mirror cstage cmd/wcc/check.c N_CALL
|
||||||
|
// coerce_floatlit at the param-typed arg.
|
||||||
|
coercefloatlit(c, a, param.lhs);
|
||||||
// #29: a rune literal narrowing into an integer param
|
// #29: a rune literal narrowing into an integer param
|
||||||
// (`take('b')` where take(b: u8)). Override atype to the
|
// (`take('b')` where take(b: u8)). Override atype to the
|
||||||
// integer target so c3's general isassignable accepts,
|
// integer target so c3's general isassignable accepts,
|
||||||
@@ -5745,6 +5807,9 @@ fn checkassign(c: *checker, n: *syntax.node) void = {
|
|||||||
};
|
};
|
||||||
let ltn: *syntax.node = exprtype(c, n.lhs, nil);
|
let ltn: *syntax.node = exprtype(c, n.lhs, nil);
|
||||||
let rtn: *syntax.node = exprtype(c, n.rhs, nil);
|
let rtn: *syntax.node = exprtype(c, n.rhs, nil);
|
||||||
|
// #120: `w = 1.0` narrows the rhs literal to the lvalue's f32. Mirror
|
||||||
|
// cstage cmd/wcc/check.c N_ASSIGN coerce_floatlit.
|
||||||
|
coercefloatlit(c, n.rhs, ltn);
|
||||||
// #29: a rune literal narrowing into an integer assign/index-store
|
// #29: a rune literal narrowing into an integer assign/index-store
|
||||||
// target (`buf[i] = 'F'`). Override rtn to the integer target so a
|
// target (`buf[i] = 'F'`). Override rtn to the integer target so a
|
||||||
// general assign typecheck accepts, mirroring cstage's coarse
|
// general assign typecheck accepts, mirroring cstage's coarse
|
||||||
|
|||||||
138
test/lang/f32_untyped_narrow_test.ww
Normal file
138
test/lang/f32_untyped_narrow_test.ww
Normal 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);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user