ir: dedicated TypeId.unresolved sentinel; kill inferred_type => .s64
An unannotated param resolving to a plausible .s64 was the classic silent-default trap (root of the 2.5 multi-param-closure bug). Replace it with a dedicated TypeId.unresolved at slot 0, so a zero-initialised or forgotten TypeId trips the sentinel instead of masquerading as a real type. - types.zig: TypeId.unresolved = 0 (void moves to 17); TypeInfo.unresolved; sizeOf/toLLVMType @panic on it (codegen tripwire); hash/eql/printer cover it. - type_bridge: inferred_type => .unresolved (was .s64). - resolveParamType: emit "parameter 'x' has no type annotation" for a genuinely-unannotated value param (comptime/variadic/pack params exempt -- they resolve via per-call substitution). - lowerLambda: resolve unannotated params from the target closure signature; otherwise emit "cannot infer type of lambda parameter". - CLAUDE.md: .void documented as an UNACCEPTABLE failed-type sentinel (it conflates with a real, heavily-checked type); prescribe a distinct .unresolved-style value + codegen tripwire. Snapshot churn: one .ir (ffi-objc-call-06) -- the runtime type-name table and typeof match arms renumber by the new builtin slot; program output unchanged.
This commit is contained in:
@@ -4289,6 +4289,9 @@ pub const LLVMEmitter = struct {
|
||||
// Comptime-only: a pack is expanded to flat positional args before
|
||||
// codegen, so it must never reach LLVM type emission.
|
||||
.pack => @panic("pack type has no LLVM representation (comptime-only)"),
|
||||
// Tripwire: a failed type resolution must have been diagnosed and
|
||||
// aborted long before LLVM emission.
|
||||
.unresolved => @panic("unresolved type reached LLVM emission — a type resolution failure was not diagnosed/aborted"),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -7101,13 +7101,22 @@ pub const Lowering = struct {
|
||||
// User params follow the ctx (optional) + env slots in `params`.
|
||||
const user_param_base: usize = (if (lambda_wants_ctx) @as(usize, 1) else 0) + 1;
|
||||
for (lam.params, 0..) |p, pi| {
|
||||
var pty = self.resolveParamType(&p);
|
||||
// Infer param type from target closure type if no annotation
|
||||
if (p.type_expr.data == .inferred_type and target_closure_params != null) {
|
||||
if (pi < target_closure_params.?.len) {
|
||||
pty = target_closure_params.?[pi];
|
||||
const pty: TypeId = blk: {
|
||||
// Unannotated lambda params take their type positionally from
|
||||
// the target `Closure(T0, …)` signature. Resolve them here so
|
||||
// `resolveParamType` (which would diagnose a missing annotation)
|
||||
// is only called for params that carry one.
|
||||
if (p.type_expr.data == .inferred_type) {
|
||||
if (target_closure_params != null and pi < target_closure_params.?.len) {
|
||||
break :blk target_closure_params.?[pi];
|
||||
}
|
||||
if (self.diagnostics) |d| {
|
||||
d.addFmt(.err, p.type_expr.span, "cannot infer type of lambda parameter '{s}'; annotate it or use the lambda where a closure type is expected", .{p.name});
|
||||
}
|
||||
break :blk .unresolved;
|
||||
}
|
||||
}
|
||||
break :blk self.resolveParamType(&p);
|
||||
};
|
||||
params.append(self.alloc, .{
|
||||
.name = self.module.types.internString(p.name),
|
||||
.ty = pty,
|
||||
@@ -10743,6 +10752,18 @@ pub const Lowering = struct {
|
||||
}
|
||||
|
||||
fn resolveParamType(self: *Lowering, p: *const ast.Param) TypeId {
|
||||
// A plain value param with no annotation can only be typed from
|
||||
// context (a lambda's target closure signature). When `resolveParamType`
|
||||
// is reached for one, there is no such context — so it's a genuine
|
||||
// "missing annotation" error, not an 8-byte-int guess. (Comptime/
|
||||
// variadic pack params also carry `inferred_type` but get their types
|
||||
// from per-call substitution, so they're exempt here.)
|
||||
if (p.type_expr.data == .inferred_type and !p.is_comptime and !p.is_variadic and !p.is_pack) {
|
||||
if (self.diagnostics) |d| {
|
||||
d.addFmt(.err, p.type_expr.span, "parameter '{s}' has no type annotation", .{p.name});
|
||||
}
|
||||
return .unresolved;
|
||||
}
|
||||
const declared_ty = self.resolveTypeWithBindings(p.type_expr);
|
||||
if (p.is_variadic) {
|
||||
// Two surface forms:
|
||||
|
||||
@@ -37,7 +37,14 @@ pub fn resolveAstType(node: ?*const Node, table: *TypeTable) TypeId {
|
||||
},
|
||||
.tuple_literal => |tl| resolveTupleLiteralAsType(&tl, table),
|
||||
.parameterized_type_expr => |pt| resolveParameterizedType(&pt, table),
|
||||
.inferred_type => .s64, // inferred — default until we have type inference
|
||||
// An unannotated param. Its type must be resolved from context
|
||||
// (contextual closure typing, generic binding, or pack substitution)
|
||||
// *before* reaching here; if it doesn't, returning a plausible `.s64`
|
||||
// silently fabricates an 8-byte int (the classic silent-default trap).
|
||||
// Return the dedicated `.unresolved` sentinel — never a legitimate
|
||||
// type — so the omission surfaces; the lowering-side `resolveParamType`
|
||||
// turns it into a real diagnostic.
|
||||
.inferred_type => .unresolved,
|
||||
// Inline type declarations (used as field types)
|
||||
.enum_decl => |ed| resolveInlineEnum(&ed, table),
|
||||
.struct_decl => |sd| resolveInlineStruct(&sd, table),
|
||||
|
||||
@@ -5,8 +5,16 @@ const Allocator = std.mem.Allocator;
|
||||
// Opaque handle into the TypeTable. First 16 slots are reserved for builtins.
|
||||
|
||||
pub const TypeId = enum(u32) {
|
||||
// Builtin slots 0–15
|
||||
void = 0,
|
||||
// Builtin slots 0–17.
|
||||
/// Resolution failed (e.g. an unannotated param whose type was never
|
||||
/// inferred from context). A dedicated sentinel — never a legitimate
|
||||
/// result — so downstream `== .void`/`== .s64` checks can't silently
|
||||
/// swallow it. Must never reach codegen; sizeOf/toLLVMType panic on it.
|
||||
///
|
||||
/// Deliberately slot 0: a zero-initialised or forgotten `TypeId` (the most
|
||||
/// common accidental value) thus reads as `.unresolved` and trips the
|
||||
/// tripwire, rather than silently masquerading as `.void`.
|
||||
unresolved = 0,
|
||||
bool = 1,
|
||||
s8 = 2,
|
||||
s16 = 3,
|
||||
@@ -23,9 +31,10 @@ pub const TypeId = enum(u32) {
|
||||
noreturn = 14,
|
||||
isize = 15,
|
||||
usize = 16,
|
||||
_, // user-defined types start at 17
|
||||
void = 17,
|
||||
_, // user-defined types start at 18
|
||||
|
||||
pub const first_user: u32 = 17;
|
||||
pub const first_user: u32 = 18;
|
||||
|
||||
pub fn index(self: TypeId) u32 {
|
||||
return @intFromEnum(self);
|
||||
@@ -73,6 +82,8 @@ pub const TypeInfo = union(enum) {
|
||||
noreturn,
|
||||
usize,
|
||||
isize,
|
||||
/// Resolution-failure sentinel (see `TypeId.unresolved`).
|
||||
unresolved,
|
||||
|
||||
pub const StructInfo = struct {
|
||||
name: StringId,
|
||||
@@ -280,9 +291,9 @@ pub const TypeTable = struct {
|
||||
.slice_arena = std.heap.ArenaAllocator.init(alloc),
|
||||
};
|
||||
|
||||
// Pre-populate builtin slots 0–15 (must match TypeId enum order)
|
||||
// Pre-populate builtin slots 0–17 (must match TypeId enum order)
|
||||
const builtins = [_]TypeInfo{
|
||||
.void, // 0
|
||||
.unresolved, // 0: resolution-failure sentinel
|
||||
.bool, // 1
|
||||
.{ .signed = 8 }, // 2: s8
|
||||
.{ .signed = 16 }, // 3: s16
|
||||
@@ -299,6 +310,7 @@ pub const TypeTable = struct {
|
||||
.noreturn, // 14
|
||||
.isize, // 15: isize (pointer-sized signed)
|
||||
.usize, // 16: usize (pointer-sized unsigned)
|
||||
.void, // 17
|
||||
};
|
||||
for (&builtins) |info| {
|
||||
table.infos.append(alloc, info) catch unreachable;
|
||||
@@ -477,6 +489,9 @@ pub const TypeTable = struct {
|
||||
// Comptime-only: a pack must be expanded to flat positional args
|
||||
// before codegen. Reaching runtime layout means a pack leaked.
|
||||
.pack => @panic("pack type has no runtime layout (comptime-only)"),
|
||||
// Tripwire: a failed type resolution must have surfaced a
|
||||
// diagnostic and aborted before any layout query.
|
||||
.unresolved => @panic("unresolved type reached sizeOf — a type resolution failure was not diagnosed/aborted"),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -674,6 +689,7 @@ pub const TypeTable = struct {
|
||||
.noreturn => "noreturn",
|
||||
.isize => "isize",
|
||||
.usize => "usize",
|
||||
.unresolved => "<unresolved>",
|
||||
else => {
|
||||
// User types — format from TypeInfo
|
||||
const info = self.get(id);
|
||||
@@ -813,7 +829,7 @@ fn hashTypeInfo(h: *std.hash.Wyhash, info: TypeInfo) void {
|
||||
switch (info) {
|
||||
.signed => |w| h.update(&.{w}),
|
||||
.unsigned => |w| h.update(&.{w}),
|
||||
.f32, .f64, .void, .bool, .string, .any, .noreturn, .usize, .isize => {},
|
||||
.f32, .f64, .void, .bool, .string, .any, .noreturn, .usize, .isize, .unresolved => {},
|
||||
.pointer => |p| h.update(std.mem.asBytes(&p.pointee)),
|
||||
.many_pointer => |p| h.update(std.mem.asBytes(&p.element)),
|
||||
.slice => |s| h.update(std.mem.asBytes(&s.element)),
|
||||
@@ -866,7 +882,7 @@ fn typeInfoEql(a: TypeInfo, b: TypeInfo) bool {
|
||||
return switch (a) {
|
||||
.signed => |w| w == b.signed,
|
||||
.unsigned => |w| w == b.unsigned,
|
||||
.f32, .f64, .void, .bool, .string, .any, .noreturn, .usize, .isize => true,
|
||||
.f32, .f64, .void, .bool, .string, .any, .noreturn, .usize, .isize, .unresolved => true,
|
||||
.pointer => |p| p.pointee == b.pointer.pointee,
|
||||
.many_pointer => |p| p.element == b.many_pointer.element,
|
||||
.slice => |s| s.element == b.slice.element,
|
||||
|
||||
Reference in New Issue
Block a user