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

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