fix(ir): route every comptime-int through the shared evaluator (0083)

Attempts 1–4 fixed the array-dimension paths but the same length-0
fabrication class survived on every other site that resolves a
compile-time integer. Unify them all on the single shared
`program_index.evalConstIntExpr` so they cannot diverge:

- All three Vector lane resolvers (resolveTypeCallWithBindings,
  resolveParameterizedWithBindings, resolveArrayLiteralType) and both
  generic value-param binders (instantiateGenericStruct,
  instantiateTypeFunction) hand-rolled an `else => 0` switch. A
  module-const lane `Vector(N, f32)` fabricated a 0-lane `<0 x float>`
  (LLVM "huge alignment" abort); a value-param `Vec(N, f32)` fabricated
  a 0 binding / wrong mangled name. They now fold through the shared
  evaluator and emit a clean diagnostic + `.unresolved` on a non-const
  operand (resolveVectorLane / resolveValueParamArg) — never 0.
- evalComptimeInt (inline-for bounds) delegated to the shared evaluator,
  so `inline for 0..M` / `0..(M+1)` fold like array dims. The `<pack>.len`
  leaf moved into the shared folder via a new `ctx.lookupPackLen`.
- The unknown-type semantic checker no longer walks a value-param
  position (`Vector(N, …)` / `Vec(N, …)`) as a type name (was reporting
  "unknown type 'N'").
- The parameterized-type-arg parser and the function-body lookahead
  (hasFnBodyAfterArrow) accept a const-EXPRESSION in a value position, so
  `Vector(M + 1, f32)` and `[M + 1]T` parse as a return type too (the
  latter a pre-existing array-dim sibling that the same heuristic broke).

Regressions: examples/1501 (named-const + const-expr lane, direct +
alias, 3/4-lane reads), 1502 (runtime lane clean-halts, exit 1, no LLVM
crash), 0207 (Vec(N)/Vec(M+1) == Vec(3) instantiation), 0610 (inline-for
const bounds). Shared-evaluator unit test extended with the pack-len arm.

zig build && zig build test && bash tests/run_examples.sh: 395 passed,
0 failed.
This commit is contained in:
agra
2026-06-04 11:32:25 +03:00
parent cd39316f5e
commit a491a1bf73
23 changed files with 340 additions and 93 deletions

View File

