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:
131
src/ir/lower.zig
131
src/ir/lower.zig
@@ -653,22 +653,27 @@ pub const Lowering = struct {
|
||||
|
||||
/// Pass 1: Scan declarations — register ASTs and extern stubs, but don't lower bodies.
|
||||
fn scanDecls(self: *Lowering, decls: []const *const Node) void {
|
||||
// Pass 0: register every integer-valued module const (`N :: 16` and the
|
||||
// typed `N : s64 : 16`) BEFORE any type alias is resolved below. A type
|
||||
// alias whose dimension is a named const (`Arr :: [N]T`) resolves its
|
||||
// dimension eagerly here, on the stateless registration path; that path
|
||||
// can only read `module_const_map`. Untyped consts would otherwise be
|
||||
// registered only in declaration order (pass 1) and typed ones only after
|
||||
// the alias fixpoint (pass 2) — so an alias declared before its const, or
|
||||
// any alias over a typed const, saw an empty table and miscompiled the
|
||||
// dimension to length 0 (issue 0083). The dimension only needs the value,
|
||||
// so a placeholder type is fine; pass 2 overwrites typed consts with the
|
||||
// resolved annotation type (issue 0070).
|
||||
// Pass 0: register every numeric-literal module const (`N :: 16` and the
|
||||
// typed `N : s64 : 16`, plus float-valued `N :: 4.0` / `N : f64 : 4.0`)
|
||||
// BEFORE any type alias is resolved below. A type alias whose dimension is
|
||||
// a named const (`Arr :: [N]T`) resolves its dimension eagerly here, on
|
||||
// the stateless registration path; that path can only read
|
||||
// `module_const_map`. Untyped consts would otherwise be registered only in
|
||||
// declaration order (pass 1) and typed ones only after the alias fixpoint
|
||||
// (pass 2) — so an alias declared before its const, or any alias over a
|
||||
// typed const, saw an empty table and miscompiled the dimension to length
|
||||
// 0 (issue 0083). A float-valued const resolves to a dimension only when
|
||||
// its value is integral (`floatToIntExact`); pre-registering it keeps the
|
||||
// forward-alias float path identical to the int path. The dimension only
|
||||
// needs the value, so a placeholder type is fine; pass 2 overwrites typed
|
||||
// consts with the resolved annotation type (issue 0070).
|
||||
for (decls) |decl| {
|
||||
if (decl.data != .const_decl) continue;
|
||||
const cd = decl.data.const_decl;
|
||||
if (cd.value.data == .int_literal) {
|
||||
self.program_index.module_const_map.put(cd.name, .{ .value = cd.value, .ty = .s64 }) catch {};
|
||||
switch (cd.value.data) {
|
||||
.int_literal => self.program_index.module_const_map.put(cd.name, .{ .value = cd.value, .ty = .s64 }) catch {},
|
||||
.float_literal => self.program_index.module_const_map.put(cd.name, .{ .value = cd.value, .ty = .f64 }) catch {},
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
for (decls) |decl| {
|
||||
@@ -11852,17 +11857,65 @@ pub const Lowering = struct {
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve a generic value-param argument (`$N: u32`) to its compile-time
|
||||
/// integer through the shared `evalConstIntExpr` folder, so a module/generic
|
||||
/// const arg (`Vec(N, f32)`) binds the same value — and mangles to the same
|
||||
/// instantiation — a literal (`Vec(3, f32)`) would. A non-const arg emits a
|
||||
/// clean diagnostic and returns null; the caller bails rather than
|
||||
/// fabricating a 0 binding under a wrong mangled name.
|
||||
fn resolveValueParamArg(self: *Lowering, arg_node: *const Node) ?i64 {
|
||||
if (program_index_mod.evalConstIntExpr(arg_node, self)) |v| return v;
|
||||
/// Resolve a generic value-param argument (`$K: u32`) to its compile-time
|
||||
/// integer AND verify it fits the param's declared integer type. The folded
|
||||
/// value is bound and mangled into the instantiation name, so a module/generic
|
||||
/// const arg (`Vec(N, f32)`), a const expression (`Make(M + 1, s64)`), an
|
||||
/// integral float (`Box(4.0)` → 4), and a literal (`Vec(3, f32)`) all bind the
|
||||
/// same value a literal would. An out-of-range arg (`Box(5_000_000_000)` for a
|
||||
/// `u32` param) or a non-const arg emits a clean diagnostic and returns null;
|
||||
/// the caller bails rather than binding a truncated / fabricated value under a
|
||||
/// wrong mangled name.
|
||||
///
|
||||
/// `type_name` is the param's declared constraint type (`"u32"`, null if
|
||||
/// unknown). A `u32` count routes through the shared
|
||||
/// `program_index.foldDimU32` — the SAME fold-and-narrow gate an array dim /
|
||||
/// Vector lane uses — so the documented "single u32 gate for value-param
|
||||
/// counts" holds; any other integer type range-checks against
|
||||
/// `program_index.intTypeRange`; an unrecognised type folds without bounding.
|
||||
fn resolveValueParamArg(self: *Lowering, arg_node: *const Node, param_name: []const u8, type_name: ?[]const u8) ?i64 {
|
||||
if (type_name) |tn| {
|
||||
if (std.mem.eql(u8, tn, "u32")) {
|
||||
switch (program_index_mod.foldDimU32(arg_node, self, 0)) {
|
||||
.ok => |n| return n,
|
||||
.not_const => {
|
||||
self.diagValueParamNotConst(arg_node, param_name);
|
||||
return null;
|
||||
},
|
||||
.below_min => |v| {
|
||||
self.diagValueParamRange(arg_node, param_name, tn, v);
|
||||
return null;
|
||||
},
|
||||
.too_large => |v| {
|
||||
self.diagValueParamRange(arg_node, param_name, tn, v);
|
||||
return null;
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
const v = program_index_mod.evalConstIntExpr(arg_node, self) orelse {
|
||||
self.diagValueParamNotConst(arg_node, param_name);
|
||||
return null;
|
||||
};
|
||||
if (type_name) |tn| {
|
||||
if (program_index_mod.intTypeRange(tn)) |r| {
|
||||
if (v < r.min or v > r.max) {
|
||||
self.diagValueParamRange(arg_node, param_name, tn, v);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
fn diagValueParamNotConst(self: *Lowering, arg_node: *const Node, param_name: []const u8) void {
|
||||
if (self.diagnostics) |d|
|
||||
d.addFmt(.err, arg_node.span, "generic value parameter must be a compile-time integer constant", .{});
|
||||
return null;
|
||||
d.addFmt(.err, arg_node.span, "generic value parameter '{s}' must be a compile-time integer constant", .{param_name});
|
||||
}
|
||||
|
||||
fn diagValueParamRange(self: *Lowering, arg_node: *const Node, param_name: []const u8, type_name: []const u8, value: i64) void {
|
||||
if (self.diagnostics) |d|
|
||||
d.addFmt(.err, arg_node.span, "value {} does not fit in {s} parameter {s}", .{ value, type_name, param_name });
|
||||
}
|
||||
|
||||
/// Resolve a .call node that represents a type constructor (e.g., List(T), Vector(N, T)).
|
||||
@@ -11999,8 +12052,9 @@ pub const Lowering = struct {
|
||||
const tname = self.formatTypeName(ty);
|
||||
name_parts.appendSlice(self.alloc, tname) catch {};
|
||||
} else {
|
||||
// Value param (e.g., $N: u32) — fold to a compile-time integer.
|
||||
const val = self.resolveValueParamArg(args[i]) orelse return .unresolved;
|
||||
// Value param (e.g., $N: u32) — fold to a compile-time integer
|
||||
// and range-check against its declared type.
|
||||
const val = self.resolveValueParamArg(args[i], tp.name, tp.value_type) orelse return .unresolved;
|
||||
cvb.put(tp.name, val) catch {};
|
||||
var val_buf: [32]u8 = undefined;
|
||||
const val_str = std.fmt.bufPrint(&val_buf, "{d}", .{val}) catch "0";
|
||||
@@ -12093,8 +12147,10 @@ pub const Lowering = struct {
|
||||
const tname = self.formatTypeName(ty);
|
||||
name_parts.appendSlice(self.alloc, tname) catch {};
|
||||
} else {
|
||||
// Value param (e.g., $N: u32) — fold to a compile-time integer.
|
||||
const val = self.resolveValueParamArg(args[i]) orelse return null;
|
||||
// Value param (e.g., $N: u32) — fold to a compile-time integer
|
||||
// and range-check against its declared type.
|
||||
const vp_type: ?[]const u8 = if (tp.constraint.data == .type_expr) tp.constraint.data.type_expr.name else null;
|
||||
const val = self.resolveValueParamArg(args[i], tp.name, vp_type) orelse return null;
|
||||
cvb.put(tp.name, val) catch {};
|
||||
var val_buf: [32]u8 = undefined;
|
||||
const val_str = std.fmt.bufPrint(&val_buf, "{d}", .{val}) catch "0";
|
||||
@@ -12309,19 +12365,26 @@ pub const Lowering = struct {
|
||||
// Build owned type_params
|
||||
const tps = self.alloc.alloc(TemplateParam, sd.type_params.len) catch return;
|
||||
for (sd.type_params, 0..) |tp, i| {
|
||||
const is_type_param = tp.is_variadic or (if (tp.constraint.data == .type_expr) blk: {
|
||||
const cname = tp.constraint.data.type_expr.name;
|
||||
// "Type" or a protocol name → type param
|
||||
break :blk std.mem.eql(u8, cname, "Type") or
|
||||
self.program_index.protocol_decl_map.contains(cname) or
|
||||
self.program_index.protocol_ast_map.contains(cname);
|
||||
} else false);
|
||||
tps[i] = .{
|
||||
.name = self.alloc.dupe(u8, tp.name) catch return,
|
||||
// $T: Type, $T: Lerpable, $T: Type/Eq — all are type params.
|
||||
// `..$Ts: []Type` (variadic) is a type-pack param. Only value
|
||||
// params like $N: u32 are non-type.
|
||||
.is_type_param = tp.is_variadic or (if (tp.constraint.data == .type_expr) blk: {
|
||||
const cname = tp.constraint.data.type_expr.name;
|
||||
// "Type" or a protocol name → type param
|
||||
break :blk std.mem.eql(u8, cname, "Type") or
|
||||
self.program_index.protocol_decl_map.contains(cname) or
|
||||
self.program_index.protocol_ast_map.contains(cname);
|
||||
} else false),
|
||||
.is_type_param = is_type_param,
|
||||
.is_variadic = tp.is_variadic,
|
||||
// Capture a value param's declared type name (`$K: u32` →
|
||||
// "u32") so instantiation can range-check the folded arg.
|
||||
.value_type = if (!is_type_param and tp.constraint.data == .type_expr)
|
||||
(self.alloc.dupe(u8, tp.constraint.data.type_expr.name) catch null)
|
||||
else
|
||||
null,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user