fix(ir): value-param type functions + range-checked dim/lane fold (0083, 0087)
Two remaining siblings in F0.4's comptime-int path.
1. Type-returning function with a value param used as a TYPE annotation
(`b : Make(N, s64)` where `Make :: ($K: u32, $T: Type) -> Type`):
- `isValueParamPosition` (semantic_diagnostics) now also skips a value
param of a `fn_ast_map` type-returning function, so `N` is not walked
as the type name "N" ("unknown type 'N'").
- `resolveParameterizedWithBindings` routes a type-returning-function
name to `instantiateTypeFunction` (the `.call` path already did).
- `instantiateTypeFunction` resolves a general return-type expression
(`return [K]T`) with bindings active — not just struct/union returns.
`Make(N, s64)`, `Make(M + 1, s64)`, `Make(3, s64)` all resolve to one
`[3]s64`.
2. Oversized dim/lane fold panicked the compiler (0087): an array dim /
Vector lane folded to a valid i64 (5e9) then narrowed to u32 with an
unchecked `@intCast`. New single gate `program_index.foldDimU32` folds
via `evalConstIntExpr` then range-checks `[min, maxInt(u32)]`; the three
narrowing sites (resolveArrayLen stateful + stateless, resolveVectorLane)
all route through it and emit a clean diagnostic + halt instead of
panicking. Value-param args stay i64 until used as a dim/lane, where the
same gate checks them.
Regressions: examples/0208 (value-param type function), examples/1130
(oversized array dim clean halt), examples/1503 (oversized Vector lane
clean halt). Marks issue 0087 RESOLVED.
Gate: zig build, zig build test, bash tests/run_examples.sh — 398 passed,
0 failed, 0 timed out.
This commit is contained in:
126
src/ir/lower.zig
126
src/ir/lower.zig
@@ -11639,37 +11639,36 @@ pub const Lowering = struct {
|
||||
|
||||
/// Fixed-array dimension hook for `TypeResolver.resolveCompound`. A literal
|
||||
/// `[16]T` and a named-const `N :: 16; [N]T` must resolve to the SAME length:
|
||||
/// the dimension is a compile-time integer, looked up in the comptime / value
|
||||
/// / module-const tables the stateful lowering owns. A dimension that isn't a
|
||||
/// compile-time integer is a hard error: emit a diagnostic so the driver
|
||||
/// aborts (`hasErrors()`), then return a harmless `0` so body lowering
|
||||
/// finishes without touching the `.unresolved` sentinel (which would `@panic`
|
||||
/// in `sizeOf` mid-lowering, before the diagnostic surfaces). The diagnostic —
|
||||
/// not the returned length — is what guarantees no garbage ships (issue 0083).
|
||||
/// the dimension folds to a compile-time integer (looked up in the comptime /
|
||||
/// value / module-const tables the stateful lowering owns) and is narrowed to
|
||||
/// `u32` through the single range-checked `program_index.foldDimU32` — never a
|
||||
/// bare `@intCast`, so an oversized-but-valid `i64` dim (`[5_000_000_000]`)
|
||||
/// diagnoses instead of panicking the compiler (issue 0087). A dimension that
|
||||
/// isn't a compile-time integer (or doesn't fit a `u32`) is a hard error:
|
||||
/// emit a diagnostic so the driver aborts (`hasErrors()`), then return a
|
||||
/// harmless `0` so body lowering finishes without touching the `.unresolved`
|
||||
/// sentinel (which would `@panic` in `sizeOf` mid-lowering, before the
|
||||
/// diagnostic surfaces). The diagnostic — not the returned length — is what
|
||||
/// guarantees no garbage ships (issue 0083).
|
||||
pub fn resolveArrayLen(self: *Lowering, len_node: *const Node) ?u32 {
|
||||
if (self.comptimeArrayDim(len_node)) |n| {
|
||||
if (n < 0) {
|
||||
switch (program_index_mod.foldDimU32(len_node, self, 0)) {
|
||||
.ok => |n| return n,
|
||||
.below_min => |v| {
|
||||
if (self.diagnostics) |d|
|
||||
d.addFmt(.err, len_node.span, "array dimension must be non-negative, got {}", .{n});
|
||||
d.addFmt(.err, len_node.span, "array dimension must be non-negative, got {}", .{v});
|
||||
return 0;
|
||||
}
|
||||
return @intCast(n);
|
||||
},
|
||||
.too_large => |v| {
|
||||
if (self.diagnostics) |d|
|
||||
d.addFmt(.err, len_node.span, "array dimension {} does not fit in u32", .{v});
|
||||
return 0;
|
||||
},
|
||||
.not_const => {
|
||||
if (self.diagnostics) |d|
|
||||
d.addFmt(.err, len_node.span, "array dimension must be a compile-time integer constant", .{});
|
||||
return 0;
|
||||
},
|
||||
}
|
||||
if (self.diagnostics) |d|
|
||||
d.addFmt(.err, len_node.span, "array dimension must be a compile-time integer constant", .{});
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// Evaluate a fixed-array dimension to a compile-time integer: a literal, a
|
||||
/// name bound to an integer (comptime-constant `OS`/loop cursors, generic
|
||||
/// `$N` value, or module-global const `N :: 16`), or a constant-foldable
|
||||
/// expression over those (`[M + 1]`, `[(M + 1) * 2]`). Delegates the
|
||||
/// expression folding to the shared `program_index.evalConstIntExpr` so this
|
||||
/// body-lowering path and the stateless registration path cannot diverge on
|
||||
/// a dimension's value. Returns null when the dimension isn't a compile-time
|
||||
/// integer.
|
||||
fn comptimeArrayDim(self: *Lowering, node: *const Node) ?i64 {
|
||||
return program_index_mod.evalConstIntExpr(node, self);
|
||||
}
|
||||
|
||||
/// Leaf-name lookup for the shared dimension evaluator: a name bound to a
|
||||
@@ -11820,20 +11819,28 @@ pub const Lowering = struct {
|
||||
}
|
||||
|
||||
/// 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.
|
||||
/// through the shared `program_index.foldDimU32` folder (min 1) — so a literal
|
||||
/// (`Vector(4, f32)`), a module/generic const (`Vector(N, f32)`), and a const
|
||||
/// expression (`Vector(M + 1, f32)`) all resolve identically, and the i64→u32
|
||||
/// narrowing is range-checked (an oversized lane diagnoses instead of
|
||||
/// panicking — issue 0087). 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;
|
||||
switch (program_index_mod.foldDimU32(lane_node, self, 1)) {
|
||||
.ok => |n| return n,
|
||||
.too_large => |v| {
|
||||
if (self.diagnostics) |d|
|
||||
d.addFmt(.err, lane_node.span, "Vector lane count {} does not fit in u32", .{v});
|
||||
return null;
|
||||
},
|
||||
.not_const, .below_min => {
|
||||
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
|
||||
@@ -11909,6 +11916,20 @@ pub const Lowering = struct {
|
||||
}
|
||||
}
|
||||
|
||||
// User-defined type-returning function used as a TYPE annotation
|
||||
// (`b : Make(N, s64)` where `Make :: ($K: u32, $T: Type) -> Type`). The
|
||||
// `.call`-node path (`resolveTypeCallWithBindings`) already routes here;
|
||||
// a `parameterized_type_expr` must too, or the function name falls through
|
||||
// to the empty-struct stub below and `b.field` / `b.len` fails.
|
||||
const resolved_name = if (self.scope) |scope| (scope.lookupFn(base_name) orelse base_name) else base_name;
|
||||
if (self.program_index.fn_ast_map.get(resolved_name)) |fd| {
|
||||
if (fd.type_params.len > 0) {
|
||||
if (self.instantiateTypeFunction(base_name, base_name, fd, pt.args)) |ty| {
|
||||
return ty;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: register as named type placeholder
|
||||
const name_id = table.internString(pt.name);
|
||||
return table.intern(.{ .@"struct" = .{ .name = name_id, .fields = &.{} } });
|
||||
@@ -12141,9 +12162,36 @@ pub const Lowering = struct {
|
||||
return self.instantiateTypeUnion(if (has_alias) alias_name else mangled_name, mangled_name, &enum_decl);
|
||||
}
|
||||
|
||||
// General case: the body returns a TYPE EXPRESSION that is not an inline
|
||||
// struct/union/enum — `return [K]T`, `Vector(K, T)`, `*T`, an alias, etc.
|
||||
// Resolve it with the value/type bindings active (so `[K]T` folds K to a
|
||||
// compile-time integer). The result is interned structurally, so
|
||||
// `Make(N, s64)`, `Make(3, s64)`, and `Make(M + 1, s64)` all yield the
|
||||
// same TypeId. `.unresolved` means the return wasn't a type expression
|
||||
// (e.g. a value-returning function in a type position) → fall through to
|
||||
// the caller's fallback rather than fabricating a type.
|
||||
if (findReturnTypeExpr(fd.body)) |ret_node| {
|
||||
const ty = self.resolveTypeWithBindings(ret_node);
|
||||
if (ty != .unresolved) return ty;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// The type expression a type-returning function yields: the value of its
|
||||
/// `return` (block body) or the bare expression (arrow body / `=> [K]T`).
|
||||
/// Used for a non-struct/union return shape, which the struct/union body
|
||||
/// walkers above don't match.
|
||||
fn findReturnTypeExpr(body: *const Node) ?*const Node {
|
||||
if (body.data == .block) {
|
||||
for (body.data.block.stmts) |stmt| {
|
||||
if (stmt.data == .return_stmt) return stmt.data.return_stmt.value;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return body;
|
||||
}
|
||||
|
||||
/// Instantiate a tagged enum from a type function body.
|
||||
fn instantiateTypeUnion(self: *Lowering, alias_name: []const u8, mangled_name: []const u8, ed: *const ast.EnumDecl) ?TypeId {
|
||||
const table = &self.module.types;
|
||||
|
||||
Reference in New Issue
Block a user