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:
29
examples/0207-generics-value-param-const.sx
Normal file
29
examples/0207-generics-value-param-const.sx
Normal 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]);
|
||||
}
|
||||
23
examples/0610-comptime-inline-for-const-bound.sx
Normal file
23
examples/0610-comptime-inline-for-const-bound.sx
Normal 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
|
||||
}
|
||||
35
examples/1501-vectors-const-lane.sx
Normal file
35
examples/1501-vectors-const-lane.sx
Normal 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);
|
||||
}
|
||||
16
examples/1502-vectors-runtime-lane-not-const.sx
Normal file
16
examples/1502-vectors-runtime-lane-not-const.sx
Normal 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);
|
||||
}
|
||||
1
examples/expected/0207-generics-value-param-const.exit
Normal file
1
examples/expected/0207-generics-value-param-const.exit
Normal file
@@ -0,0 +1 @@
|
||||
0
|
||||
1
examples/expected/0207-generics-value-param-const.stderr
Normal file
1
examples/expected/0207-generics-value-param-const.stderr
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
3
examples/expected/0207-generics-value-param-const.stdout
Normal file
3
examples/expected/0207-generics-value-param-const.stdout
Normal 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
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
sum 0..M = 3
|
||||
sum 0..(M+1) = 6
|
||||
1
examples/expected/1501-vectors-const-lane.exit
Normal file
1
examples/expected/1501-vectors-const-lane.exit
Normal file
@@ -0,0 +1 @@
|
||||
0
|
||||
1
examples/expected/1501-vectors-const-lane.stderr
Normal file
1
examples/expected/1501-vectors-const-lane.stderr
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
4
examples/expected/1501-vectors-const-lane.stdout
Normal file
4
examples/expected/1501-vectors-const-lane.stdout
Normal 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
|
||||
@@ -0,0 +1 @@
|
||||
1
|
||||
11
examples/expected/1502-vectors-runtime-lane-not-const.stderr
Normal file
11
examples/expected/1502-vectors-runtime-lane-not-const.stderr
Normal 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);
|
||||
| ^^^
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -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 1–4 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;
|
||||
|
||||
124
src/ir/lower.zig
124
src/ir/lower.zig
@@ -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";
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 => {},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 ───────────────────────────────────────────────────
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user