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:
agra
2026-05-29 22:25:45 +03:00
parent 5fd513466f
commit 55e62694d1
6 changed files with 575 additions and 509 deletions

View File

@@ -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"),
};
}

View File

@@ -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:

View File

@@ -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),

View File

@@ -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 015
void = 0,
// Builtin slots 017.
/// 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 015 (must match TypeId enum order)
// Pre-populate builtin slots 017 (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,