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:
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
| ^^^^^^^^
|
| ^^^^^^^^
|
||||||
|
|||||||
@@ -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 1–4 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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
13
specs.md
13
specs.md
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user