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

@@ -0,0 +1,29 @@
// A generic value parameter (`$K: u32`) bound from a named const or a
// constant-foldable expression resolves to the SAME monomorphised instantiation
// as the literal form: `Vec(N, f32)` (N a module const) and `Vec(M + 1, f32)`
// (a const expression) are both `Vec(3, f32)`. The struct-copy assignment is the
// proof — it type-checks only because the two spellings name one instantiation.
//
// Regression (issue 0083): the value-param binder hand-rolled an `else => 0`
// switch, so a named-const value arg either fabricated a 0 binding under a wrong
// mangled name or was rejected outright as "unknown type 'N'". It now folds
// through the shared const-int evaluator (`program_index.evalConstIntExpr`).
#import "modules/std.sx";
N :: 3;
M :: 2;
Vec :: struct ($K: u32, $T: Type) { data: [K]T; }
main :: () {
a : Vec(N, f32) = ---; // named-const value param
a.data[0] = 10.0; a.data[1] = 20.0; a.data[2] = 30.0;
print("named: len={} a0={} a2={}\n", a.data.len, a.data[0], a.data[2]);
e : Vec(M + 1, f32) = ---; // const-expr value param (M + 1 == 3)
e.data[0] = 1.0; e.data[2] = 9.0;
print("expr: len={} e2={}\n", e.data.len, e.data[2]);
b : Vec(3, f32) = a; // same instantiation → struct copy type-checks
print("copy: len={} b2={}\n", b.data.len, b.data[2]);
}

View File

@@ -0,0 +1,23 @@
// `inline for 0..K` with a named-const or constant-foldable bound unrolls at
// compile time, just like a literal bound.
//
// Regression (issue 0083): the inline-for bound folder (`evalComptimeInt`) only
// handled literals, comptime cursors, and `<pack>.len`, so `inline for 0..M`
// (M a module const) and `inline for 0..(M + 1)` (a const expression) both
// failed with "range end is not a compile-time integer". `evalComptimeInt` now
// delegates to the single shared const-int evaluator
// (`program_index.evalConstIntExpr`), so the inline-for bound and an array
// dimension fold the same shapes to the same value.
#import "modules/std.sx";
M :: 3;
main :: () {
s := 0;
inline for 0..M: (i) { s += i; }
print("sum 0..M = {}\n", s); // 0 + 1 + 2 = 3
t := 0;
inline for 0..(M + 1): (i) { t += i; }
print("sum 0..(M+1) = {}\n", t); // 0 + 1 + 2 + 3 = 6
}

View File

@@ -0,0 +1,35 @@
// A `Vector` lane count from a named const or a constant-foldable expression
// resolves to the SAME layout as a literal lane — DIRECT (param / return type)
// and via a type ALIAS. A 3-lane (named const `N`) and a 4-lane (const expr
// `M + 1`) prove the lane VALUE is folded, not fabricated: reading `.w` requires
// the 4-lane vector to actually have four lanes.
//
// Regression (issue 0083): the stateful Vector lane resolvers hand-rolled an
// `else => 0` switch, so a module-const lane (`Vector(N, f32)`) lowered a 0-lane
// `<0 x float>` and died in LLVM verification ("huge alignment values are
// unsupported"); a const-expr lane (`Vector(M + 1, f32)`) was rejected at parse.
// Both now fold through the single shared const-int evaluator
// (`program_index.evalConstIntExpr`) — the same one the array-dimension path
// uses — so a named-const / const-expr lane is identical to a literal lane.
#import "modules/std.sx";
N :: 3;
M :: 3;
LaneAlias :: Vector(N, f32); // ALIAS: 3-lane via named const.
ExprAlias :: Vector(M + 1, f32); // ALIAS: 4-lane via const expression.
mk3 :: () -> Vector(N, f32) { .[1.0, 2.0, 3.0] } // DIRECT named-const lane.
mk4 :: () -> Vector(M + 1, f32) { .[1.0, 2.0, 3.0, 4.0] } // DIRECT const-expr lane.
main :: () {
a := mk3();
print("direct3: {} {} {}\n", a.x, a.y, a.z);
b := mk4();
print("direct4: {} {} {} {}\n", b.x, b.y, b.z, b.w);
c : LaneAlias = .[5.0, 6.0, 7.0];
print("alias3: {}\n", c.z);
d : ExprAlias = .[5.0, 6.0, 7.0, 8.0];
print("alias4: {}\n", d.w);
}