@@ -3984,32 +3984,14 @@ pub const Lowering = struct {
return self.builder.constInt(0, .void);
}
/// Evaluate a node to a comptime integer: literal, comptime-constant
/// identifier, or `<pack>.len` (resolves to the monomorphised arity).
/// Evaluate an `inline for` range bound to a comptime integer. Delegates to
/// the shared `program_index.evalConstIntExpr` — the SAME folder the array
/// dimension / Vector lane / value-param paths use — so a literal, a comptime
/// constant (cursor), a module/generic const (`inline for 0..M`), a
/// `<pack>.len` leaf, and any constant-foldable expression over those
/// (`inline for 0..(M + 1)`) all resolve identically. One folder, one answer.
fn evalComptimeInt(self: *Lowering, node: *const Node) ?i64 {
switch (node.data) {
.int_literal => |lit| return lit.value,
.identifier => |id| {
if (self.comptime_constants.get(id.name)) |cv| {
switch (cv) {
.int_val => |iv| return iv,
else => return null,
}
}
return null;
},
.field_access => |fa| {
if (self.pack_param_count) |ppc| {
if (fa.object.data == .identifier and std.mem.eql(u8, fa.field, "len")) {
if (ppc.get(fa.object.data.identifier.name)) |n| {
return @as(i64, @intCast(n));
}
}
}
return null;
},
else => return null,
}
return program_index_mod.evalConstIntExpr(node, self);
}
fn lowerMatch(self: *Lowering, me: *const ast.MatchExpr) Ref {
@@ -5428,12 +5410,9 @@ pub const Lowering = struct {
};
if (std.mem.eql(u8, callee_name, "Vector")) {
if (cl.args.len == 2) {
const length: u32 = switch (cl.args[0].data) {
.int_literal => |lit| @intCast(@as(u64, @bitCast(lit.value))),
else => 0,
};
const length = self.resolveVectorLane(cl.args[0]) orelse return .unresolved;
const elem = self.resolveTypeWithBindings(cl.args[1]);
if (length > 0) return self.module.types.vectorOf(elem, length);
return self.module.types.vectorOf(elem, length);
}
}
// Try as generic struct
@@ -11699,6 +11678,18 @@ pub const Lowering = struct {
return self.comptimeIntNamed(name);
}
/// Pack-length leaf for the shared integer-expression evaluator: a pack
/// name's monomorphised arity (e.g. an `inline for 0..xs.len` bound).
/// Resolves through `pack_param_count`, which is populated when a comptime
/// call binds a pack name. A name with no active pack binding is not a
/// compile-time integer leaf here → null.
pub fn lookupPackLen(self: *Lowering, name: []const u8) ?i64 {
if (self.pack_param_count) |ppc| {
if (ppc.get(name)) |n| return @intCast(n);
}
return null;
}
/// Resolve a name to a compile-time integer across the three const tables.
fn comptimeIntNamed(self: *Lowering, name: []const u8) ?i64 {
if (self.comptime_constants.get(name)) |cv| switch (cv) {
@@ -11828,6 +11819,36 @@ pub const Lowering = struct {
return .{ .l = self };
}
/// Resolve a `Vector(N, T)` lane count to a positive compile-time integer
/// through the shared `evalConstIntExpr` folder — so a literal (`Vector(4,
/// f32)`), a module/generic const (`Vector(N, f32)`), and a const expression
/// (`Vector(M + 1, f32)`) all resolve identically. A non-const lane
/// (`Vector(get(), f32)`) or a non-positive one emits a clean diagnostic and
/// returns null; the caller yields `.unresolved` rather than fabricating a
/// `<0 x float>` lane count that crashes LLVM verification.
fn resolveVectorLane(self: *Lowering, lane_node: *const Node) ?u32 {
const v = program_index_mod.evalConstIntExpr(lane_node, self);
if (v == null or v.? < 1) {
if (self.diagnostics) |d|
d.addFmt(.err, lane_node.span, "Vector lane count must be a positive compile-time integer constant", .{});
return null;
}
return @intCast(v.?);
}
/// 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;
if (self.diagnostics) |d|
d.addFmt(.err, arg_node.span, "generic value parameter must be a compile-time integer constant", .{});
return null;
}
/// Resolve a .call node that represents a type constructor (e.g., List(T), Vector(N, T)).
fn resolveTypeCallWithBindings(self: *Lowering, cl: *const ast.Call) TypeId {
const callee_name: []const u8 = switch (cl.callee.data) {
@@ -11837,16 +11858,7 @@ pub const Lowering = struct {
};
// Built-in: Vector(N, T)
if (std.mem.eql(u8, callee_name, "Vector") and cl.args.len == 2) {
const length: u32 = switch (cl.args[0].data) {
.int_literal => |lit| @intCast(@as(u64, @bitCast(lit.value))),
.identifier => |id| blk: {
if (self.comptime_value_bindings) |cvb| {
if (cvb.get(id.name)) |v| break :blk @intCast(@as(u64, @bitCast(v)));
}
break :blk 0;
},
else => 0,
};
const length = self.resolveVectorLane(cl.args[0]) orelse return .unresolved;
const elem = self.resolveTypeWithBindings(cl.args[1]);
return self.module.types.vectorOf(elem, length);
}
@@ -11878,24 +11890,7 @@ pub const Lowering = struct {
// Vector(N, T) — built-in parameterized type
if (std.mem.eql(u8, base_name, "Vector")) {
if (pt.args.len == 2) {
// Resolve length: literal, or bound comptime value
const length: u32 = switch (pt.args[0].data) {
.int_literal => |lit| @intCast(@as(u64, @bitCast(lit.value))),
.identifier => |id| blk: {
if (self.comptime_value_bindings) |cvb| {
if (cvb.get(id.name)) |v| break :blk @intCast(@as(u64, @bitCast(v)));
}
break :blk 0;
},
.type_expr => |te| blk: {
if (self.comptime_value_bindings) |cvb| {
if (cvb.get(te.name)) |v| break :blk @intCast(@as(u64, @bitCast(v)));
}
break :blk 0;
},
else => 0,
};
// Resolve element type through bindings
const length = self.resolveVectorLane(pt.args[0]) orelse return .unresolved;
const elem = self.resolveTypeWithBindings(pt.args[1]);
return table.vectorOf(elem, length);
}
@@ -11974,11 +11969,8 @@ pub const Lowering = struct {
const tname = self.formatTypeName(ty);
name_parts.appendSlice(self.alloc, tname) catch {};
} else {
// Value param (e.g., $N: u32) — extract integer
const val: i64 = switch (args[i].data) {
.int_literal => |lit| lit.value,
else => 0,
};
// Value param (e.g., $N: u32) — fold to a compile-time integer.
const val = self.resolveValueParamArg(args[i]) 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";
@@ -12071,10 +12063,8 @@ pub const Lowering = struct {
const tname = self.formatTypeName(ty);
name_parts.appendSlice(self.alloc, tname) catch {};
} else {
const val: i64 = switch (args[i].data) {
.int_literal => |lit| lit.value,
else => 0,
};
// Value param (e.g., $N: u32) — fold to a compile-time integer.
const val = self.resolveValueParamArg(args[i]) 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";