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:
26
CLAUDE.md
26
CLAUDE.md
@@ -93,10 +93,28 @@ everything else.
|
|||||||
|
|
||||||
✅ **Required:** when a lookup that *must* succeed fails, emit a
|
✅ **Required:** when a lookup that *must* succeed fails, emit a
|
||||||
diagnostic via `self.diagnostics.addFmt(.err, span, "...", .{...})`
|
diagnostic via `self.diagnostics.addFmt(.err, span, "...", .{...})`
|
||||||
and return the most clearly-broken sentinel the calling code can
|
and return a **dedicated, unmistakable sentinel** — one that can never
|
||||||
survive (e.g. `.void`, a `Ref.none`, or via a `?T` return that forces
|
be confused with a legitimate result — or a `?T` return that forces the
|
||||||
the caller to handle the null). Errors must surface to the user as
|
caller to handle the null. Errors must surface to the user as text, not
|
||||||
text, not as a silently-corrupted size or alignment.
|
as a silently-corrupted size or alignment.
|
||||||
|
|
||||||
|
❌ **`.void` is an UNACCEPTABLE sentinel for a failed *type* lookup.**
|
||||||
|
`void` is a real, heavily-checked type (void returns, void params, "no
|
||||||
|
value" markers), and pervasive `if (ty == .void) { skip / return-nothing }`
|
||||||
|
checks would silently swallow the failure — trading one silent default
|
||||||
|
(`.s64`) for another (`.void`) one layer down. The same objection rules
|
||||||
|
out `noreturn` (diverging expressions) and any other load-bearing builtin.
|
||||||
|
Instead, add a **distinct** `.unresolved`-style `TypeId` whose sole meaning
|
||||||
|
is "resolution failed". A dedicated value (1) can't be mistaken for a real
|
||||||
|
type by any downstream check, (2) makes the exhaustive `switch`es in the
|
||||||
|
type table fail to compile until every site handles it (forcing coverage),
|
||||||
|
and (3) can be a hard tripwire — `if (ty == .unresolved) @panic(...)` in
|
||||||
|
codegen/emit guarantees it never silently ships. The one-time plumbing
|
||||||
|
cost is exactly the trade this file mandates ("the field plumbing is a
|
||||||
|
one-time cost; silent-clobber debugging is forever"). The same principle
|
||||||
|
applies to non-type sentinels: prefer a `Ref.none`-style value that is
|
||||||
|
distinct from every valid result, not a real one that "looks broken
|
||||||
|
enough".
|
||||||
|
|
||||||
If you find an existing default-return in the compiler that swallows
|
If you find an existing default-return in the compiler that swallows
|
||||||
a lookup failure, treat it as a discovered bug — file an issue per
|
a lookup failure, treat it as a discovered bug — file an issue per
|
||||||
|
|||||||
@@ -4289,6 +4289,9 @@ pub const LLVMEmitter = struct {
|
|||||||
// Comptime-only: a pack is expanded to flat positional args before
|
// Comptime-only: a pack is expanded to flat positional args before
|
||||||
// codegen, so it must never reach LLVM type emission.
|
// codegen, so it must never reach LLVM type emission.
|
||||||
.pack => @panic("pack type has no LLVM representation (comptime-only)"),
|
.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`.
|
// 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;
|
const user_param_base: usize = (if (lambda_wants_ctx) @as(usize, 1) else 0) + 1;
|
||||||
for (lam.params, 0..) |p, pi| {
|
for (lam.params, 0..) |p, pi| {
|
||||||
var pty = self.resolveParamType(&p);
|
const pty: TypeId = blk: {
|
||||||
// Infer param type from target closure type if no annotation
|
// Unannotated lambda params take their type positionally from
|
||||||
if (p.type_expr.data == .inferred_type and target_closure_params != null) {
|
// the target `Closure(T0, …)` signature. Resolve them here so
|
||||||
if (pi < target_closure_params.?.len) {
|
// `resolveParamType` (which would diagnose a missing annotation)
|
||||||
pty = target_closure_params.?[pi];
|
// 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, .{
|
params.append(self.alloc, .{
|
||||||
.name = self.module.types.internString(p.name),
|
.name = self.module.types.internString(p.name),
|
||||||
.ty = pty,
|
.ty = pty,
|
||||||
@@ -10743,6 +10752,18 @@ pub const Lowering = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn resolveParamType(self: *Lowering, p: *const ast.Param) TypeId {
|
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);
|
const declared_ty = self.resolveTypeWithBindings(p.type_expr);
|
||||||
if (p.is_variadic) {
|
if (p.is_variadic) {
|
||||||
// Two surface forms:
|
// Two surface forms:
|
||||||
|
|||||||
@@ -37,7 +37,14 @@ pub fn resolveAstType(node: ?*const Node, table: *TypeTable) TypeId {
|
|||||||
},
|
},
|
||||||
.tuple_literal => |tl| resolveTupleLiteralAsType(&tl, table),
|
.tuple_literal => |tl| resolveTupleLiteralAsType(&tl, table),
|
||||||
.parameterized_type_expr => |pt| resolveParameterizedType(&pt, 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)
|
// Inline type declarations (used as field types)
|
||||||
.enum_decl => |ed| resolveInlineEnum(&ed, table),
|
.enum_decl => |ed| resolveInlineEnum(&ed, table),
|
||||||
.struct_decl => |sd| resolveInlineStruct(&sd, 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.
|
// Opaque handle into the TypeTable. First 16 slots are reserved for builtins.
|
||||||
|
|
||||||
pub const TypeId = enum(u32) {
|
pub const TypeId = enum(u32) {
|
||||||
// Builtin slots 0–15
|
// Builtin slots 0–17.
|
||||||
void = 0,
|
/// 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,
|
bool = 1,
|
||||||
s8 = 2,
|
s8 = 2,
|
||||||
s16 = 3,
|
s16 = 3,
|
||||||
@@ -23,9 +31,10 @@ pub const TypeId = enum(u32) {
|
|||||||
noreturn = 14,
|
noreturn = 14,
|
||||||
isize = 15,
|
isize = 15,
|
||||||
usize = 16,
|
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 {
|
pub fn index(self: TypeId) u32 {
|
||||||
return @intFromEnum(self);
|
return @intFromEnum(self);
|
||||||
@@ -73,6 +82,8 @@ pub const TypeInfo = union(enum) {
|
|||||||
noreturn,
|
noreturn,
|
||||||
usize,
|
usize,
|
||||||
isize,
|
isize,
|
||||||
|
/// Resolution-failure sentinel (see `TypeId.unresolved`).
|
||||||
|
unresolved,
|
||||||
|
|
||||||
pub const StructInfo = struct {
|
pub const StructInfo = struct {
|
||||||
name: StringId,
|
name: StringId,
|
||||||
@@ -280,9 +291,9 @@ pub const TypeTable = struct {
|
|||||||
.slice_arena = std.heap.ArenaAllocator.init(alloc),
|
.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{
|
const builtins = [_]TypeInfo{
|
||||||
.void, // 0
|
.unresolved, // 0: resolution-failure sentinel
|
||||||
.bool, // 1
|
.bool, // 1
|
||||||
.{ .signed = 8 }, // 2: s8
|
.{ .signed = 8 }, // 2: s8
|
||||||
.{ .signed = 16 }, // 3: s16
|
.{ .signed = 16 }, // 3: s16
|
||||||
@@ -299,6 +310,7 @@ pub const TypeTable = struct {
|
|||||||
.noreturn, // 14
|
.noreturn, // 14
|
||||||
.isize, // 15: isize (pointer-sized signed)
|
.isize, // 15: isize (pointer-sized signed)
|
||||||
.usize, // 16: usize (pointer-sized unsigned)
|
.usize, // 16: usize (pointer-sized unsigned)
|
||||||
|
.void, // 17
|
||||||
};
|
};
|
||||||
for (&builtins) |info| {
|
for (&builtins) |info| {
|
||||||
table.infos.append(alloc, info) catch unreachable;
|
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
|
// Comptime-only: a pack must be expanded to flat positional args
|
||||||
// before codegen. Reaching runtime layout means a pack leaked.
|
// before codegen. Reaching runtime layout means a pack leaked.
|
||||||
.pack => @panic("pack type has no runtime layout (comptime-only)"),
|
.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",
|
.noreturn => "noreturn",
|
||||||
.isize => "isize",
|
.isize => "isize",
|
||||||
.usize => "usize",
|
.usize => "usize",
|
||||||
|
.unresolved => "<unresolved>",
|
||||||
else => {
|
else => {
|
||||||
// User types — format from TypeInfo
|
// User types — format from TypeInfo
|
||||||
const info = self.get(id);
|
const info = self.get(id);
|
||||||
@@ -813,7 +829,7 @@ fn hashTypeInfo(h: *std.hash.Wyhash, info: TypeInfo) void {
|
|||||||
switch (info) {
|
switch (info) {
|
||||||
.signed => |w| h.update(&.{w}),
|
.signed => |w| h.update(&.{w}),
|
||||||
.unsigned => |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)),
|
.pointer => |p| h.update(std.mem.asBytes(&p.pointee)),
|
||||||
.many_pointer => |p| h.update(std.mem.asBytes(&p.element)),
|
.many_pointer => |p| h.update(std.mem.asBytes(&p.element)),
|
||||||
.slice => |s| h.update(std.mem.asBytes(&s.element)),
|
.slice => |s| h.update(std.mem.asBytes(&s.element)),
|
||||||
@@ -866,7 +882,7 @@ fn typeInfoEql(a: TypeInfo, b: TypeInfo) bool {
|
|||||||
return switch (a) {
|
return switch (a) {
|
||||||
.signed => |w| w == b.signed,
|
.signed => |w| w == b.signed,
|
||||||
.unsigned => |w| w == b.unsigned,
|
.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,
|
.pointer => |p| p.pointee == b.pointer.pointee,
|
||||||
.many_pointer => |p| p.element == b.many_pointer.element,
|
.many_pointer => |p| p.element == b.many_pointer.element,
|
||||||
.slice => |s| s.element == b.slice.element,
|
.slice => |s| s.element == b.slice.element,
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user