fix(ir): evalConstFloatExpr reaches parity with evalConstIntExpr — numeric-limit float leaves + float % fold under the unified rule [F0.11]

The compile-time float evaluator lagged the integer one: it had no
numeric-limit field-access arm, so `y : s64 = f64.true_min + 0.5` (=0.5)
silently truncated to 0 even though the direct `f64.true_min` already
errored; the arm-by-arm audit also found a missing `%` arm, so
`y : s64 = 5.5 % 2.0` (=1.5) silently truncated to 1.

Bring evalConstFloatExpr to PARITY with evalConstIntExpr:
- Add a `.field_access` arm resolving a builtin FLOAT numeric-limit
  accessor (`f64.max`, `f32.epsilon`, `f64.true_min`, …) via the SAME
  `type_resolver.floatLimitFor` that `lowerNumericLimit` uses — the float
  twin of the int evaluator's `integerLimitFor` arm.
- Add a `.mod` arm via `@rem` (matching evalConstIntExpr and codegen's
  `frem`): `6.0 % 4.0` folds to 2 (via int delegation), `5.5 % 2.0` = 1.5
  is rejected.

The two evaluators now share every leaf/operator shape, so no
compile-time-const float form escapes the unified float→int rule at one
site while folding at another. All five sites (local/field/param/const/
array-dim) stay consistent.

Regression: 0168 (positive) adds `f64.max - f64.max` → 0, `6.0 % 4.0` → 2,
integer-limit `s8.max`/`[u8.max]` unregressed, `xx` escapes for both new
forms; 1146 (negative) adds `f64.true_min + 0.5` and `5.5 % 2.0` erroring
at a binding site; program_index.test.zig covers the floatLimitFor arm and
the `%` arm. specs.md + readme.md state the parity. issues/0095 RESOLVED
banner gains the attempt-5 entry.
This commit is contained in:
agra
2026-06-05 18:15:17 +03:00
parent b73363ca4c
commit 74f675ac0b
9 changed files with 189 additions and 44 deletions

View File