View File

@@ -0,0 +1,16 @@
// A `Vector` lane count that is not a compile-time integer (here a runtime
// function call) is a hard error — a clean sx diagnostic with a non-zero exit,
// NOT a fabricated `<0 x float>` lane that crashes LLVM verification.
//
// Regression (issue 0083): the Vector lane resolver hand-rolled an `else => 0`
// switch that silently fabricated a 0-lane vector for a non-const lane. It now
// folds the lane through the shared const-int evaluator and, when that yields no
// compile-time integer, emits this diagnostic and halts the build.
#import "modules/std.sx";
lanes :: () -> u32 { return 3; }
main :: () {
v : Vector(lanes(), f32) = ---;
print("unreachable: {}\n", v.x);
}

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1,3 @@
named: len=3 a0=10.000000 a2=30.000000
expr: len=3 e2=9.000000
copy: len=3 b2=30.000000

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1,2 @@
sum 0..M = 3
sum 0..(M+1) = 6

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1,4 @@
direct3: 1.000000 2.000000 3.000000
direct4: 1.000000 2.000000 3.000000 4.000000
alias3: 7.000000
alias4: 8.000000

View File

@@ -0,0 +1 @@
1

View File

@@ -0,0 +1,11 @@
error: Vector lane count must be a positive compile-time integer constant
--> examples/1502-vectors-runtime-lane-not-const.sx:14:16
|
14 | v : Vector(lanes(), f32) = ---;
| ^^^^^^^
error: field 'x' not found on type 'unresolved'
--> examples/1502-vectors-runtime-lane-not-const.sx:15:32
|
15 | print("unreachable: {}\n", v.x);
| ^^^

View File

@@ -0,0 +1 @@

View File

