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);

View File

@@ -258,10 +258,17 @@ pub fn evalConstIntExpr(node: *const Node, ctx: anytype) ?i64 {
/// / comptime consts, `<IntType>.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 `<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) {
.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,
};
},