fix(ir): float / folds as FLOAT division under the unified narrowing rule — int folder refuses a float-operand / [F0.11]
The shared compile-time integer folder (`evalConstIntExpr`) accepts an integral float literal/const as an integer leaf (`[4.0]` → 4) and then applied INTEGER arithmetic to the whole expression — so `5.0 / 2.0` folded as `divTrunc(5,2)` = 2 instead of float division (`2.5`). The bug fired at all FIVE unified-rule sites (typed local, field default, param default, typed const, array dimension), because the typed sites evaluate through `evalConstFloatExpr` (which delegates the node to the int folder) and the count sites through `foldCountI64` (int folder first). Fix at the single root: `evalConstIntExpr`'s `.div` arm refuses to fold a division whose lhs/rhs is float-valued (`isFloatValuedExpr`), so the value surfaces through `evalConstFloatExpr` + the unified rule — an integral quotient (`6.0 / 2.0` → 3) folds, a non-integral one (`5.0 / 2.0` = 2.5, mixed `5 / 2.0`, float-const `F / G`) errors. Genuine integer `/` (`5 / 2` → 2) is unchanged; `*`/`+`/`-` need no guard (they agree between int and float for the integral operands the int folder ever sees). `isFloatValuedExpr` judges a const-leaf by VALUE (`moduleConstIsFloatTyped` recurses into the const's value with the existing cycle-guard frame), so an untyped float-EXPRESSION const (`ME :: 4.0 + 1.0`, placeholder type s64) is caught at both the count path and — via `foldComptimeFloatInit`'s guard — the typed-binding path. A backtick RAW receiver (`` `f64.epsilon ``) is a field read, not a float limit (is_raw check, issues 0092/0093). Regression: examples/1147 (negative — `5.0 / 2.0` errors at all five sites plus untyped float-EXPR const div); 0168 extended (positive — `6.0 / 2.0`, `12.0 / 4.0`, `[6.0/2.0]`, `xx (5.0/2.0)` → 2); unit tests "the int folder refuses a FLOAT division" and "moduleConstIsFloatTyped judges a const by VALUE". specs.md + readme.md state the float-`/` rule.
This commit is contained in:
@@ -30,6 +30,7 @@ Box :: struct {
|
||||
n : s64 = 4.0; // integral float field default → folds to 4
|
||||
ne : s64 = M + 2.0; // integral int-const-EXPR field default → folds to 4
|
||||
nf : s64 = F + 1.5; // integral float-const-LEAF field default → folds to 4
|
||||
nd : s64 = 8.0 / 2.0; // integral float-DIVISION field default → folds to 4
|
||||
}
|
||||
|
||||
withDefault :: (x : s64 = 6.0) -> s64 { return x; } // param default → 6
|
||||
@@ -37,6 +38,7 @@ withFlt :: (x : s64 = F + 1.5) -> s64 { return x; } // float-const-leaf pa
|
||||
|
||||
K : s64 : 8.0; // integral float module const → folds to 8
|
||||
KF : s64 : F + 1.5; // integral float-const-LEAF module const → folds to 4
|
||||
KD : s64 : 12.0 / 4.0; // integral float-DIVISION module const → folds to 3
|
||||
|
||||
ArrFE :: [F + 1.5]s64; // array-dim type ALIAS over a float-const-leaf expr → [4]s64
|
||||
// (the stateless registration path must agree with the
|
||||
@@ -53,9 +55,16 @@ main :: () {
|
||||
neg : s64 = -2.0;
|
||||
print("neg={}\n", neg);
|
||||
|
||||
// Struct field defaults fold (literal + int-const expr + float-const leaf).
|
||||
// Integral float DIVISION folds (the subtle case: integral operands, but the
|
||||
// `/` is float division). `6.0 / 2.0` = 3.0 → 3; the int folder refuses the
|
||||
// float `/` and the unified rule folds the integral result.
|
||||
zd : s64 = 6.0 / 2.0;
|
||||
print("localDiv={}\n", zd);
|
||||
|
||||
// Struct field defaults fold (literal + int-const expr + float-const leaf +
|
||||
// float division).
|
||||
b := Box.{};
|
||||
print("field={} fieldExpr={} fieldFlt={}\n", b.n, b.ne, b.nf);
|
||||
print("field={} fieldExpr={} fieldFlt={} fieldDiv={}\n", b.n, b.ne, b.nf, b.nd);
|
||||
|
||||
// Param defaults fold.
|
||||
print("param={} paramFlt={}\n", withDefault(), withFlt());
|
||||
@@ -64,6 +73,11 @@ main :: () {
|
||||
a : [K]s64 = ---;
|
||||
print("const={} constFlt={} len={}\n", K, KF, a.len);
|
||||
|
||||
// Integral float-DIVISION const folds, and drives an array dimension directly
|
||||
// (`[6.0 / 2.0]` → len 3) through the SAME refuse-int-fold / fold-float rule.
|
||||
ad2 : [6.0 / 2.0]s64 = ---;
|
||||
print("constDiv={} dimDiv={}\n", KD, ad2.len);
|
||||
|
||||
// Array DIMENSION — the fifth site joins the unified rule: an integral
|
||||
// float-const-leaf expression folds to a count whether written DIRECTLY
|
||||
// (`[F + 1.5]` → 4), THROUGH a float-expr const (`[KF]`, KF = F + 1.5 = 4),
|
||||
@@ -100,5 +114,6 @@ main :: () {
|
||||
xf : s64 = xx (F + 0.25);
|
||||
xl : s64 = xx (f64.true_min + 0.5);
|
||||
xm : s64 = xx (5.5 % 2.0);
|
||||
print("xx={} cast={} xxExpr={} xxFlt={} xxLimit={} xxMod={}\n", e, c, xc, xf, xl, xm);
|
||||
xd : s64 = xx (5.0 / 2.0); // non-integral float DIVISION → truncates to 2
|
||||
print("xx={} cast={} xxExpr={} xxFlt={} xxLimit={} xxMod={} xxDiv={}\n", e, c, xc, xf, xl, xm, xd);
|
||||
}
|
||||
|
||||
46
examples/1147-diagnostics-float-division-narrowing.sx
Normal file
46
examples/1147-diagnostics-float-division-narrowing.sx
Normal file
@@ -0,0 +1,46 @@
|
||||
// Unified float→int narrowing rule (F0.11), float-DIVISION pin: a compile-time
|
||||
// float division (`5.0 / 2.0` = 2.5) is a NON-INTEGRAL float, so narrowing it
|
||||
// implicitly into an integer-typed binding is a COMPILE ERROR — exactly like any
|
||||
// other non-integral float (example 1146). The division is the subtle case: its
|
||||
// operands (`5.0`, `2.0`) are individually INTEGRAL, so a naive integer fold
|
||||
// would truncate `5.0 / 2.0` to 2 with no diagnostic. The rule fires at all five
|
||||
// sites — a typed module CONST, a struct FIELD default, a function PARAM default,
|
||||
// a typed LOCAL, and an array DIMENSION — because the shared compile-time integer
|
||||
// folder now refuses a division with a float operand, deferring it to the float
|
||||
// evaluator + the unified rule (integral folds, non-integral errors). A float
|
||||
// operand on either side (literal or float-typed const) makes the `/` a float
|
||||
// division.
|
||||
//
|
||||
// The escape hatch stays open: `xx (5.0 / 2.0)` truncates to 2 with no error, and
|
||||
// an INTEGRAL float division (`6.0 / 2.0` → 3) folds — both exercised on the
|
||||
// POSITIVE side (example 0168).
|
||||
//
|
||||
// Regression (issue 0095, F0.11-6): `5.0 / 2.0` at a typed local, field default,
|
||||
// param default, typed const, and array dimension all silently folded to 2 via
|
||||
// integer truncating division; each now rejects the non-integral float.
|
||||
#import "modules/std.sx";
|
||||
|
||||
// An UNTYPED float-EXPRESSION const carries a placeholder `s64` type, yet its
|
||||
// value is float — `ME / 2` is still float division and must reject (judged by
|
||||
// the const's VALUE, not its declared type), at BOTH the typed-binding path and
|
||||
// the count path.
|
||||
ME :: 4.0 + 1.0; // untyped float-EXPRESSION const (= 5.0)
|
||||
|
||||
// Typed CONST: declared but not referenced, so the single narrowing error is not
|
||||
// followed by a downstream "unresolved const" cascade.
|
||||
K : s64 : 5.0 / 2.0; // 2.5 non-integral float-DIVISION const → error
|
||||
|
||||
BadField :: struct {
|
||||
f : s64 = 5.0 / 2.0; // non-integral float-DIVISION field default → error
|
||||
}
|
||||
|
||||
badParam :: (x : s64 = 5.0 / 2.0) -> s64 { return x; } // float-DIVISION param default → error
|
||||
|
||||
main :: () {
|
||||
local : s64 = 5.0 / 2.0; // non-integral float-DIVISION local → error
|
||||
dim : [5.0 / 2.0]s64 = ---; // non-integral float-DIVISION array dimension → error
|
||||
cdiv : s64 = ME / 2; // untyped float-EXPR const division (5.0/2 = 2.5) → error
|
||||
cdim : [ME / 2]s64 = ---; // same, at the count path → error
|
||||
b := BadField.{};
|
||||
print("{} {} {} {} {} {}\n", local, b.f, badParam(), dim.len, cdiv, cdim.len);
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
local=4 localExpr=4 localFlt=4
|
||||
neg=-2
|
||||
field=4 fieldExpr=4 fieldFlt=4
|
||||
localDiv=3
|
||||
field=4 fieldExpr=4 fieldFlt=4 fieldDiv=4
|
||||
param=6 paramFlt=4
|
||||
const=8 constFlt=4 len=8
|
||||
constDiv=3 dimDiv=3
|
||||
dim.direct=4 dim.const=4 dim.alias=4
|
||||
limit=0 fmod=2
|
||||
intlimit=127 intcount=255
|
||||
xx=4 cast=1 xxExpr=2 xxFlt=2 xxLimit=0 xxMod=1
|
||||
xx=4 cast=1 xxExpr=2 xxFlt=2 xxLimit=0 xxMod=1 xxDiv=2
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
1
|
||||
@@ -0,0 +1,41 @@
|
||||
error: cannot implicitly narrow non-integral float '2.5' to 's64'; use an explicit cast (`xx`/`cast`)
|
||||
--> examples/1147-diagnostics-float-division-narrowing.sx:31:11
|
||||
|
|
||||
31 | K : s64 : 5.0 / 2.0; // 2.5 non-integral float-DIVISION const → error
|
||||
| ^^^^^^^^^
|
||||
|
||||
error: cannot implicitly narrow non-integral float '2.5' to 's64'; use an explicit cast (`xx`/`cast`)
|
||||
--> examples/1147-diagnostics-float-division-narrowing.sx:40:19
|
||||
|
|
||||
40 | local : s64 = 5.0 / 2.0; // non-integral float-DIVISION local → error
|
||||
| ^^^^^^^^^
|
||||
|
||||
error: array dimension must be an integer, but '2.5' is a non-integral float
|
||||
--> examples/1147-diagnostics-float-division-narrowing.sx:41:12
|
||||
|
|
||||
41 | dim : [5.0 / 2.0]s64 = ---; // non-integral float-DIVISION array dimension → error
|
||||
| ^^^^^^^^^
|
||||
|
||||
error: cannot implicitly narrow non-integral float '2.5' to 's64'; use an explicit cast (`xx`/`cast`)
|
||||
--> examples/1147-diagnostics-float-division-narrowing.sx:42:18
|
||||
|
|
||||
42 | cdiv : s64 = ME / 2; // untyped float-EXPR const division (5.0/2 = 2.5) → error
|
||||
| ^^^^^^
|
||||
|
||||
error: array dimension must be an integer, but '2.5' is a non-integral float
|
||||
--> examples/1147-diagnostics-float-division-narrowing.sx:43:13
|
||||
|
|
||||
43 | cdim : [ME / 2]s64 = ---; // same, at the count path → error
|
||||
| ^^^^^^
|
||||
|
||||
error: cannot implicitly narrow non-integral float '2.5' to 's64'; use an explicit cast (`xx`/`cast`)
|
||||
--> examples/1147-diagnostics-float-division-narrowing.sx:34:15
|
||||
|
|
||||
34 | f : s64 = 5.0 / 2.0; // non-integral float-DIVISION field default → error
|
||||
| ^^^^^^^^^
|
||||
|
||||
error: cannot implicitly narrow non-integral float '2.5' to 's64'; use an explicit cast (`xx`/`cast`)
|
||||
--> examples/1147-diagnostics-float-division-narrowing.sx:37:24
|
||||
|
|
||||
37 | badParam :: (x : s64 = 5.0 / 2.0) -> s64 { return x; } // float-DIVISION param default → error
|
||||
| ^^^^^^^^^
|
||||
@@ -141,6 +141,30 @@
|
||||
> f64.max` → 0, `6.0 % 4.0` → 2, integer-limit `s8.max`/`[u8.max]` unregressed,
|
||||
> `xx` escapes for both new forms) and 1146 (negative: `f64.true_min + 0.5` and
|
||||
> `5.5 % 2.0` error at a binding site).
|
||||
>
|
||||
> **Completion (F0.11 attempt 6)** — attempt 5 reached evaluator parity for
|
||||
> leaves/operators, but a structural hole remained in the SHARED integer folder:
|
||||
> `evalConstIntExpr` accepts an integral float literal/const as an integer leaf
|
||||
> (`[4.0]` → 4) and then applies INTEGER arithmetic to the whole expression — so a
|
||||
> float DIVISION with integral-looking operands (`5.0 / 2.0`) folded as integer
|
||||
> truncating division (`divTrunc(5,2)` = 2) instead of float division (`2.5`). The
|
||||
> bug fired at ALL FIVE sites (`5.0 / 2.0` printed `2` at a typed local, field
|
||||
> default, param default, typed const, and array dimension), because the typed
|
||||
> sites evaluate through `evalConstFloatExpr` (which delegates the whole node to
|
||||
> the int folder) and the count sites through `foldCountI64` (which tries the int
|
||||
> folder first). Closed at the single root: `evalConstIntExpr`'s `.div` arm now
|
||||
> REFUSES to fold a division whose lhs/rhs is float-valued (a new
|
||||
> `isFloatValuedExpr` predicate, resolving a float-typed const leaf through each
|
||||
> ctx's `nameIsFloatTyped`) — so the division surfaces through `evalConstFloatExpr`
|
||||
> (float `/`) + the unified rule: an integral quotient (`6.0 / 2.0` → 3) folds, a
|
||||
> non-integral one (`5.0 / 2.0` = 2.5, mixed `5 / 2.0`, float-const `F / G`)
|
||||
> errors. Genuine integer `/` (`5 / 2` → 2) is unchanged; `*`/`+`/`-` need no guard
|
||||
> (they agree between int and float for the integral operands the int folder ever
|
||||
> sees). Regression: `examples/1147-diagnostics-float-division-narrowing.sx`
|
||||
> (negative — `5.0 / 2.0` errors at all five sites), the integral-`/` positives
|
||||
> added to `examples/0168` (`6.0 / 2.0` local/field, `12.0 / 4.0` const, `[6.0 /
|
||||
> 2.0]` dim, `xx (5.0 / 2.0)` → 2), and unit
|
||||
> `program_index.test.zig` "the int folder refuses a FLOAT division".
|
||||
|
||||
## Symptom
|
||||
A typed LOCAL (and likely typed param/field) silently truncates a floating-point
|
||||
|
||||
@@ -130,8 +130,10 @@ array dimension uses: an **integral** compile-time float folds to its integer, a
|
||||
or *any* compile-time-constant float expression — including one that references a
|
||||
float-typed const (`F : f64 : 2.5; y : s64 = F + 1.5` → `4`), a builtin float
|
||||
numeric-limit accessor (`f64.max - f64.max` → `0`, while `f64.true_min + 0.5`
|
||||
errors), or a float `%` (`6.0 % 4.0` → `2`, while `5.5 % 2.0` = `1.5` errors): the
|
||||
compile-time float evaluator recognises every leaf shape the integer one does, so
|
||||
errors), a float `%` (`6.0 % 4.0` → `2`, while `5.5 % 2.0` = `1.5` errors), or a
|
||||
float `/` (`6.0 / 2.0` → `3`, while `5.0 / 2.0` = `2.5` errors — a float `/` is
|
||||
always float division, never integer truncation, even with integral operands):
|
||||
the compile-time float evaluator recognises every leaf shape the integer one does, so
|
||||
no constant float form escapes the rule at one site while folding at another — and
|
||||
is uniform
|
||||
across a typed local, a parameter default, a struct field default, a call
|
||||
|
||||
19
specs.md
19
specs.md
@@ -898,7 +898,10 @@ is written — a literal (`4.0`), a float-typed const (`N : f64 : 4.0`), or a
|
||||
const **expression** whose value is integral, including one built from a
|
||||
non-integral float-const leaf (`F : f64 : 2.5; [F + 1.5]s64` ≡ `[4]s64`, and
|
||||
likewise through a const, `K : s64 : F + 1.5; [K]s64`), a builtin float
|
||||
numeric-limit accessor (`[f64.max - f64.max]s64` → length 0), or a float `%`. A
|
||||
numeric-limit accessor (`[f64.max - f64.max]s64` → length 0), a float `%`, or a
|
||||
float `/` whose quotient is integral (`[6.0 / 2.0]s64` ≡ `[3]s64`; a non-integral
|
||||
quotient like `[5.0 / 2.0]s64` = 2.5 is rejected — a float `/` is always float
|
||||
division, never integer truncation, even when both operands are integral). A
|
||||
count and a typed
|
||||
binding's float→integer initializer share the *same* compile-time float
|
||||
evaluation, so they agree at every site — direct, through a const, or via a type
|
||||
@@ -1440,14 +1443,18 @@ array dimension / lane count uses (see "Array dimensions are integral", §2):
|
||||
here is *any* compile-time-constant float expression — an integer-const leaf
|
||||
(`M + 2.0`), a float-typed const leaf (`F : f64 : 2.5; y : s64 = F + 1.5` → 4),
|
||||
a builtin float numeric-limit accessor (`f64.max - f64.max` → 0), a float `%`
|
||||
(`6.0 % 4.0` → 2), or any combination of them. The compile-time float evaluator
|
||||
recognises every leaf/operator shape the integer evaluator does (literal, named
|
||||
const, numeric-limit accessor, `+ - * / %`, unary negate), so no constant float
|
||||
form folds at one site while truncating at another.
|
||||
(`6.0 % 4.0` → 2), or a float `/` whose quotient is integral (`6.0 / 2.0` → 3),
|
||||
or any combination of them. The compile-time float evaluator recognises every
|
||||
leaf/operator shape the integer evaluator does (literal, named const,
|
||||
numeric-limit accessor, `+ - * / %`, unary negate), so no constant float form
|
||||
folds at one site while truncating at another. A float `/` is always FLOAT
|
||||
division even when both operands are integral — `6.0 / 2.0` is `3.0` (folds),
|
||||
but `5.0 / 2.0` is `2.5` (errors) — never integer truncating division.
|
||||
- A **non-integral** compile-time float — literal OR const expression — is a
|
||||
**compile error** with one uniform wording at every site:
|
||||
`y : s64 = 1.5`, `y : s64 = M + 0.5`, `y : s64 = F + 0.25` (= 2.75),
|
||||
`y : s64 = f64.true_min + 0.5` (= 0.5), and `y : s64 = 5.5 % 2.0` (= 1.5) all →
|
||||
`y : s64 = f64.true_min + 0.5` (= 0.5), `y : s64 = 5.5 % 2.0` (= 1.5), and
|
||||
`y : s64 = 5.0 / 2.0` (= 2.5) all →
|
||||
"cannot implicitly narrow non-integral float '…' to 's64'; use an explicit
|
||||
cast (`xx`/`cast`)".
|
||||
- This applies uniformly to a typed **local**, a function **param default**, a
|
||||
|
||||
@@ -12137,6 +12137,16 @@ pub const Lowering = struct {
|
||||
return program_index_mod.moduleConstFloat(&self.program_index.module_const_map, &self.module.types, name);
|
||||
}
|
||||
|
||||
/// True iff `name` is a FLOAT-valued module const (`F : f64 : 2.5`,
|
||||
/// `K : f64 : 4.0`, untyped `M :: 4.0`, untyped-EXPR `ME :: 4.0 + 1.0`). The
|
||||
/// int folder's division arm consults this so a `/` with a float-const operand
|
||||
/// is recognised as float division (issue 0095 / F0.11-6). Comptime / generic
|
||||
/// value bindings are always integer-valued, so only the module-const table
|
||||
/// can name a float.
|
||||
pub fn nameIsFloatTyped(self: *Lowering, name: []const u8) bool {
|
||||
return program_index_mod.moduleConstIsFloatTyped(&self.program_index.module_const_map, &self.module.types, name);
|
||||
}
|
||||
|
||||
/// Resolve a name to a compile-time integer across the three const tables.
|
||||
fn comptimeIntNamed(self: *Lowering, name: []const u8) ?i64 {
|
||||
if (self.comptime_constants.get(name)) |cv| switch (cv) {
|
||||
@@ -14644,8 +14654,12 @@ pub const Lowering = struct {
|
||||
// Only a FLOAT-flavored initializer narrows here; a plain comptime int
|
||||
// (`5`, `M + 2`) is left to the normal integer path. Safe to infer now —
|
||||
// `evalConstFloatExpr` only succeeds for literal / const-arithmetic
|
||||
// nodes, never an unbound pack index.
|
||||
if (!isFloat(self.inferExprType(node))) return null;
|
||||
// nodes, never an unbound pack index. `inferExprType` is the primary
|
||||
// signal, but it reads a const's DECLARED type — which is a placeholder
|
||||
// `s64` for an untyped float-EXPRESSION const (`ME :: 4.0 + 1.0`), so
|
||||
// `ME / 2` would look like integer division; `isFloatValuedExpr` (judging
|
||||
// by VALUE) catches that case so it narrows under the unified rule too.
|
||||
if (!isFloat(self.inferExprType(node)) and !program_index_mod.isFloatValuedExpr(node, self)) return null;
|
||||
// Integral comptime float folds to its int (`floatToIntExact`, the same
|
||||
// facility the array-dim / `$K: Count` paths use); a non-integral one is
|
||||
// the narrowing error.
|
||||
|
||||
@@ -104,6 +104,10 @@ const DimCtx = struct {
|
||||
pub fn lookupDimName(_: DimCtx, name: []const u8) ?i64 {
|
||||
if (std.mem.eql(u8, name, "M")) return 4;
|
||||
if (std.mem.eql(u8, name, "N")) return 6;
|
||||
// `K : f64 : 4.0` is an INTEGRAL float const: it folds to 4 through the
|
||||
// int delegation (`floatToIntExact`) yet stays float-typed — the case the
|
||||
// division guard must still recognise as float division.
|
||||
if (std.mem.eql(u8, name, "K")) return 4;
|
||||
return null;
|
||||
}
|
||||
// `xs` stands in for a pack of arity 3; every other name has no pack length.
|
||||
@@ -113,12 +117,19 @@ const DimCtx = struct {
|
||||
}
|
||||
// `F` stands in for a NON-INTEGRAL float module const (`F : f64 : 2.5`): the
|
||||
// int folder cannot resolve it, so only the float-leaf lookup surfaces it.
|
||||
// Integer consts (`M`/`N`) are resolved by the int delegation and never reach
|
||||
// this arm; `Z` is genuinely runtime.
|
||||
// `K` stands in for an INTEGRAL float const (`K : f64 : 4.0`) — it folds to 4
|
||||
// through the int delegation yet is still float-typed. Integer consts (`M`/`N`)
|
||||
// are resolved by the int delegation and never reach this arm; `Z` is runtime.
|
||||
pub fn lookupFloatName(_: DimCtx, name: []const u8) ?f64 {
|
||||
if (std.mem.eql(u8, name, "F")) return 2.5;
|
||||
if (std.mem.eql(u8, name, "K")) return 4.0;
|
||||
return null;
|
||||
}
|
||||
// The float-typed-const predicate the division guard consults: `F`/`K` are
|
||||
// float-typed module consts, every other name is not.
|
||||
pub fn nameIsFloatTyped(_: DimCtx, name: []const u8) bool {
|
||||
return std.mem.eql(u8, name, "F") or std.mem.eql(u8, name, "K");
|
||||
}
|
||||
};
|
||||
|
||||
fn nLit(v: i64) ast.Node {
|
||||
@@ -271,6 +282,53 @@ test "moduleConstInt folds expression-RHS consts and rejects cycles" {
|
||||
try std.testing.expect(pi.moduleConstInt(&map, &table, "C") == null);
|
||||
}
|
||||
|
||||
test "moduleConstIsFloatTyped judges a const by VALUE, catching untyped float-EXPR consts" {
|
||||
var table = types.TypeTable.init(std.testing.allocator);
|
||||
defer table.deinit();
|
||||
var map = std.StringHashMap(pi.ModuleConstInfo).init(std.testing.allocator);
|
||||
defer map.deinit();
|
||||
|
||||
// KT : f64 : 4.0 (typed float), MI :: 2 (untyped int), ML :: 5.0 (untyped
|
||||
// float literal → f64), ME :: 4.0 + 1.0 (untyped float EXPRESSION, placeholder
|
||||
// type s64 yet float-valued), IE :: 1 + 2 (untyped int expression).
|
||||
var kt_val = nFloat(4.0);
|
||||
var mi_val = nLit(2);
|
||||
var ml_val = nFloat(5.0);
|
||||
var four = nFloat(4.0);
|
||||
var one_f = nFloat(1.0);
|
||||
var me_val = nBin(.add, &four, &one_f);
|
||||
var l1 = nLit(1);
|
||||
var l2 = nLit(2);
|
||||
var ie_val = nBin(.add, &l1, &l2);
|
||||
try map.put("KT", .{ .value = &kt_val, .ty = .f64 });
|
||||
try map.put("MI", .{ .value = &mi_val, .ty = .s64 });
|
||||
try map.put("ML", .{ .value = &ml_val, .ty = .f64 }); // pass-0 stores a float literal as f64
|
||||
try map.put("ME", .{ .value = &me_val, .ty = .s64 }); // pass-0 placeholder for a binary_op
|
||||
try map.put("IE", .{ .value = &ie_val, .ty = .s64 });
|
||||
|
||||
// Float-valued: a typed float const, an untyped float literal, AND an untyped
|
||||
// float EXPRESSION whose declared type is the s64 placeholder (judged by value).
|
||||
try std.testing.expect(pi.moduleConstIsFloatTyped(&map, &table, "KT"));
|
||||
try std.testing.expect(pi.moduleConstIsFloatTyped(&map, &table, "ML"));
|
||||
try std.testing.expect(pi.moduleConstIsFloatTyped(&map, &table, "ME"));
|
||||
// NOT float-valued: an int const, an int expression, an absent name.
|
||||
try std.testing.expect(!pi.moduleConstIsFloatTyped(&map, &table, "MI"));
|
||||
try std.testing.expect(!pi.moduleConstIsFloatTyped(&map, &table, "IE"));
|
||||
try std.testing.expect(!pi.moduleConstIsFloatTyped(&map, &table, "absent"));
|
||||
|
||||
// A cyclic const has no value: the frame guard returns false without looping.
|
||||
var a_id = nIdent("A");
|
||||
var b_id = nIdent("B");
|
||||
var az = nFloat(0.0);
|
||||
var a_val = nBin(.add, &b_id, &az);
|
||||
var b_val = nBin(.add, &a_id, &az);
|
||||
try map.put("A", .{ .value = &a_val, .ty = .s64 });
|
||||
try map.put("B", .{ .value = &b_val, .ty = .s64 });
|
||||
// The `+ 0.0` literal still makes them float-valued (a finite, non-cyclic leaf
|
||||
// is reached before the cycle); the point is it TERMINATES.
|
||||
try std.testing.expect(pi.moduleConstIsFloatTyped(&map, &table, "A"));
|
||||
}
|
||||
|
||||
test "moduleConstInt gates the fold on the declared type, not the initializer node" {
|
||||
var table = types.TypeTable.init(std.testing.allocator);
|
||||
defer table.deinit();
|
||||
@@ -446,3 +504,56 @@ test "foldCountI64 / foldDimU32 fold an integral float count, reject a non-integ
|
||||
var negf = nNeg(&f4); // -4.0 → -4
|
||||
try std.testing.expectEqual(pi.DimU32{ .below_min = -4 }, pi.foldDimU32(&negf, ctx, 0));
|
||||
}
|
||||
|
||||
test "the int folder refuses a FLOAT division (issue 0095 / F0.11-6)" {
|
||||
const eval = pi.evalConstIntExpr;
|
||||
const ctx = DimCtx{}; // K : f64 : 4.0 (integral float const), M = 4 (int const)
|
||||
|
||||
var five = nLit(5);
|
||||
var two = nLit(2);
|
||||
var six = nLit(6);
|
||||
var f5 = nFloat(5.0);
|
||||
var f2 = nFloat(2.0);
|
||||
var f6 = nFloat(6.0);
|
||||
var k = nIdent("K"); // integral float const (folds to 4, yet float-typed)
|
||||
var m = nIdent("M"); // integer const (4)
|
||||
|
||||
// Genuine INTEGER division still truncates (`5 / 2` → 2, `6 / 2` → 3).
|
||||
var idiv = nBin(.div, &five, &two);
|
||||
var idiv2 = nBin(.div, &six, &two);
|
||||
try std.testing.expectEqual(@as(?i64, 2), eval(&idiv, ctx));
|
||||
try std.testing.expectEqual(@as(?i64, 3), eval(&idiv2, ctx));
|
||||
|
||||
// FLOAT division is REFUSED by the int folder (returns null), even when the
|
||||
// result is integral (`6.0 / 2.0`) — so it surfaces through the float folder
|
||||
// + the unified narrowing rule instead of truncating. A float operand on
|
||||
// either side (literal or float-typed const) is enough.
|
||||
var fdiv_nonint = nBin(.div, &f5, &f2); // 5.0 / 2.0 = 2.5
|
||||
var fdiv_int = nBin(.div, &f6, &f2); // 6.0 / 2.0 = 3.0 (integral, still refused)
|
||||
var fdiv_mixedl = nBin(.div, &f5, &two); // 5.0 / 2 = 2.5 (mixed promotes to float)
|
||||
var fdiv_mixedr = nBin(.div, &five, &f2); // 5 / 2.0 = 2.5
|
||||
var fdiv_const = nBin(.div, &k, &two); // K / 2 = 4.0/2 = 2.0 (float const, refused)
|
||||
try std.testing.expect(eval(&fdiv_nonint, ctx) == null);
|
||||
try std.testing.expect(eval(&fdiv_int, ctx) == null);
|
||||
try std.testing.expect(eval(&fdiv_mixedl, ctx) == null);
|
||||
try std.testing.expect(eval(&fdiv_mixedr, ctx) == null);
|
||||
try std.testing.expect(eval(&fdiv_const, ctx) == null);
|
||||
|
||||
// The float folder recovers the TRUE float value of the refused divisions, so
|
||||
// the unified rule can fold the integral one and reject the non-integral one.
|
||||
const evalf = pi.evalConstFloatExpr;
|
||||
try std.testing.expectEqual(@as(?f64, 2.5), evalf(&fdiv_nonint, ctx));
|
||||
try std.testing.expectEqual(@as(?f64, 3.0), evalf(&fdiv_int, ctx));
|
||||
try std.testing.expectEqual(@as(?f64, 2.0), evalf(&fdiv_const, ctx));
|
||||
// An int-const division (`M / 2` = 4/2) is NOT float division — it truncates.
|
||||
var mdiv = nBin(.div, &m, &two);
|
||||
try std.testing.expectEqual(@as(?i64, 2), eval(&mdiv, ctx));
|
||||
|
||||
// Non-division float arithmetic is unaffected: `*`/`+`/`-` over integral
|
||||
// operands agree between int and float, so they still fold via the int folder
|
||||
// (`6.0 * 2.0` → 12, `K - 2.0` → 2).
|
||||
var fmul = nBin(.mul, &f6, &f2); // 6.0 * 2.0 = 12
|
||||
var ksub = nBin(.sub, &k, &f2); // K - 2.0 = 2
|
||||
try std.testing.expectEqual(@as(?i64, 12), eval(&fmul, ctx));
|
||||
try std.testing.expectEqual(@as(?i64, 2), eval(&ksub, ctx));
|
||||
}
|
||||
|
||||
@@ -106,8 +106,38 @@ const ModuleConstCtx = struct {
|
||||
pub fn lookupFloatName(self: ModuleConstCtx, name: []const u8) ?f64 {
|
||||
return moduleConstFloatFramed(self.consts, self.table, name, self.frame);
|
||||
}
|
||||
/// True iff `name` names a FLOAT-valued const (see `moduleConstFloatValuedFramed`),
|
||||
/// resolved through the SAME cycle-guarded frame so a float-const leaf that
|
||||
/// references another const is judged consistently with `lookupFloatName`.
|
||||
pub fn nameIsFloatTyped(self: ModuleConstCtx, name: []const u8) bool {
|
||||
return moduleConstFloatValuedFramed(self.consts, self.table, name, self.frame);
|
||||
}
|
||||
};
|
||||
|
||||
/// True iff `ty` is a float type — one half of the float-valued-const test the
|
||||
/// int folder's division arm relies on. Module consts only ever carry the builtin
|
||||
/// `f32` / `f64`.
|
||||
fn isFloatConstType(ty: TypeId) bool {
|
||||
return ty == .f32 or ty == .f64;
|
||||
}
|
||||
|
||||
/// True iff `name` is a FLOAT-valued module const — judged by the const's VALUE,
|
||||
/// not only its DECLARED type, so it catches both a typed float const
|
||||
/// (`K : f64 : 4.0`, `F : f64 : 2.5`) AND an UNTYPED float-EXPRESSION const
|
||||
/// (`ME :: 4.0 + 1.0`), whose pass-0 placeholder type is `s64` even though its
|
||||
/// value is float. The int folder's division arm consults this to tell a FLOAT
|
||||
/// division apart from an integer one even when both operands fold to integers
|
||||
/// (`K / 3`, `ME / 3`). `frame` cycle-guards a const whose value references
|
||||
/// another const; a name already on the chain has no compile-time value → not
|
||||
/// float-valued (issue 0095 / F0.11-6).
|
||||
fn moduleConstFloatValuedFramed(consts: *const std.StringHashMap(ModuleConstInfo), table: *const types.TypeTable, name: []const u8, parent: ?*const ModuleConstFrame) bool {
|
||||
if (moduleConstFrameContains(parent, name)) return false;
|
||||
const ci = consts.get(name) orelse return false;
|
||||
if (isFloatConstType(ci.ty)) return true;
|
||||
var frame = ModuleConstFrame{ .name = name, .parent = parent };
|
||||
return isFloatValuedExpr(ci.value, ModuleConstCtx{ .consts = consts, .table = table, .frame = &frame });
|
||||
}
|
||||
|
||||
/// A module const may serve as an integer COUNT only when its DECLARED type is
|
||||
/// numeric — an integer of any width or a float (an integral float folds to its
|
||||
/// int via `floatToIntExact`). `moduleConstIntFramed` consults this so a count
|
||||
@@ -173,6 +203,61 @@ pub fn moduleConstFloat(consts: *const std.StringHashMap(ModuleConstInfo), table
|
||||
return moduleConstFloatFramed(consts, table, name, null);
|
||||
}
|
||||
|
||||
/// True iff `name` is a FLOAT-valued module const — judged by VALUE, so it covers
|
||||
/// a typed float const (`K : f64 : 4.0`), an untyped float-EXPRESSION const
|
||||
/// (`ME :: 4.0 + 1.0`, whose placeholder type is `s64`), and a non-integral float
|
||||
/// const (`F : f64 : 2.5`). SINGLE source for the stateful (`Lowering`) and
|
||||
/// stateless (`type_bridge`) division-arm float checks, so they agree on which
|
||||
/// const-leaf divisions are float (issue 0095 / F0.11-6).
|
||||
pub fn moduleConstIsFloatTyped(consts: *const std.StringHashMap(ModuleConstInfo), table: *const types.TypeTable, name: []const u8) bool {
|
||||
return moduleConstFloatValuedFramed(consts, table, name, null);
|
||||
}
|
||||
|
||||
/// True iff `node` is a FLOAT-valued compile-time expression — a float literal,
|
||||
/// a float-typed const leaf (`F : f64 : 2.5`, `K : f64 : 4.0`), a builtin float
|
||||
/// numeric-limit (`f64.max`), or arithmetic over any of those. THE predicate the
|
||||
/// int folder's division arm consults: `/` with a float operand is FLOAT division
|
||||
/// (`5.0 / 2.0` = 2.5), and folding it with integer truncating division would
|
||||
/// silently accept a non-integral float at a count / typed binding (issue 0095 /
|
||||
/// F0.11-6). `+ - *` agree between int and float arithmetic for the integral
|
||||
/// operands the int folder ever sees (a non-integral operand folds to null first),
|
||||
/// so ONLY `/` needs this guard. A leaf name resolves through `ctx.nameIsFloatTyped`
|
||||
/// — the same ctx that supplies `lookupDimName`/`lookupFloatName` — so an INTEGRAL
|
||||
/// float const (`K : f64 : 4.0`, which folds to 4 as a standalone count) is still
|
||||
/// recognised as float-valued inside a division.
|
||||
///
|
||||
/// Also the precise "is this a compile-time float-valued initializer" test the
|
||||
/// typed-binding narrowing path (`Lowering.foldComptimeFloatInit`) uses alongside
|
||||
/// `inferExprType`, so an untyped float-EXPRESSION const (`ME :: 4.0 + 1.0`,
|
||||
/// placeholder type `s64`) flowing into an integer binding (`x : s64 = ME / 2`)
|
||||
/// is judged float-valued even though `inferExprType` reads its placeholder type.
|
||||
pub fn isFloatValuedExpr(node: *const Node, ctx: anytype) bool {
|
||||
return switch (node.data) {
|
||||
.float_literal => true,
|
||||
.int_literal => false,
|
||||
.identifier => |id| ctx.nameIsFloatTyped(id.name),
|
||||
.type_expr => |te| ctx.nameIsFloatTyped(te.name),
|
||||
.field_access => |fa| blk: {
|
||||
// A backtick RAW receiver (`` `f64.epsilon ``) is an ordinary field
|
||||
// READ on a value whose spelling shadows a builtin type, NOT the
|
||||
// numeric-limit accessor — so it is not a float leaf (issues 0092 /
|
||||
// 0093). Only a BARE type receiver folds to a float limit.
|
||||
const obj_name: ?[]const u8 = switch (fa.object.data) {
|
||||
.identifier => |id| if (id.is_raw) null else id.name,
|
||||
.type_expr => |te| if (te.is_raw) null else te.name,
|
||||
else => null,
|
||||
};
|
||||
if (obj_name) |on| {
|
||||
if (type_resolver.TypeResolver.floatLimitFor(on, fa.field) != null) break :blk true;
|
||||
}
|
||||
break :blk false;
|
||||
},
|
||||
.unary_op => |u| isFloatValuedExpr(u.operand, ctx),
|
||||
.binary_op => |b| isFloatValuedExpr(b.lhs, ctx) or isFloatValuedExpr(b.rhs, ctx),
|
||||
else => false,
|
||||
};
|
||||
}
|
||||
|
||||
/// Evaluate a constant integer expression to its value. THE single
|
||||
/// integer-expression folder for the compiler — array dimensions (`[N]T`,
|
||||
/// `[M + 1]T`), Vector lane counts (`Vector(N, f32)`), generic value-param
|
||||
@@ -184,6 +269,13 @@ pub fn moduleConstFloat(consts: *const std.StringHashMap(ModuleConstInfo), table
|
||||
/// forms (`[M + N - 1]`, `[(M + 1) * 2]`) fold (a grouping `(…)` carries no AST
|
||||
/// node; the parser returns the inner expression).
|
||||
///
|
||||
/// ONE exception keeps a float operation out of integer arithmetic: a `/` whose
|
||||
/// lhs/rhs is float-valued (`5.0 / 2.0`, `K / 3` with `K : f64 : 4.0`) is FLOAT
|
||||
/// division, NOT integer truncation, so this folder refuses it (`isFloatValuedExpr`)
|
||||
/// and lets `evalConstFloatExpr` + the unified narrowing rule see the true value
|
||||
/// (issue 0095 / F0.11-6). `+ - *` need no such guard — they agree between int and
|
||||
/// float arithmetic for the integral operands this folder ever sees.
|
||||
///
|
||||
/// Leaves resolve through the ctx, so each call site shares the SAME folding
|
||||
/// logic while contributing its own bindings:
|
||||
/// - `ctx.lookupDimName(name)` — a name bound to a compile-time integer. The
|
||||
@@ -238,7 +330,17 @@ pub fn evalConstIntExpr(node: *const Node, ctx: anytype) ?i64 {
|
||||
.add => std.math.add(i64, l, r) catch null,
|
||||
.sub => std.math.sub(i64, l, r) catch null,
|
||||
.mul => std.math.mul(i64, l, r) catch null,
|
||||
.div => std.math.divTrunc(i64, l, r) catch null,
|
||||
// A division with a FLOAT operand is FLOAT division (`5.0 / 2.0`
|
||||
// = 2.5, `K / 3` with `K : f64 : 4.0` = 1.333…), NOT integer
|
||||
// truncating division — refuse to fold it here so the value
|
||||
// surfaces through `evalConstFloatExpr` + the unified float→int
|
||||
// rule (integral folds, non-integral errors) instead of silently
|
||||
// truncating to an integer (issue 0095 / F0.11-6). A genuine
|
||||
// integer `/` (both operands integer-valued) still truncates.
|
||||
.div => if (isFloatValuedExpr(b.lhs, ctx) or isFloatValuedExpr(b.rhs, ctx))
|
||||
null
|
||||
else
|
||||
std.math.divTrunc(i64, l, r) catch null,
|
||||
.mod => if (r == 0) null else @rem(l, r),
|
||||
else => null,
|
||||
};
|
||||
|
||||
@@ -91,6 +91,16 @@ const StatelessInner = struct {
|
||||
const consts = self.consts orelse return null;
|
||||
return program_index_mod.moduleConstFloat(consts, self.table, name);
|
||||
}
|
||||
/// True iff `name` is a FLOAT-typed module const — the registration-time twin
|
||||
/// of `Lowering.nameIsFloatTyped`, routed through the SAME
|
||||
/// `program_index.moduleConstIsFloatTyped` so the int folder's division arm
|
||||
/// classifies a const-leaf division identically on the alias-registration path
|
||||
/// as on the direct form (issue 0095 / F0.11-6, the issue-0083 unify-or-diverge
|
||||
/// rule extended to the division guard).
|
||||
pub fn nameIsFloatTyped(self: StatelessInner, name: []const u8) bool {
|
||||
const consts = self.consts orelse return false;
|
||||
return program_index_mod.moduleConstIsFloatTyped(consts, self.table, name);
|
||||
}
|
||||
};
|
||||
|
||||
/// Fold a registration-time array dimension to its `DimU32` outcome through the
|
||||
|
||||
Reference in New Issue
Block a user