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

@@ -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