@@ -3,10 +3,14 @@
// `floatToIntExact` rule an array dimension / `$K: Count` already uses — across // `floatToIntExact` rule an array dimension / `$K: Count` already uses — across
// all FIVE sites: a typed LOCAL, a struct FIELD default, a typed module CONST, a // all FIVE sites: a typed LOCAL, a struct FIELD default, a typed module CONST, a
// function PARAM default, and an array DIMENSION. It folds whether written as a // function PARAM default, and an array DIMENSION. It folds whether written as a
// float LITERAL (`4.0`), an INT-const-EXPRESSION (`M + 2.0`, with `M :: 2`), or a // float LITERAL (`4.0`), an INT-const-EXPRESSION (`M + 2.0`, with `M :: 2`), a
// FLOAT-const-LEAF expression whose sum is integral (`F + 1.5`, with // FLOAT-const-LEAF expression whose sum is integral (`F + 1.5`, with
// `F : f64 : 2.5`, = 4.0) — including such a float-const-leaf expression driving // `F : f64 : 2.5`, = 4.0) — including such a float-const-leaf expression driving
// an array dimension directly, through a const, or via a type alias. // an array dimension directly, through a const, or via a type alias — a builtin
// FLOAT numeric-limit leaf in an integral expression (`f64.max - f64.max` = 0),
// and an integral float `%` (`6.0 % 4.0` = 2). The compile-time float evaluator
// is at parity with the integer one, so integer numeric-limit accessors (`s8.max`,
// `[u8.max]` count) keep folding through the shared int folder, unregressed.
// The escape hatch (`xx` / `cast`) still TRUNCATES any float, integral or not — // The escape hatch (`xx` / `cast`) still TRUNCATES any float, integral or not —
// including a non-integral const expression (`xx (M + 0.5)` / `xx (F + 0.25)`). // including a non-integral const expression (`xx (M + 0.5)` / `xx (F + 0.25)`).
// //
@@ -69,12 +73,32 @@ main :: () {
aa : ArrFE = ---; aa : ArrFE = ---;
print("dim.direct={} dim.const={} dim.alias={}\n", ad.len, ak.len, aa.len); print("dim.direct={} dim.const={} dim.alias={}\n", ad.len, ak.len, aa.len);
// Numeric-limit float leaf in an expression: an INTEGRAL result folds (the
// compile-time float evaluator is at parity with the integer one — a
// `f64`/`f32` `.max`/`.min`/`.epsilon`/… leaf is recognised inside an
// expression, not only as a direct value). `f64.max - f64.max` = 0.0 → 0.
lim : s64 = f64.max - f64.max;
// Integral float `%` (parity with int `%`): `6.0 % 4.0` = 2.0 → 2.
fm : s64 = 6.0 % 4.0;
print("limit={} fmod={}\n", lim, fm);
// Integer numeric-limit accessors (NL.1) are unregressed by the float-leaf
// parity work: they still fold at a binding (`s8.max` = 127) and as an array
// dimension count (`[u8.max]` = len 255), through the SAME int folder.
il : s64 = s8.max;
iarr : [u8.max]s64 = ---;
print("intlimit={} intcount={}\n", il, iarr.len);
// Explicit escape: `xx` / `cast` always truncate, integral or not — // Explicit escape: `xx` / `cast` always truncate, integral or not —
// including a non-integral const EXPRESSION (`xx (M + 0.5)` → 2) and a // including a non-integral const EXPRESSION (`xx (M + 0.5)` → 2), a
// non-integral float-const-LEAF expression (`xx (F + 0.25)` → 2). // non-integral float-const-LEAF expression (`xx (F + 0.25)` → 2), a
// non-integral numeric-limit expr (`xx (f64.true_min + 0.5)` → 0), and a
// non-integral float `%` (`xx (5.5 % 2.0)` → 1).
e : s64 = xx 4.9; e : s64 = xx 4.9;
c : s64 = cast(s64) 1.5; c : s64 = cast(s64) 1.5;
xc : s64 = xx (M + 0.5); xc : s64 = xx (M + 0.5);
xf : s64 = xx (F + 0.25); xf : s64 = xx (F + 0.25);
print("xx={} cast={} xxExpr={} xxFlt={}\n", e, c, xc, xf); 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);
} }

View File

