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:
@@ -104,6 +104,10 @@ const DimCtx = struct {
|
||||
pub fn lookupDimName(_: DimCtx, name: []const u8) ?i64 {
|
||||
if (std.mem.eql(u8, name, "M")) return 4;
|
||||
if (std.mem.eql(u8, name, "N")) return 6;
|
||||
// `K : f64 : 4.0` is an INTEGRAL float const: it folds to 4 through the
|
||||
// int delegation (`floatToIntExact`) yet stays float-typed — the case the
|
||||
// division guard must still recognise as float division.
|
||||
if (std.mem.eql(u8, name, "K")) return 4;
|
||||
return null;
|
||||
}
|
||||
// `xs` stands in for a pack of arity 3; every other name has no pack length.
|
||||
@@ -113,12 +117,19 @@ const DimCtx = struct {
|
||||
}
|
||||
// `F` stands in for a NON-INTEGRAL float module const (`F : f64 : 2.5`): the
|
||||
// int folder cannot resolve it, so only the float-leaf lookup surfaces it.
|
||||
// Integer consts (`M`/`N`) are resolved by the int delegation and never reach
|
||||
// this arm; `Z` is genuinely runtime.
|
||||
// `K` stands in for an INTEGRAL float const (`K : f64 : 4.0`) — it folds to 4
|
||||
// through the int delegation yet is still float-typed. Integer consts (`M`/`N`)
|
||||
// are resolved by the int delegation and never reach this arm; `Z` is runtime.
|
||||
pub fn lookupFloatName(_: DimCtx, name: []const u8) ?f64 {
|
||||
if (std.mem.eql(u8, name, "F")) return 2.5;
|
||||
if (std.mem.eql(u8, name, "K")) return 4.0;
|
||||
return null;
|
||||
}
|
||||
// The float-typed-const predicate the division guard consults: `F`/`K` are
|
||||
// float-typed module consts, every other name is not.
|
||||
pub fn nameIsFloatTyped(_: DimCtx, name: []const u8) bool {
|
||||
return std.mem.eql(u8, name, "F") or std.mem.eql(u8, name, "K");
|
||||
}
|
||||
};
|
||||
|
||||
fn nLit(v: i64) ast.Node {
|
||||
@@ -271,6 +282,53 @@ test "moduleConstInt folds expression-RHS consts and rejects cycles" {
|
||||
try std.testing.expect(pi.moduleConstInt(&map, &table, "C") == null);
|
||||
}
|
||||
|
||||
test "moduleConstIsFloatTyped judges a const by VALUE, catching untyped float-EXPR consts" {
|
||||
var table = types.TypeTable.init(std.testing.allocator);
|
||||
defer table.deinit();
|
||||
var map = std.StringHashMap(pi.ModuleConstInfo).init(std.testing.allocator);
|
||||
defer map.deinit();
|
||||
|
||||
// KT : f64 : 4.0 (typed float), MI :: 2 (untyped int), ML :: 5.0 (untyped
|
||||
// float literal → f64), ME :: 4.0 + 1.0 (untyped float EXPRESSION, placeholder
|
||||
// type s64 yet float-valued), IE :: 1 + 2 (untyped int expression).
|
||||
var kt_val = nFloat(4.0);
|
||||
var mi_val = nLit(2);
|
||||
var ml_val = nFloat(5.0);
|
||||
var four = nFloat(4.0);
|
||||
var one_f = nFloat(1.0);
|
||||
var me_val = nBin(.add, &four, &one_f);
|
||||
var l1 = nLit(1);
|
||||
var l2 = nLit(2);
|
||||
var ie_val = nBin(.add, &l1, &l2);
|
||||
try map.put("KT", .{ .value = &kt_val, .ty = .f64 });
|
||||
try map.put("MI", .{ .value = &mi_val, .ty = .s64 });
|
||||
try map.put("ML", .{ .value = &ml_val, .ty = .f64 }); // pass-0 stores a float literal as f64
|
||||
try map.put("ME", .{ .value = &me_val, .ty = .s64 }); // pass-0 placeholder for a binary_op
|
||||
try map.put("IE", .{ .value = &ie_val, .ty = .s64 });
|
||||
|
||||
// Float-valued: a typed float const, an untyped float literal, AND an untyped
|
||||
// float EXPRESSION whose declared type is the s64 placeholder (judged by value).
|
||||
try std.testing.expect(pi.moduleConstIsFloatTyped(&map, &table, "KT"));
|
||||
try std.testing.expect(pi.moduleConstIsFloatTyped(&map, &table, "ML"));
|
||||
try std.testing.expect(pi.moduleConstIsFloatTyped(&map, &table, "ME"));
|
||||
// NOT float-valued: an int const, an int expression, an absent name.
|
||||
try std.testing.expect(!pi.moduleConstIsFloatTyped(&map, &table, "MI"));
|
||||
try std.testing.expect(!pi.moduleConstIsFloatTyped(&map, &table, "IE"));
|
||||
try std.testing.expect(!pi.moduleConstIsFloatTyped(&map, &table, "absent"));
|
||||
|
||||
// A cyclic const has no value: the frame guard returns false without looping.
|
||||
var a_id = nIdent("A");
|
||||
var b_id = nIdent("B");
|
||||
var az = nFloat(0.0);
|
||||
var a_val = nBin(.add, &b_id, &az);
|
||||
var b_val = nBin(.add, &a_id, &az);
|
||||
try map.put("A", .{ .value = &a_val, .ty = .s64 });
|
||||
try map.put("B", .{ .value = &b_val, .ty = .s64 });
|
||||
// The `+ 0.0` literal still makes them float-valued (a finite, non-cyclic leaf
|
||||
// is reached before the cycle); the point is it TERMINATES.
|
||||
try std.testing.expect(pi.moduleConstIsFloatTyped(&map, &table, "A"));
|
||||
}
|
||||
|
||||
test "moduleConstInt gates the fold on the declared type, not the initializer node" {
|
||||
var table = types.TypeTable.init(std.testing.allocator);
|
||||
defer table.deinit();
|
||||
@@ -446,3 +504,56 @@ test "foldCountI64 / foldDimU32 fold an integral float count, reject a non-integ
|
||||
var negf = nNeg(&f4); // -4.0 → -4
|
||||
try std.testing.expectEqual(pi.DimU32{ .below_min = -4 }, pi.foldDimU32(&negf, ctx, 0));
|
||||
}
|
||||
|
||||
test "the int folder refuses a FLOAT division (issue 0095 / F0.11-6)" {
|
||||
const eval = pi.evalConstIntExpr;
|
||||
const ctx = DimCtx{}; // K : f64 : 4.0 (integral float const), M = 4 (int const)
|
||||
|
||||
var five = nLit(5);
|
||||
var two = nLit(2);
|
||||
var six = nLit(6);
|
||||
var f5 = nFloat(5.0);
|
||||
var f2 = nFloat(2.0);
|
||||
var f6 = nFloat(6.0);
|
||||
var k = nIdent("K"); // integral float const (folds to 4, yet float-typed)
|
||||
var m = nIdent("M"); // integer const (4)
|
||||
|
||||
// Genuine INTEGER division still truncates (`5 / 2` → 2, `6 / 2` → 3).
|
||||
var idiv = nBin(.div, &five, &two);
|
||||
var idiv2 = nBin(.div, &six, &two);
|
||||
try std.testing.expectEqual(@as(?i64, 2), eval(&idiv, ctx));
|
||||
try std.testing.expectEqual(@as(?i64, 3), eval(&idiv2, ctx));
|
||||
|
||||
// FLOAT division is REFUSED by the int folder (returns null), even when the
|
||||
// result is integral (`6.0 / 2.0`) — so it surfaces through the float folder
|
||||
// + the unified narrowing rule instead of truncating. A float operand on
|
||||
// either side (literal or float-typed const) is enough.
|
||||
var fdiv_nonint = nBin(.div, &f5, &f2); // 5.0 / 2.0 = 2.5
|
||||
var fdiv_int = nBin(.div, &f6, &f2); // 6.0 / 2.0 = 3.0 (integral, still refused)
|
||||
var fdiv_mixedl = nBin(.div, &f5, &two); // 5.0 / 2 = 2.5 (mixed promotes to float)
|
||||
var fdiv_mixedr = nBin(.div, &five, &f2); // 5 / 2.0 = 2.5
|
||||
var fdiv_const = nBin(.div, &k, &two); // K / 2 = 4.0/2 = 2.0 (float const, refused)
|
||||
try std.testing.expect(eval(&fdiv_nonint, ctx) == null);
|
||||
try std.testing.expect(eval(&fdiv_int, ctx) == null);
|
||||
try std.testing.expect(eval(&fdiv_mixedl, ctx) == null);
|
||||
try std.testing.expect(eval(&fdiv_mixedr, ctx) == null);
|
||||
try std.testing.expect(eval(&fdiv_const, ctx) == null);
|
||||
|
||||
// The float folder recovers the TRUE float value of the refused divisions, so
|
||||
// the unified rule can fold the integral one and reject the non-integral one.
|
||||
const evalf = pi.evalConstFloatExpr;
|
||||
try std.testing.expectEqual(@as(?f64, 2.5), evalf(&fdiv_nonint, ctx));
|
||||
try std.testing.expectEqual(@as(?f64, 3.0), evalf(&fdiv_int, ctx));
|
||||
try std.testing.expectEqual(@as(?f64, 2.0), evalf(&fdiv_const, ctx));
|
||||
// An int-const division (`M / 2` = 4/2) is NOT float division — it truncates.
|
||||
var mdiv = nBin(.div, &m, &two);
|
||||
try std.testing.expectEqual(@as(?i64, 2), eval(&mdiv, ctx));
|
||||
|
||||
// Non-division float arithmetic is unaffected: `*`/`+`/`-` over integral
|
||||
// operands agree between int and float, so they still fold via the int folder
|
||||
// (`6.0 * 2.0` → 12, `K - 2.0` → 2).
|
||||
var fmul = nBin(.mul, &f6, &f2); // 6.0 * 2.0 = 12
|
||||
var ksub = nBin(.sub, &k, &f2); // K - 2.0 = 2
|
||||
try std.testing.expectEqual(@as(?i64, 12), eval(&fmul, ctx));
|
||||
try std.testing.expectEqual(@as(?i64, 2), eval(&ksub, ctx));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user