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:
@@ -106,8 +106,38 @@ const ModuleConstCtx = struct {
|
||||
pub fn lookupFloatName(self: ModuleConstCtx, name: []const u8) ?f64 {
|
||||
return moduleConstFloatFramed(self.consts, self.table, name, self.frame);
|
||||
}
|
||||
/// True iff `name` names a FLOAT-valued const (see `moduleConstFloatValuedFramed`),
|
||||
/// resolved through the SAME cycle-guarded frame so a float-const leaf that
|
||||
/// references another const is judged consistently with `lookupFloatName`.
|
||||
pub fn nameIsFloatTyped(self: ModuleConstCtx, name: []const u8) bool {
|
||||
return moduleConstFloatValuedFramed(self.consts, self.table, name, self.frame);
|
||||
}
|
||||
};
|
||||
|
||||
/// True iff `ty` is a float type — one half of the float-valued-const test the
|
||||
/// int folder's division arm relies on. Module consts only ever carry the builtin
|
||||
/// `f32` / `f64`.
|
||||
fn isFloatConstType(ty: TypeId) bool {
|
||||
return ty == .f32 or ty == .f64;
|
||||
}
|
||||
|
||||
/// True iff `name` is a FLOAT-valued module const — judged by the const's VALUE,
|
||||
/// not only its DECLARED type, so it catches both a typed float const
|
||||
/// (`K : f64 : 4.0`, `F : f64 : 2.5`) AND an UNTYPED float-EXPRESSION const
|
||||
/// (`ME :: 4.0 + 1.0`), whose pass-0 placeholder type is `s64` even though its
|
||||
/// value is float. The int folder's division arm consults this to tell a FLOAT
|
||||
/// division apart from an integer one even when both operands fold to integers
|
||||
/// (`K / 3`, `ME / 3`). `frame` cycle-guards a const whose value references
|
||||
/// another const; a name already on the chain has no compile-time value → not
|
||||
/// float-valued (issue 0095 / F0.11-6).
|
||||
fn moduleConstFloatValuedFramed(consts: *const std.StringHashMap(ModuleConstInfo), table: *const types.TypeTable, name: []const u8, parent: ?*const ModuleConstFrame) bool {
|
||||
if (moduleConstFrameContains(parent, name)) return false;
|
||||
const ci = consts.get(name) orelse return false;
|
||||
if (isFloatConstType(ci.ty)) return true;
|
||||
var frame = ModuleConstFrame{ .name = name, .parent = parent };
|
||||
return isFloatValuedExpr(ci.value, ModuleConstCtx{ .consts = consts, .table = table, .frame = &frame });
|
||||
}
|
||||
|
||||
/// A module const may serve as an integer COUNT only when its DECLARED type is
|
||||
/// numeric — an integer of any width or a float (an integral float folds to its
|
||||
/// int via `floatToIntExact`). `moduleConstIntFramed` consults this so a count
|
||||
@@ -173,6 +203,61 @@ pub fn moduleConstFloat(consts: *const std.StringHashMap(ModuleConstInfo), table
|
||||
return moduleConstFloatFramed(consts, table, name, null);
|
||||
}
|
||||
|
||||
/// True iff `name` is a FLOAT-valued module const — judged by VALUE, so it covers
|
||||
/// a typed float const (`K : f64 : 4.0`), an untyped float-EXPRESSION const
|
||||
/// (`ME :: 4.0 + 1.0`, whose placeholder type is `s64`), and a non-integral float
|
||||
/// const (`F : f64 : 2.5`). SINGLE source for the stateful (`Lowering`) and
|
||||
/// stateless (`type_bridge`) division-arm float checks, so they agree on which
|
||||
/// const-leaf divisions are float (issue 0095 / F0.11-6).
|
||||
pub fn moduleConstIsFloatTyped(consts: *const std.StringHashMap(ModuleConstInfo), table: *const types.TypeTable, name: []const u8) bool {
|
||||
return moduleConstFloatValuedFramed(consts, table, name, null);
|
||||
}
|
||||
|
||||
/// True iff `node` is a FLOAT-valued compile-time expression — a float literal,
|
||||
/// a float-typed const leaf (`F : f64 : 2.5`, `K : f64 : 4.0`), a builtin float
|
||||
/// numeric-limit (`f64.max`), or arithmetic over any of those. THE predicate the
|
||||
/// int folder's division arm consults: `/` with a float operand is FLOAT division
|
||||
/// (`5.0 / 2.0` = 2.5), and folding it with integer truncating division would
|
||||
/// silently accept a non-integral float at a count / typed binding (issue 0095 /
|
||||
/// F0.11-6). `+ - *` agree between int and float arithmetic for the integral
|
||||
/// operands the int folder ever sees (a non-integral operand folds to null first),
|
||||
/// so ONLY `/` needs this guard. A leaf name resolves through `ctx.nameIsFloatTyped`
|
||||
/// — the same ctx that supplies `lookupDimName`/`lookupFloatName` — so an INTEGRAL
|
||||
/// float const (`K : f64 : 4.0`, which folds to 4 as a standalone count) is still
|
||||
/// recognised as float-valued inside a division.
|
||||
///
|
||||
/// Also the precise "is this a compile-time float-valued initializer" test the
|
||||
/// typed-binding narrowing path (`Lowering.foldComptimeFloatInit`) uses alongside
|
||||
/// `inferExprType`, so an untyped float-EXPRESSION const (`ME :: 4.0 + 1.0`,
|
||||
/// placeholder type `s64`) flowing into an integer binding (`x : s64 = ME / 2`)
|
||||
/// is judged float-valued even though `inferExprType` reads its placeholder type.
|
||||
pub fn isFloatValuedExpr(node: *const Node, ctx: anytype) bool {
|
||||
return switch (node.data) {
|
||||
.float_literal => true,
|
||||
.int_literal => false,
|
||||
.identifier => |id| ctx.nameIsFloatTyped(id.name),
|
||||
.type_expr => |te| ctx.nameIsFloatTyped(te.name),
|
||||
.field_access => |fa| blk: {
|
||||
// A backtick RAW receiver (`` `f64.epsilon ``) is an ordinary field
|
||||
// READ on a value whose spelling shadows a builtin type, NOT the
|
||||
// numeric-limit accessor — so it is not a float leaf (issues 0092 /
|
||||
// 0093). Only a BARE type receiver folds to a float limit.
|
||||
const obj_name: ?[]const u8 = switch (fa.object.data) {
|
||||
.identifier => |id| if (id.is_raw) null else id.name,
|
||||
.type_expr => |te| if (te.is_raw) null else te.name,
|
||||
else => null,
|
||||
};
|
||||
if (obj_name) |on| {
|
||||
if (type_resolver.TypeResolver.floatLimitFor(on, fa.field) != null) break :blk true;
|
||||
}
|
||||
break :blk false;
|
||||
},
|
||||
.unary_op => |u| isFloatValuedExpr(u.operand, ctx),
|
||||
.binary_op => |b| isFloatValuedExpr(b.lhs, ctx) or isFloatValuedExpr(b.rhs, ctx),
|
||||
else => false,
|
||||
};
|
||||
}
|
||||
|
||||
/// Evaluate a constant integer expression to its value. THE single
|
||||
/// integer-expression folder for the compiler — array dimensions (`[N]T`,
|
||||
/// `[M + 1]T`), Vector lane counts (`Vector(N, f32)`), generic value-param
|
||||
@@ -184,6 +269,13 @@ pub fn moduleConstFloat(consts: *const std.StringHashMap(ModuleConstInfo), table
|
||||
/// forms (`[M + N - 1]`, `[(M + 1) * 2]`) fold (a grouping `(…)` carries no AST
|
||||
/// node; the parser returns the inner expression).
|
||||
///
|
||||
/// ONE exception keeps a float operation out of integer arithmetic: a `/` whose
|
||||
/// lhs/rhs is float-valued (`5.0 / 2.0`, `K / 3` with `K : f64 : 4.0`) is FLOAT
|
||||
/// division, NOT integer truncation, so this folder refuses it (`isFloatValuedExpr`)
|
||||
/// and lets `evalConstFloatExpr` + the unified narrowing rule see the true value
|
||||
/// (issue 0095 / F0.11-6). `+ - *` need no such guard — they agree between int and
|
||||
/// float arithmetic for the integral operands this folder ever sees.
|
||||
///
|
||||
/// Leaves resolve through the ctx, so each call site shares the SAME folding
|
||||
/// logic while contributing its own bindings:
|
||||
/// - `ctx.lookupDimName(name)` — a name bound to a compile-time integer. The
|
||||
@@ -238,7 +330,17 @@ pub fn evalConstIntExpr(node: *const Node, ctx: anytype) ?i64 {
|
||||
.add => std.math.add(i64, l, r) catch null,
|
||||
.sub => std.math.sub(i64, l, r) catch null,
|
||||
.mul => std.math.mul(i64, l, r) catch null,
|
||||
.div => std.math.divTrunc(i64, l, r) catch null,
|
||||
// A division with a FLOAT operand is FLOAT division (`5.0 / 2.0`
|
||||
// = 2.5, `K / 3` with `K : f64 : 4.0` = 1.333…), NOT integer
|
||||
// truncating division — refuse to fold it here so the value
|
||||
// surfaces through `evalConstFloatExpr` + the unified float→int
|
||||
// rule (integral folds, non-integral errors) instead of silently
|
||||
// truncating to an integer (issue 0095 / F0.11-6). A genuine
|
||||
// integer `/` (both operands integer-valued) still truncates.
|
||||
.div => if (isFloatValuedExpr(b.lhs, ctx) or isFloatValuedExpr(b.rhs, ctx))
|
||||
null
|
||||
else
|
||||
std.math.divTrunc(i64, l, r) catch null,
|
||||
.mod => if (r == 0) null else @rem(l, r),
|
||||
else => null,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user