@@ -72,6 +72,37 @@
> alias, scalar / string / struct element types); `1129` re-pointed at a genuinely
> non-const dimension (`[get()]s64`, a runtime call) so it still proves the
> stateless clean-halt.
>
> **Unified comptime-int evaluator (attempt 5).** Attempts 14 fixed the array
> *dimension* paths but the SAME length-0 fabrication class survived on the
> siblings that resolve a comptime integer elsewhere: the three Vector lane
> resolvers (`resolveTypeCallWithBindings`, `resolveParameterizedWithBindings`,
> `resolveArrayLiteralType`) and the two generic value-param binders
> (`instantiateGenericStruct`, `instantiateTypeFunction`) each hand-rolled an
> `else => 0` switch, so `Vector(N, f32)` / `Vec(N, f32)` (N a module const)
> fabricated a 0-lane `<0 x float>` (LLVM "huge alignment" abort) or a 0 binding
> under a wrong mangled name; and the `inline for` bound folder (`evalComptimeInt`)
> only knew literals / comptime cursors / `<pack>.len`, so `inline for 0..M` failed
> outright. Fix: every one of those sites now routes through the single shared
> `program_index.evalConstIntExpr` — `evalComptimeInt` delegates to it (the pack
> `.len` leaf moved into the shared folder via a new `ctx.lookupPackLen`); the
> Vector lane and value-param resolvers fold through it and emit a clean diagnostic
> + `.unresolved` (never `else => 0`) on a non-const operand. Two enabling fixes
> upstream of resolution: the unknown-type semantic checker no longer walks a
> value-param position (`Vector(N, …)` / `Vec(N, …)`) as a type name (it was
> reporting "unknown type 'N'"); and both the parameterized-type-arg parser and
> the function-body-detection 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 attempt-4 sibling miss).
> Files: `src/ir/program_index.zig` (+`.test.zig`), `src/ir/lower.zig`,
> `src/ir/type_bridge.zig`, `src/ir/semantic_diagnostics.zig`, `src/parser.zig`.
> Regressions: `examples/1501-vectors-const-lane.sx` (named-const + const-expr
> lane, direct + alias, 3- and 4-lane reads), `examples/1502-vectors-runtime-lane-
> not-const.sx` (a runtime lane clean-halts, exit 1, no LLVM crash),
> `examples/0207-generics-value-param-const.sx` (`Vec(N,f32)` / `Vec(M+1,f32)`
> resolve to the same instantiation as `Vec(3,f32)`),
> `examples/0610-comptime-inline-for-const-bound.sx` (`inline for 0..M` and
> `0..(M+1)` unroll).
## Symptom
A fixed array whose dimension is a module-global integer constant (`N :: 16;

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";

View File

@@ -106,6 +106,11 @@ const DimCtx = struct {
if (std.mem.eql(u8, name, "N")) return 6;
return null;
}
// `xs` stands in for a pack of arity 3; every other name has no pack length.
pub fn lookupPackLen(_: DimCtx, name: []const u8) ?i64 {
if (std.mem.eql(u8, name, "xs")) return 3;
return null;
}
};
fn nLit(v: i64) ast.Node {
@@ -120,6 +125,9 @@ fn nBin(op: ast.BinaryOp.Op, l: *ast.Node, r: *ast.Node) ast.Node {
fn nNeg(operand: *ast.Node) ast.Node {
return .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .unary_op = .{ .op = .negate, .operand = operand } } };
}
fn nField(obj: *ast.Node, field: []const u8) ast.Node {
return .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .field_access = .{ .object = obj, .field = field } } };
}
test "evalConstIntExpr folds constant-expression array dimensions, halts on non-const" {
const eval = pi.evalConstIntExpr;
@@ -157,6 +165,19 @@ test "evalConstIntExpr folds constant-expression array dimensions, halts on non-
var neg = nNeg(&m);
try std.testing.expectEqual(@as(?i64, -4), eval(&neg, ctx));
// `<pack>.len` leaf resolves via `ctx.lookupPackLen` and folds in an
// expression (`xs.len` → 3, `xs.len - 1` → 2). A `.len` on a non-pack name
// and a non-`len` field are not compile-time integer leaves → null.
var xs = nIdent("xs");
var xslen = nField(&xs, "len");
var xslen_m1 = nBin(.sub, &xslen, &one);
try std.testing.expectEqual(@as(?i64, 3), eval(&xslen, ctx));
try std.testing.expectEqual(@as(?i64, 2), eval(&xslen_m1, ctx));
var zlen = nField(&z, "len");
var xscap = nField(&xs, "cap");
try std.testing.expect(eval(&zlen, ctx) == null);
try std.testing.expect(eval(&xscap, ctx) == null);
// Genuinely non-const operand, division by zero, a non-arithmetic operator,
// and overflow all yield null → the caller's clean compile-halt (no panic,
// no fabricated length).

View File

@@ -55,25 +55,44 @@ pub fn moduleConstInt(consts: *const std.StringHashMap(ModuleConstInfo), name: [
return null;
}
/// Evaluate a constant-expression array dimension to its integer value. 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). Leaf names resolve through
/// `ctx.lookupDimName`, so the stateful body-lowering path (which also sees
/// comptime constants and generic `$N` value bindings) and the stateless
/// registration path (module consts only) share THIS expression-folding logic
/// and cannot disagree on a dimension's value — the same unify-or-die rule that
/// keeps an array laid out via a type alias identical to the direct form
/// (issue 0083). Returns null when any operand is not a compile-time integer (a
/// runtime value, a non-comptime call, an unbound name) or the arithmetic
/// overflows / divides by zero: the caller then emits the clean compile-halting
/// diagnostic, never a fabricated length.
/// 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
/// 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).
///
/// 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
/// stateful body-lowering ctx sees comptime constants, generic `$N` value
/// bindings, and module consts; the stateless registration ctx sees module
/// consts only.
/// - `ctx.lookupPackLen(name)` — a `<pack>.len` leaf → the pack's
/// monomorphised arity. Only the body-lowering ctx knows pack arities; the
/// stateless ctx returns null.
///
/// Returns null when any operand is not a compile-time integer (a runtime value,
/// a non-comptime call, an unbound name) or the arithmetic overflows / divides
/// by zero: the caller then emits the clean compile-halting diagnostic, never a
/// fabricated length / lane count / value-param.
pub fn evalConstIntExpr(node: *const Node, ctx: anytype) ?i64 {
return switch (node.data) {
.int_literal => |lit| lit.value,
.identifier => |id| ctx.lookupDimName(id.name),
.type_expr => |te| ctx.lookupDimName(te.name),
.field_access => |fa| blk: {
// `<pack>.len` resolves to the monomorphised arity (e.g. an
// `inline for 0..xs.len` bound). Any other field access is not a
// compile-time integer leaf.
if (fa.object.data == .identifier and std.mem.eql(u8, fa.field, "len")) {
break :blk ctx.lookupPackLen(fa.object.data.identifier.name);
}
break :blk null;
},
.unary_op => |u| switch (u.op) {
.negate => {
const v = evalConstIntExpr(u.operand, ctx) orelse return null;

View File

@@ -612,6 +612,20 @@ pub const UnknownTypeChecker = struct {
}
}
/// True when arg `i` of a parameterized type `base(...)` is a VALUE
/// parameter (a compile-time integer such as a `Vector` lane count or a
/// generic `$N: u32` arg), not a type. Such a position must be skipped by
/// the unknown-type walk: a module-const arg (`Vector(N, f32)`) is a value,
/// not a type name. `Vector`'s arg 0 is always its lane count; a generic
/// struct template's value-param positions come from its declared params.
fn isValueParamPosition(self: UnknownTypeChecker, base: []const u8, i: usize) bool {
if (std.mem.eql(u8, base, "Vector")) return i == 0;
if (self.index.struct_template_map.get(base)) |tmpl| {
if (i < tmpl.type_params.len) return !tmpl.type_params[i].is_type_param;
}
return false;
}
/// Recurse a type-annotation node to its leaf names, reporting any unknown.
fn checkTypeNodeForUnknown(
self: UnknownTypeChecker,
@@ -643,8 +657,20 @@ pub const UnknownTypeChecker = struct {
if (ct.return_type) |rt| self.checkTypeNodeForUnknown(rt, declared, in_scope, type_vals);
},
// Builtin constructors (Vector) and generic templates resolve the
// base name specially; just check the type args.
.parameterized_type_expr => |pt| for (pt.args) |a| self.checkTypeNodeForUnknown(a, declared, in_scope, type_vals),
// base name specially; check only the TYPE args. A value-param
// position (a `Vector` lane count, or a generic `$N: u32` arg) holds
// a compile-time integer — `Vector(N, f32)` / `Vec(N, f32)` with `N`
// a module const — not a type name, so it must not be walked as one
// (it would falsely report "unknown type 'N'"). The lowering
// resolvers fold the value and emit the precise diagnostic if it
// isn't a compile-time integer.
.parameterized_type_expr => |pt| {
const base = if (std.mem.lastIndexOfScalar(u8, pt.name, '.')) |dot| pt.name[dot + 1 ..] else pt.name;
for (pt.args, 0..) |a, i| {
if (self.isValueParamPosition(base, i)) continue;
self.checkTypeNodeForUnknown(a, declared, in_scope, type_vals);
}
},
else => {},
}
}

View File

@@ -68,6 +68,13 @@ const StatelessInner = struct {
const consts = self.consts orelse return null;
return program_index_mod.moduleConstInt(consts, name);
}
/// Pack-length leaf for the shared integer-expression evaluator. The
/// registration-time path has no pack-arity information (packs are bound
/// during body lowering), so a `<pack>.len` dimension is never a
/// compile-time integer here → null → the clean unresolved-dim diagnostic.
pub fn lookupPackLen(_: StatelessInner, _: []const u8) ?i64 {
return null;
}
};
// ── AST Node → TypeId ───────────────────────────────────────────────────

View File

@@ -723,7 +723,25 @@ pub const Parser = struct {
if (args.items.len > 0) {
try self.expect(.comma);
}
// Args can be int literals (for lengths) or type expressions
// Pack-spread type arg: `Combined($R, ..sources.T)`.
if (self.current.tag == .dot_dot) {
const sp_start = self.current.loc.start;
self.advance(); // skip '..'
const operand = try self.parseTypeExpr();
try args.append(self.allocator, try self.createNode(sp_start, .{ .spread_expr = .{ .operand = operand } }));
continue;
}
// An arg is either a TYPE (`f32`, `*T`, `[]u8`, `List(T)`) or a
// compile-time integer expression in a value position — a
// `Vector` lane count or a generic `$N: u32` arg: `Vector(N, f32)`,
// `Vector(M + 1, f32)`. Parse the primary as a literal / type,
// then continue as a const-int expression iff an arithmetic
// operator follows. A complete type arg is always followed by
// `,` / `)`, so `parseBinaryRhs` is a no-op for plain types and
// the continuation is unambiguous; `Prec.additive` bounds it to
// `+ - * / %`. The shared evaluator folds the expression; a
// non-const value position is diagnosed during lowering.
var arg: *Node = undefined;
if (self.current.tag == .int_literal) {
const arg_start = self.current.loc.start;
const text = self.tokenSlice(self.current);
@@ -738,16 +756,12 @@ pub const Parser = struct {
return self.fail("invalid integer literal in type argument");
};
self.advance();
try args.append(self.allocator, try self.createNode(arg_start, .{ .int_literal = .{ .value = value } }));
} else if (self.current.tag == .dot_dot) {
// Pack-spread type arg: `Combined($R, ..sources.T)`.
const sp_start = self.current.loc.start;
self.advance(); // skip '..'
const operand = try self.parseTypeExpr();
try args.append(self.allocator, try self.createNode(sp_start, .{ .spread_expr = .{ .operand = operand } }));
arg = try self.createNode(arg_start, .{ .int_literal = .{ .value = value } });
} else {
try args.append(self.allocator, try self.parseTypeExpr());
arg = try self.parseTypeExpr();
}
arg = try self.parseBinaryRhs(arg, Prec.additive);
try args.append(self.allocator, arg);
}
try self.expect(.r_paren);
return try self.createNode(start, .{ .parameterized_type_expr = .{
@@ -3501,7 +3515,15 @@ pub const Parser = struct {
self.current.tag == .l_bracket or self.current.tag == .r_bracket or
self.current.tag == .l_paren or self.current.tag == .r_paren or
self.current.tag == .comma or self.current.tag == .int_literal or
self.current.tag == .star or self.current.tag == .question or
// Arithmetic operators appear in a const-expression dimension /
// lane / value-param in a return type: `-> [N + 1]f32`,
// `-> Vector(N + 1, f32)`. They must be skipped while scanning
// for the body brace, else the decl is misread as a bodyless
// function-type alias and the `{` body errors as "expected ';'".
// (`.star` doubles as the pointer sigil and is already listed.)
self.current.tag == .star or self.current.tag == .slash or
self.current.tag == .percent or self.current.tag == .plus or
self.current.tag == .minus or self.current.tag == .question or
self.current.tag == .bang or
self.current.tag == .colon or self.current.tag == .arrow)
{