mem: interp sweep — every silent arm now bails with a named reason

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: <reason>` 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.
This commit is contained in:
agra
2026-05-25 11:57:12 +03:00
parent 4de565b7da
commit e9df33a7e3

View File

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