@@ -4,13 +4,17 @@
// PARAM default, a struct FIELD default, AND an array DIMENSION; each emits a // PARAM default, a struct FIELD default, AND an array DIMENSION; each emits a
// narrowing diagnostic at the offending float and aborts (exit 1). It fires // narrowing diagnostic at the offending float and aborts (exit 1). It fires
// whether the float is a LITERAL (`1.5`), an INT-const-expression (`M + 0.5`, // whether the float is a LITERAL (`1.5`), an INT-const-expression (`M + 0.5`,
// with `M :: 2`), or a FLOAT-const-leaf expression (`F + 0.25`, with // with `M :: 2`), a FLOAT-const-leaf expression (`F + 0.25`, with `F : f64 : 2.5`,
// `F : f64 : 2.5`, = 2.75) — all three are the core of issue 0095, which // = 2.75), a builtin FLOAT numeric-limit leaf inside an expression
// previously slipped through and truncated to 2. The fix is the integral-fold / // (`f64.true_min + 0.5` = 0.5), or a float `%` whose remainder is non-integral
// non-integral-error rule shared across all five sites (local, field, param, // (`5.5 % 2.0` = 1.5) — all of these are the core of issue 0095, which previously
// const, and array dimension), applied to ANY compile-time-constant float // slipped through and truncated. The fix is the integral-fold / non-integral-error
// expression (literal, int-const leaf, float-const leaf, and combinations). The // rule shared across all five sites (local, field, param, const, and array
// array-dimension site phrases the same rejection as "must be an integer". // dimension), applied to ANY compile-time-constant float expression (literal,
// int-const leaf, float-const leaf, numeric-limit leaf, `+ - * / %`, and
// combinations) — the compile-time float evaluator is at parity with the integer
// one, so no float leaf shape escapes. The array-dimension site phrases the same
// rejection as "must be an integer".
// //
// The escape hatch stays open: `y : s64 = xx 1.5` (or `cast(s64) 1.5`) // The escape hatch stays open: `y : s64 = xx 1.5` (or `cast(s64) 1.5`)
// truncates with no error — exercised on the POSITIVE side (example 0168). // truncates with no error — exercised on the POSITIVE side (example 0168).
@@ -36,9 +40,11 @@ main :: () {
y : s64 = 1.5; // non-integral float LITERAL local → error y : s64 = 1.5; // non-integral float LITERAL local → error
ye : s64 = M + 0.5; // non-integral int-const-EXPRESSION local → error ye : s64 = M + 0.5; // non-integral int-const-EXPRESSION local → error
yf : s64 = F + 0.25; // non-integral float-const-LEAF local → error yf : s64 = F + 0.25; // non-integral float-const-LEAF local → error
yn : s64 = f64.true_min + 0.5; // non-integral numeric-limit float expr → error
ym : s64 = 5.5 % 2.0; // non-integral float `%` remainder (1.5) → error
ad : [F + 0.25]s64 = ---; // non-integral float-const-LEAF array DIMENSION → error ad : [F + 0.25]s64 = ---; // non-integral float-const-LEAF array DIMENSION → error
b := Bad.{}; b := Bad.{};
print("{} {} {}\n", b.f, b.fe, b.ff); print("{} {} {}\n", b.f, b.fe, b.ff);
print("{} {} {}\n", badLit(), badExpr(), badFlt()); print("{} {} {}\n", badLit(), badExpr(), badFlt());
print("{} {} {} {}\n", y, ye, yf, ad.len); print("{} {} {} {} {} {}\n", y, ye, yf, yn, ym, ad.len);
} }

View File

@@ -4,4 +4,6 @@ field=4 fieldExpr=4 fieldFlt=4
param=6 paramFlt=4 param=6 paramFlt=4
const=8 constFlt=4 len=8 const=8 constFlt=4 len=8
dim.direct=4 dim.const=4 dim.alias=4 dim.direct=4 dim.const=4 dim.alias=4
xx=4 cast=1 xxExpr=2 xxFlt=2 limit=0 fmod=2
intlimit=127 intcount=255
xx=4 cast=1 xxExpr=2 xxFlt=2 xxLimit=0 xxMod=1

View File

