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

@@ -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 `<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
// float division by zero are not compile-time float leaves → null.
var zp = nBin(.add, &z, &half);