diff --git a/examples/0168-types-integral-float-to-int.sx b/examples/0168-types-integral-float-to-int.sx index 9d53099..bdd67bf 100644 --- a/examples/0168-types-integral-float-to-int.sx +++ b/examples/0168-types-integral-float-to-int.sx @@ -3,10 +3,14 @@ // `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 // 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 // `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 — // including a non-integral const expression (`xx (M + 0.5)` / `xx (F + 0.25)`). // @@ -69,12 +73,32 @@ main :: () { aa : ArrFE = ---; 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 — - // including a non-integral const EXPRESSION (`xx (M + 0.5)` → 2) and a - // non-integral float-const-LEAF expression (`xx (F + 0.25)` → 2). + // including a non-integral const EXPRESSION (`xx (M + 0.5)` → 2), a + // 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; c : s64 = cast(s64) 1.5; xc : s64 = xx (M + 0.5); 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); } diff --git a/examples/1146-diagnostics-nonintegral-float-to-int.sx b/examples/1146-diagnostics-nonintegral-float-to-int.sx index 04deee8..abd262d 100644 --- a/examples/1146-diagnostics-nonintegral-float-to-int.sx +++ b/examples/1146-diagnostics-nonintegral-float-to-int.sx @@ -4,13 +4,17 @@ // 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 // 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 -// `F : f64 : 2.5`, = 2.75) — all three are the core of issue 0095, which -// previously slipped through and truncated to 2. The fix is the integral-fold / -// non-integral-error rule shared across all five sites (local, field, param, -// const, and array dimension), applied to ANY compile-time-constant float -// expression (literal, int-const leaf, float-const leaf, and combinations). The -// array-dimension site phrases the same rejection as "must be an integer". +// with `M :: 2`), a FLOAT-const-leaf expression (`F + 0.25`, with `F : f64 : 2.5`, +// = 2.75), a builtin FLOAT numeric-limit leaf inside an expression +// (`f64.true_min + 0.5` = 0.5), or a float `%` whose remainder is non-integral +// (`5.5 % 2.0` = 1.5) — all of these are the core of issue 0095, which previously +// slipped through and truncated. The fix is the integral-fold / non-integral-error +// rule shared across all five sites (local, field, param, const, and array +// 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`) // 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 ye : s64 = M + 0.5; // non-integral int-const-EXPRESSION 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 b := Bad.{}; print("{} {} {}\n", b.f, b.fe, b.ff); print("{} {} {}\n", badLit(), badExpr(), badFlt()); - print("{} {} {} {}\n", y, ye, yf, ad.len); + print("{} {} {} {} {} {}\n", y, ye, yf, yn, ym, ad.len); } diff --git a/examples/expected/0168-types-integral-float-to-int.stdout b/examples/expected/0168-types-integral-float-to-int.stdout index c856d3a..9782e35 100644 --- a/examples/expected/0168-types-integral-float-to-int.stdout +++ b/examples/expected/0168-types-integral-float-to-int.stdout @@ -4,4 +4,6 @@ field=4 fieldExpr=4 fieldFlt=4 param=6 paramFlt=4 const=8 constFlt=4 len=8 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 diff --git a/examples/expected/1146-diagnostics-nonintegral-float-to-int.stderr b/examples/expected/1146-diagnostics-nonintegral-float-to-int.stderr index eaba571..ab1aa0b 100644 --- a/examples/expected/1146-diagnostics-nonintegral-float-to-int.stderr +++ b/examples/expected/1146-diagnostics-nonintegral-float-to-int.stderr @@ -1,59 +1,71 @@ 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`) - --> 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`) - --> 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 - --> examples/1146-diagnostics-nonintegral-float-to-int.sx:39:11 +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: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`) - --> 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`) - --> 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`) - --> 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`) - --> 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`) - --> 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`) - --> 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 | ^^^^^^^^ diff --git a/issues/0095-typed-local-float-int-narrowing.md b/issues/0095-typed-local-float-int-narrowing.md index 4be5ae6..3b6ba88 100644 --- a/issues/0095-typed-local-float-int-narrowing.md +++ b/issues/0095-typed-local-float-int-narrowing.md @@ -95,6 +95,28 @@ > 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). > +> **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 / `.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 — > 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, @@ -110,9 +132,15 @@ > len 4; 1146 adds `[F + 0.25]s64` erroring; `examples/1132` now expects the > precise non-integral-float dim wording. Unit: > `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 -> "foldCountI64 / foldDimU32 fold an integral float count, reject a non-integral -> one" (the count fold + the `non_integral_float` / `below_min` distinction). +> (covers the float-const leaf: `F` → 2.5, `F + 0.25` → 2.75, `F + 1.5` → 4.0; +> attempt 5 adds the numeric-limit leaf `f64.max`/`f64.true_min`/`f32.epsilon`, +> `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 A typed LOCAL (and likely typed param/field) silently truncates a floating-point diff --git a/readme.md b/readme.md index bde6bb2..6b05fa6 100644 --- a/readme.md +++ b/readme.md @@ -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 **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 -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 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 diff --git a/specs.md b/specs.md index 35f310c..c716d68 100644 --- a/specs.md +++ b/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 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 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 evaluation, so they agree at every site — direct, through a const, or via a type 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 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), - 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 **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 cast (`xx`/`cast`)". - This applies uniformly to a typed **local**, a function **param default**, a diff --git a/src/ir/program_index.test.zig b/src/ir/program_index.test.zig index e58e368..dff5337 100644 --- a/src/ir/program_index.test.zig +++ b/src/ir/program_index.test.zig @@ -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, 4.0), eval(&fh, ctx)); + // A builtin FLOAT numeric-limit accessor is a compile-time float leaf — the + // twin of `evalConstIntExpr`'s `.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 // float division by zero are not compile-time float leaves → null. var zp = nBin(.add, &z, &half); diff --git a/src/ir/program_index.zig b/src/ir/program_index.zig index f109ecd..0b55c81 100644 --- a/src/ir/program_index.zig +++ b/src/ir/program_index.zig @@ -258,10 +258,17 @@ pub fn evalConstIntExpr(node: *const Node, ctx: anytype) ?i64 { /// / comptime consts, `.min`/`.max`, and integer arithmetic resolve /// through the SINGLE int folder — no parallel integer logic here); only the /// genuinely float-producing shapes — a float literal, a NON-INTEGRAL float-const -/// leaf, a unary negate, and `+ - * /` arithmetic involving a float — are -/// evaluated here in `f64`. A `%`, comparison, or any other shape is not a +/// leaf, a builtin FLOAT numeric-limit accessor (`f64.max`, `f32.epsilon`, +/// `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. /// +/// 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 /// 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 @@ -280,6 +287,24 @@ pub fn evalConstFloatExpr(node: *const Node, ctx: anytype) ?f64 { // float (the integral / integer cases were caught by the int delegation). .identifier => |id| ctx.lookupFloatName(id.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 `.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 `.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) { .negate => { 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, .mul => 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, }; },