@@ -1,59 +1,71 @@
error: cannot implicitly narrow non-integral float '1.5' to 's64'; use an explicit cast (`xx`/`cast`) error: cannot implicitly narrow non-integral float '1.5' to 's64'; use an explicit cast (`xx`/`cast`)
--> examples/1146-diagnostics-nonintegral-float-to-int.sx:36:16 --> examples/1146-diagnostics-nonintegral-float-to-int.sx:40:16
| |
36 | y : s64 = 1.5; // non-integral float LITERAL local → error 40 | y : s64 = 1.5; // non-integral float LITERAL local → error
| ^^^ | ^^^
error: cannot implicitly narrow non-integral float '2.5' to 's64'; use an explicit cast (`xx`/`cast`) error: cannot implicitly narrow non-integral float '2.5' to 's64'; use an explicit cast (`xx`/`cast`)
--> examples/1146-diagnostics-nonintegral-float-to-int.sx:37:16 --> examples/1146-diagnostics-nonintegral-float-to-int.sx:41:16
| |
37 | ye : s64 = M + 0.5; // non-integral int-const-EXPRESSION local → error 41 | ye : s64 = M + 0.5; // non-integral int-const-EXPRESSION local → error
| ^^^^^^^ | ^^^^^^^
error: cannot implicitly narrow non-integral float '2.75' to 's64'; use an explicit cast (`xx`/`cast`) error: cannot implicitly narrow non-integral float '2.75' to 's64'; use an explicit cast (`xx`/`cast`)
--> examples/1146-diagnostics-nonintegral-float-to-int.sx:38:16 --> examples/1146-diagnostics-nonintegral-float-to-int.sx:42:16
| |
38 | yf : s64 = F + 0.25; // non-integral float-const-LEAF local → error 42 | yf : s64 = F + 0.25; // non-integral float-const-LEAF local → error
| ^^^^^^^^ | ^^^^^^^^
error: array dimension must be an integer, but '2.75' is a non-integral float error: cannot implicitly narrow non-integral float '0.5' to 's64'; use an explicit cast (`xx`/`cast`)
--> examples/1146-diagnostics-nonintegral-float-to-int.sx:39:11 --> examples/1146-diagnostics-nonintegral-float-to-int.sx:43:16
| |
39 | ad : [F + 0.25]s64 = ---; // non-integral float-const-LEAF array DIMENSION → error 43 | yn : s64 = f64.true_min + 0.5; // non-integral numeric-limit float expr → error
| ^^^^^^^^^^^^^^^^^^
error: cannot implicitly narrow non-integral float '1.5' to 's64'; use an explicit cast (`xx`/`cast`)
--> examples/1146-diagnostics-nonintegral-float-to-int.sx:44:16
|
44 | ym : s64 = 5.5 % 2.0; // non-integral float `%` remainder (1.5) → error
| ^^^^^^^^^
error: array dimension must be an integer, but '2.75' is a non-integral float
--> examples/1146-diagnostics-nonintegral-float-to-int.sx:45:11
|
45 | ad : [F + 0.25]s64 = ---; // non-integral float-const-LEAF array DIMENSION → error
| ^^^^^^^^ | ^^^^^^^^
error: cannot implicitly narrow non-integral float '3.5' to 's64'; use an explicit cast (`xx`/`cast`) error: cannot implicitly narrow non-integral float '3.5' to 's64'; use an explicit cast (`xx`/`cast`)
--> examples/1146-diagnostics-nonintegral-float-to-int.sx:26:16 --> examples/1146-diagnostics-nonintegral-float-to-int.sx:30:16
| |
26 | f : s64 = 3.5; // non-integral float LITERAL field default → error 30 | f : s64 = 3.5; // non-integral float LITERAL field default → error
| ^^^ | ^^^
error: cannot implicitly narrow non-integral float '2.5' to 's64'; use an explicit cast (`xx`/`cast`) error: cannot implicitly narrow non-integral float '2.5' to 's64'; use an explicit cast (`xx`/`cast`)
--> examples/1146-diagnostics-nonintegral-float-to-int.sx:27:16 --> examples/1146-diagnostics-nonintegral-float-to-int.sx:31:16
| |
27 | fe : s64 = M + 0.5; // non-integral int-const-EXPR field default → error 31 | fe : s64 = M + 0.5; // non-integral int-const-EXPR field default → error
| ^^^^^^^ | ^^^^^^^
error: cannot implicitly narrow non-integral float '2.75' to 's64'; use an explicit cast (`xx`/`cast`) error: cannot implicitly narrow non-integral float '2.75' to 's64'; use an explicit cast (`xx`/`cast`)
--> examples/1146-diagnostics-nonintegral-float-to-int.sx:28:16 --> examples/1146-diagnostics-nonintegral-float-to-int.sx:32:16
| |
28 | ff : s64 = F + 0.25; // non-integral float-const-LEAF field default → error 32 | ff : s64 = F + 0.25; // non-integral float-const-LEAF field default → error
| ^^^^^^^^ | ^^^^^^^^
error: cannot implicitly narrow non-integral float '2.5' to 's64'; use an explicit cast (`xx`/`cast`) error: cannot implicitly narrow non-integral float '2.5' to 's64'; use an explicit cast (`xx`/`cast`)
--> examples/1146-diagnostics-nonintegral-float-to-int.sx:31:23 --> examples/1146-diagnostics-nonintegral-float-to-int.sx:35:23
| |
31 | badLit :: (x : s64 = 2.5) -> s64 { return x; } // non-integral LITERAL param default → error 35 | badLit :: (x : s64 = 2.5) -> s64 { return x; } // non-integral LITERAL param default → error
| ^^^ | ^^^
error: cannot implicitly narrow non-integral float '2.5' to 's64'; use an explicit cast (`xx`/`cast`) error: cannot implicitly narrow non-integral float '2.5' to 's64'; use an explicit cast (`xx`/`cast`)
--> examples/1146-diagnostics-nonintegral-float-to-int.sx:32:23 --> examples/1146-diagnostics-nonintegral-float-to-int.sx:36:23
| |
32 | badExpr :: (x : s64 = M + 0.5) -> s64 { return x; } // non-integral int-const-EXPR param default → error 36 | badExpr :: (x : s64 = M + 0.5) -> s64 { return x; } // non-integral int-const-EXPR param default → error
| ^^^^^^^ | ^^^^^^^
error: cannot implicitly narrow non-integral float '2.75' to 's64'; use an explicit cast (`xx`/`cast`) error: cannot implicitly narrow non-integral float '2.75' to 's64'; use an explicit cast (`xx`/`cast`)
--> examples/1146-diagnostics-nonintegral-float-to-int.sx:33:23 --> examples/1146-diagnostics-nonintegral-float-to-int.sx:37:23
| |
33 | badFlt :: (x : s64 = F + 0.25) -> s64 { return x; } // non-integral float-const-LEAF param default → error 37 | badFlt :: (x : s64 = F + 0.25) -> s64 { return x; } // non-integral float-const-LEAF param default → error
| ^^^^^^^^ | ^^^^^^^^

