fix(ir): integral-float counts + range-checked value-param binds (0083)
Item 2 (Agra ruling): a compile-time INTEGRAL float (`4.0`, `N : f64 : 4.0`, `N :: 4.0`) used as an array dimension / Vector lane / generic value-param count / `inline for` bound now folds to its integer at the shared leaf — `program_index.floatToIntExact`, used by both the `.float_literal` arm of `evalConstIntExpr` and `moduleConstInt`. All four consumers route through the one evaluator, so `[4.0]s64` lays out the same `[4]s64` uniformly; a non-integral (`4.5`) or negative value stays rejected by the downstream `foldDimU32` gate. Pass-0 now pre-registers float-valued module consts for forward-alias parity with int consts. Item 1: a generic value-param bind (`Box($K: u32)`) never range-checked the folded arg, so `Box(5_000_000_000)` compiled and ran. The bind now range-checks against the param's declared type — a `u32` count through the shared `foldDimU32` gate (making program_index's "single u32 gate for value-param counts" doc true), any other integer type through the new `program_index.intTypeRange` — and emits a clean "value N does not fit in u32 parameter K" otherwise. The declared type is threaded via a new `TemplateParam.value_type`. Regressions: examples 0145 (integral-float array dim), 1504 (Vector lane), 0611 (inline-for bound), 0209 (value-param integral-float), 1132 (non-integral float dim rejected), 1133 (negative float dim rejected), 1134 (oversized u32 value-param rejected) + program_index float-fold unit tests. Gate: zig build, zig build test, 406/0 run_examples.
This commit is contained in:
@@ -18,6 +18,10 @@ pub const TemplateParam = struct {
|
||||
name: []const u8,
|
||||
is_type_param: bool, // true for $T: Type, false for $N: u32
|
||||
is_variadic: bool = false, // `..$Ts: []Type` — binds remaining type args as a pack
|
||||
// Declared constraint type NAME for a value (non-type) param (`$K: u32` →
|
||||
// "u32"), used to range-check the folded arg at instantiation; null for a
|
||||
// type/variadic param or when the constraint isn't a plain type name.
|
||||
value_type: ?[]const u8 = null,
|
||||
};
|
||||
|
||||
pub const ProtocolMethodInfo = struct {
|
||||
@@ -41,6 +45,24 @@ pub const ModuleConstInfo = struct {
|
||||
ty: TypeId,
|
||||
};
|
||||
|
||||
/// A finite, INTEGRAL `f64` (`4.0`) → its exact `i64` value; a non-integral
|
||||
/// (`4.5`), infinite, NaN, or out-of-`i64`-range float → null. THE single place
|
||||
/// the "an integral float counts as an integer count" rule lives, shared by the
|
||||
/// `.float_literal` leaf of `evalConstIntExpr` (a direct `[4.0]T` dim) and
|
||||
/// `moduleConstInt` (a float-typed module const `N : f64 : 4.0` used as a
|
||||
/// count). One source, so an integral float resolves to the SAME integer at
|
||||
/// every dimension / lane / count / value-param / inline-for site; positivity
|
||||
/// and u32-range are still enforced downstream by `foldDimU32`.
|
||||
pub fn floatToIntExact(v: f64) ?i64 {
|
||||
if (!std.math.isFinite(v)) return null;
|
||||
if (@trunc(v) != v) return null;
|
||||
// `-2^63` is exactly representable and is `minInt(i64)`; `2^63` is the first
|
||||
// f64 above `maxInt(i64)`. Guard both so `@intFromFloat`'s range assert can
|
||||
// never trip on a valid-but-oversized integral float.
|
||||
if (v < -9223372036854775808.0 or v >= 9223372036854775808.0) return null;
|
||||
return @intFromFloat(v);
|
||||
}
|
||||
|
||||
/// A name bound to a module-global integer constant → its value, else null.
|
||||
/// SINGLE source for both array-dimension resolvers — the stateful
|
||||
/// body-lowering path (`Lowering.comptimeIntNamed`) and the stateless
|
||||
@@ -48,12 +70,17 @@ pub const ModuleConstInfo = struct {
|
||||
/// which named consts a `[N]T` dimension resolves to; if they diverge, an array
|
||||
/// laid out via a type alias (`Arr :: [N]T`, stateless) gets a different length
|
||||
/// than the direct form (`a : [N]T`, stateful) — the issue-0083 miscompile.
|
||||
/// Untyped (`N :: 16`) and typed (`N : s64 : 16`) consts both store an
|
||||
/// `.int_literal` value node, so both resolve here identically.
|
||||
/// Untyped (`N :: 16`) and typed (`N : s64 : 16`) consts store an `.int_literal`
|
||||
/// value node; a float-typed const (`N : f64 : 4.0`, `N :: 4.0`) stores a
|
||||
/// `.float_literal` and resolves iff its value is an integral float (via
|
||||
/// `floatToIntExact`) — `4.5` is not an integer → null.
|
||||
pub fn moduleConstInt(consts: *const std.StringHashMap(ModuleConstInfo), name: []const u8) ?i64 {
|
||||
const ci = consts.get(name) orelse return null;
|
||||
if (ci.value.data == .int_literal) return ci.value.data.int_literal.value;
|
||||
return null;
|
||||
return switch (ci.value.data) {
|
||||
.int_literal => |lit| lit.value,
|
||||
.float_literal => |lit| floatToIntExact(lit.value),
|
||||
else => null,
|
||||
};
|
||||
}
|
||||
|
||||
/// Evaluate a constant integer expression to its value. THE single
|
||||
@@ -62,9 +89,10 @@ pub fn moduleConstInt(consts: *const std.StringHashMap(ModuleConstInfo), name: [
|
||||
/// args (`Vec(N, f32)`), and `inline for 0..M` bounds all route here so they
|
||||
/// cannot disagree on what a given expression evaluates to (the issue-0083
|
||||
/// two-resolver class of bug). Folds integer `+ - * / %` and unary negate over
|
||||
/// int literals and named module / comptime consts — recursively, so nested and
|
||||
/// parenthesised forms (`[M + N - 1]`, `[(M + 1) * 2]`) fold (a grouping `(…)`
|
||||
/// carries no AST node; the parser returns the inner expression).
|
||||
/// int literals, integral float literals (`[4.0]T` → 4, via `floatToIntExact`),
|
||||
/// and named module / comptime consts — recursively, so nested and parenthesised
|
||||
/// forms (`[M + N - 1]`, `[(M + 1) * 2]`) fold (a grouping `(…)` carries no AST
|
||||
/// node; the parser returns the inner expression).
|
||||
///
|
||||
/// Leaves resolve through the ctx, so each call site shares the SAME folding
|
||||
/// logic while contributing its own bindings:
|
||||
@@ -83,6 +111,8 @@ pub fn moduleConstInt(consts: *const std.StringHashMap(ModuleConstInfo), name: [
|
||||
pub fn evalConstIntExpr(node: *const Node, ctx: anytype) ?i64 {
|
||||
return switch (node.data) {
|
||||
.int_literal => |lit| lit.value,
|
||||
// An integral float literal (`[4.0]T`) folds to its integer; `4.5` → null.
|
||||
.float_literal => |lit| floatToIntExact(lit.value),
|
||||
.identifier => |id| ctx.lookupDimName(id.name),
|
||||
.type_expr => |te| ctx.lookupDimName(te.name),
|
||||
.field_access => |fa| blk: {
|
||||
@@ -166,6 +196,32 @@ pub fn reportDimError(diag: *errors.DiagnosticList, span: ?ast.Span, result: Dim
|
||||
}
|
||||
}
|
||||
|
||||
/// The inclusive `[min, max]` integer range a value of a fixed-width integer
|
||||
/// type can hold, addressed by the type NAME as written on a generic value-param
|
||||
/// constraint (`$K: u32`). null for a non-integer / unrecognised name — the
|
||||
/// caller then skips the range check (folds without bounding) rather than
|
||||
/// guessing. Bounds are clamped into `i64`: a `u64`/`usize` ceiling exceeds
|
||||
/// `i64`, but a folded value-param arg is already an `i64`, so `maxInt(i64)` is
|
||||
/// its effective ceiling and the only failure a `u64` param can have is a
|
||||
/// negative arg. THE single declared-type → range map for the value-param gate,
|
||||
/// so the bound at every binding site agrees. The `u32` count case is gated
|
||||
/// through `foldDimU32` instead (the documented dim/lane/value-param u32 gate);
|
||||
/// both encode the same `[0, maxInt(u32)]`.
|
||||
pub const IntRange = struct { min: i64, max: i64 };
|
||||
pub fn intTypeRange(name: []const u8) ?IntRange {
|
||||
const eql = std.mem.eql;
|
||||
if (eql(u8, name, "u8")) return .{ .min = 0, .max = std.math.maxInt(u8) };
|
||||
if (eql(u8, name, "u16")) return .{ .min = 0, .max = std.math.maxInt(u16) };
|
||||
if (eql(u8, name, "u32")) return .{ .min = 0, .max = std.math.maxInt(u32) };
|
||||
if (eql(u8, name, "u64") or eql(u8, name, "usize")) return .{ .min = 0, .max = std.math.maxInt(i64) };
|
||||
if (eql(u8, name, "s8")) return .{ .min = std.math.minInt(i8), .max = std.math.maxInt(i8) };
|
||||
if (eql(u8, name, "s16")) return .{ .min = std.math.minInt(i16), .max = std.math.maxInt(i16) };
|
||||
if (eql(u8, name, "s32")) return .{ .min = std.math.minInt(i32), .max = std.math.maxInt(i32) };
|
||||
if (eql(u8, name, "s64") or eql(u8, name, "isize") or eql(u8, name, "int"))
|
||||
return .{ .min = std.math.minInt(i64), .max = std.math.maxInt(i64) };
|
||||
return null;
|
||||
}
|
||||
|
||||
pub const GlobalInfo = struct { id: inst.GlobalId, ty: TypeId };
|
||||
|
||||
/// Single lowering access point for declaration-name / import / visibility
|
||||
|
||||
Reference in New Issue
Block a user