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:
agra
2026-06-05 19:26:22 +03:00
parent 74f675ac0b
commit e442cdf5e7
13 changed files with 393 additions and 18 deletions

View File

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