View File

@@ -95,6 +95,28 @@
> path. This relaxes the F0.4 `examples/1132` wording (a non-integral float const > path. This relaxes the F0.4 `examples/1132` wording (a non-integral float const
> dim now reports the precise "non-integral float" message; it still errors). > dim now reports the precise "non-integral float" message; it still errors).
> >
> **Completion (F0.11 attempt 5)** — attempts 14 unified all five sites for
> literal / int-const-expr / float-const-leaf forms, but `evalConstFloatExpr` still
> LAGGED `evalConstIntExpr`: the int evaluator resolves a numeric-limit field-access
> leaf (`f64.true_min`, `f64.max`) via `type_resolver.integerLimitFor`, but the
> float evaluator had no parallel arm, so `y : s64 = f64.true_min + 0.5` (= 0.5)
> truncated silently to 0 (the direct `f64.true_min` already errored via the IR-level
> `constFloatInfo` path, but the *expression* form escaped). Closed by bringing the
> two evaluators to PARITY:
> - `evalConstFloatExpr` gains a `.field_access` arm that resolves a builtin FLOAT
> numeric-limit accessor through `type_resolver.TypeResolver.floatLimitFor` (the
> SAME facility `lowerNumericLimit` uses) — the float twin of the int evaluator's
> `integerLimitFor` arm. Integer limits / `<pack>.len` are still resolved by the
> int delegation, so only the float-limit case lands here.
> - The audit also surfaced a missing `%` arm: the int evaluator folds `.mod` but
> the float one did not, so `y : s64 = 5.5 % 2.0` (= 1.5) truncated silently to 1.
> `evalConstFloatExpr` now handles `.mod` via `@rem` (matching `evalConstIntExpr`
> and codegen's `frem`; `6.0 % 4.0` folds to 2 via the int delegation, `5.5 % 2.0`
> = 1.5 is rejected). The two evaluators are now at full leaf/operator parity, so
> no compile-time-const float shape escapes the rule at one site while folding at
> another. (A comptime-fn returning float is a genuinely new form for BOTH and is
> out of scope.)
>
> Regression tests: `examples/0168-types-integral-float-to-int.sx` (positive — > Regression tests: `examples/0168-types-integral-float-to-int.sx` (positive —
> local/field/param/const fold, integral int-const-EXPRESSION (`M + 2.0`) AND > local/field/param/const fold, integral int-const-EXPRESSION (`M + 2.0`) AND
> float-const-LEAF (`F + 1.5`, `F : f64 : 2.5`) fold at local/field/param/const, > float-const-LEAF (`F + 1.5`, `F : f64 : 2.5`) fold at local/field/param/const,
@@ -110,9 +132,15 @@
> len 4; 1146 adds `[F + 0.25]s64` erroring; `examples/1132` now expects the > len 4; 1146 adds `[F + 0.25]s64` erroring; `examples/1132` now expects the
> precise non-integral-float dim wording. Unit: > precise non-integral-float dim wording. Unit:
> `program_index.test.zig` "evalConstFloatExpr folds comptime float expressions" > `program_index.test.zig` "evalConstFloatExpr folds comptime float expressions"
> (covers the float-const leaf: `F` → 2.5, `F + 0.25` → 2.75, `F + 1.5` → 4.0) and > (covers the float-const leaf: `F` → 2.5, `F + 0.25` → 2.75, `F + 1.5` → 4.0;
> "foldCountI64 / foldDimU32 fold an integral float count, reject a non-integral > attempt 5 adds the numeric-limit leaf `f64.max`/`f64.true_min`/`f32.epsilon`,
> one" (the count fold + the `non_integral_float` / `below_min` distinction). > `f64.max - f64.max` → 0, `f64.true_min + 0.5` → 0.5, and the `%` arm `5.5 % 2.0`
> → 1.5 / `% 0.0` → null) and "foldCountI64 / foldDimU32 fold an integral float
> count, reject a non-integral one" (the count fold + the `non_integral_float` /
> `below_min` distinction). Attempt 5 also extends 0168 (positive: `f64.max -
> 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).
## Symptom ## Symptom
A typed LOCAL (and likely typed param/field) silently truncates a floating-point A typed LOCAL (and likely typed param/field) silently truncates a floating-point

View File

@@ -128,7 +128,12 @@ integer-typed binding *without* a cast follows the same integral-fold rule an
array dimension uses: an **integral** compile-time float folds to its integer, a array dimension uses: an **integral** compile-time float folds to its integer, a
**non-integral** one is a compile error. It holds whether the value is a literal **non-integral** one is a compile error. It holds whether the value is a literal
or *any* compile-time-constant float expression — including one that references 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`) — and is uniform 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
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 across a typed local, a parameter default, a struct field default, a call
argument, a typed constant, **and an array dimension / count** — `y : s64 = 4.0`, argument, a typed constant, **and an array dimension / count** — `y : s64 = 4.0`,
`K : s64 : 4.0`, `y : s64 = M + 2.0`, and `[F + 1.5]s64` (≡ `[4]s64`, whether `K : s64 : 4.0`, `y : s64 = M + 2.0`, and `[F + 1.5]s64` (≡ `[4]s64`, whether

View File

@@ -897,7 +897,9 @@ be an integer, but '4.5' is a non-integral float"). This holds however the float
is written — a literal (`4.0`), a float-typed const (`N : f64 : 4.0`), or a 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 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 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 count and a typed 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
count and a typed
binding's float→integer initializer share the *same* compile-time float 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 evaluation, so they agree at every site — direct, through a const, or via a type
alias (see "Implicit float → integer", §2 Type Conversions). alias (see "Implicit float → integer", §2 Type Conversions).
@@ -1437,10 +1439,15 @@ array dimension / lane count uses (see "Array dimensions are integral", §2):
`n : s64 = -2.0` ≡ `-2`, `y : s64 = M + 2.0` → 4 (`M :: 2`). A const expression `n : s64 = -2.0` ≡ `-2`, `y : s64 = M + 2.0` → 4 (`M :: 2`). A const expression
here is *any* compile-time-constant float expression — an integer-const leaf 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), (`M + 2.0`), a float-typed const leaf (`F : f64 : 2.5; y : s64 = F + 1.5` → 4),
or any combination of them. 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.
- A **non-integral** compile-time float — literal OR const expression — is a - A **non-integral** compile-time float — literal OR const expression — is a
**compile error** with one uniform wording at every site: **compile error** with one uniform wording at every site:
`y : s64 = 1.5`, `y : s64 = M + 0.5`, and `y : s64 = F + 0.25` (= 2.75) all → `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 →
"cannot implicitly narrow non-integral float '…' to 's64'; use an explicit "cannot implicitly narrow non-integral float '…' to 's64'; use an explicit
cast (`xx`/`cast`)". cast (`xx`/`cast`)".
- This applies uniformly to a typed **local**, a function **param default**, a - This applies uniformly to a typed **local**, a function **param default**, a

View File

@@ -368,6 +368,38 @@ test "evalConstFloatExpr folds comptime float expressions, halts on runtime leav
try std.testing.expectEqual(@as(?f64, 2.75), eval(&fq, ctx)); try std.testing.expectEqual(@as(?f64, 2.75), eval(&fq, ctx));
try std.testing.expectEqual(@as(?f64, 4.0), eval(&fh, ctx)); try std.testing.expectEqual(@as(?f64, 4.0), eval(&fh, ctx));
// A builtin FLOAT numeric-limit accessor is a compile-time float leaf — the
// twin of `evalConstIntExpr`'s `<IntType>.min`/`.max` arm, via the shared
// `type_resolver.floatLimitFor`. It folds as a direct leaf AND inside an
// expression: `f64.max - f64.max` = 0.0 (integral → folds), `f64.true_min +
// 0.5` = 0.5 (non-integral → the narrowing rule rejects it). A non-limit
// field on a float type is not a leaf → null (issue 0095, attempt 5 parity).
var f64ty = nIdent("f64");
var f32ty = nIdent("f32");
var fmax = nField(&f64ty, "max");
var ftmin = nField(&f64ty, "true_min");
var feps = nField(&f32ty, "epsilon");
var fbogus = nField(&f64ty, "bogus");
try std.testing.expectEqual(@as(?f64, std.math.floatMax(f64)), eval(&fmax, ctx));
try std.testing.expectEqual(@as(?f64, std.math.floatTrueMin(f64)), eval(&ftmin, ctx));
try std.testing.expectEqual(@as(?f64, @as(f64, std.math.floatEps(f32))), eval(&feps, ctx));
try std.testing.expect(eval(&fbogus, ctx) == null);
var lim_diff = nBin(.sub, &fmax, &fmax);
var lim_nonint = nBin(.add, &ftmin, &half);
try std.testing.expectEqual(@as(?f64, 0.0), eval(&lim_diff, ctx));
try std.testing.expectEqual(@as(?f64, 0.5), eval(&lim_nonint, ctx));
// `%` mirrors the int folder's `.mod` (and codegen's `frem`): `@rem`. A
// non-integral-operand remainder (`5.5 % 2.0` = 1.5) reaches this arm (the
// integral-operand case `6.0 % 4.0` folds via the int delegation); a zero
// divisor → null.
var fivehalf = nFloat(5.5);
var zero_f0 = nFloat(0.0);
var fmod = nBin(.mod, &fivehalf, &two_f);
var fmodz = nBin(.mod, &fivehalf, &zero_f0);
try std.testing.expectEqual(@as(?f64, 1.5), eval(&fmod, ctx));
try std.testing.expect(eval(&fmodz, ctx) == null);
// A runtime operand poisons the whole fold; a non-arithmetic operator and a // A runtime operand poisons the whole fold; a non-arithmetic operator and a
// float division by zero are not compile-time float leaves → null. // float division by zero are not compile-time float leaves → null.
var zp = nBin(.add, &z, &half); var zp = nBin(.add, &z, &half);

View File

@@ -258,10 +258,17 @@ pub fn evalConstIntExpr(node: *const Node, ctx: anytype) ?i64 {
/// / comptime consts, `<IntType>.min`/`.max`, and integer arithmetic resolve /// / comptime consts, `<IntType>.min`/`.max`, and integer arithmetic resolve
/// through the SINGLE int folder — no parallel integer logic here); only the /// through the SINGLE int folder — no parallel integer logic here); only the
/// genuinely float-producing shapes — a float literal, a NON-INTEGRAL float-const /// genuinely float-producing shapes — a float literal, a NON-INTEGRAL float-const
/// leaf, a unary negate, and `+ - * /` arithmetic involving a float — are /// leaf, a builtin FLOAT numeric-limit accessor (`f64.max`, `f32.epsilon`,
/// evaluated here in `f64`. A `%`, comparison, or any other shape is not a /// `f64.true_min`, …), a unary negate, and `+ - * / %` arithmetic involving a
/// float — are evaluated here in `f64`. A comparison or any other shape is not a
/// compile-time float leaf → null. /// compile-time float leaf → null.
/// ///
/// This evaluator is at PARITY with `evalConstIntExpr` — every leaf / node kind
/// the int folder recognises (literal, named const leaf, numeric-limit
/// field-access, unary negate, `+ - * / %`) is mirrored here in `f64` (delegating
/// integer subtrees), so no compile-time-const float shape escapes the unified
/// float→int narrowing rule at one site while folding at another.
///
/// A NAMED-const leaf resolves through `ctx.lookupFloatName`, the float twin of /// A NAMED-const leaf resolves through `ctx.lookupFloatName`, the float twin of
/// the `lookupDimName` the int folder uses: a numeric module const whose value is /// the `lookupDimName` the int folder uses: a numeric module const whose value is
/// a non-integral float (`F : f64 : 2.5`) surfaces here so `F + 0.25` (= 2.75) is /// a non-integral float (`F : f64 : 2.5`) surfaces here so `F + 0.25` (= 2.75) is
@@ -280,6 +287,24 @@ pub fn evalConstFloatExpr(node: *const Node, ctx: anytype) ?f64 {
// float (the integral / integer cases were caught by the int delegation). // float (the integral / integer cases were caught by the int delegation).
.identifier => |id| ctx.lookupFloatName(id.name), .identifier => |id| ctx.lookupFloatName(id.name),
.type_expr => |te| ctx.lookupFloatName(te.name), .type_expr => |te| ctx.lookupFloatName(te.name),
.field_access => |fa| blk: {
// A numeric-limit accessor on a builtin FLOAT type (`f64.true_min`,
// `f32.epsilon`, `f64.max`, …) is a compile-time float leaf — the
// float twin of `evalConstIntExpr`'s `<IntType>.min`/`.max` arm, via
// the SAME `type_resolver` fold (the facility `lowerNumericLimit`
// uses) so the two evaluators can't disagree on what `f64.max`
// evaluates to. Integer limits and `<pack>.len` are already resolved
// by the int delegation above, so only the float-limit case remains.
const obj_name: ?[]const u8 = switch (fa.object.data) {
.identifier => |id| id.name,
.type_expr => |te| te.name,
else => null,
};
if (obj_name) |on| {
if (type_resolver.TypeResolver.floatLimitFor(on, fa.field)) |v| break :blk v;
}
break :blk null;
},
.unary_op => |u| switch (u.op) { .unary_op => |u| switch (u.op) {
.negate => { .negate => {
const v = evalConstFloatExpr(u.operand, ctx) orelse return null; const v = evalConstFloatExpr(u.operand, ctx) orelse return null;
@@ -295,6 +320,10 @@ pub fn evalConstFloatExpr(node: *const Node, ctx: anytype) ?f64 {
.sub => l - r, .sub => l - r,
.mul => l * r, .mul => l * r,
.div => if (r == 0.0) null else l / r, .div => if (r == 0.0) null else l / r,
// `%` mirrors `evalConstIntExpr`'s `.mod` (and codegen's `frem`):
// `@rem` truncated remainder, so `5.5 % 2.0` = 1.5 surfaces as a
// non-integral float instead of silently truncating.
.mod => if (r == 0.0) null else @rem(l, r),
else => null, else => null,
}; };
}, },