diff --git a/src/core.zig b/src/core.zig index 97e909c..00f770b 100644 --- a/src/core.zig +++ b/src/core.zig @@ -182,6 +182,7 @@ pub const Compilation = struct { if (self.ir_emitter) |*e| interp.build_config = &e.build_config; ir.Interpreter.last_bail_op = null; ir.Interpreter.last_bail_builtin = null; + ir.Interpreter.last_bail_detail = null; const result = interp.call(id, args) catch |err| { if (interp.output.items.len > 0) std.debug.print("{s}", .{interp.output.items}); return err; diff --git a/src/ir/interp.zig b/src/ir/interp.zig index 420f825..11773da 100644 --- a/src/ir/interp.zig +++ b/src/ir/interp.zig @@ -152,6 +152,27 @@ pub const Interpreter = struct { pub var last_bail_file: ?[]const u8 = null; pub var last_bail_offset: u32 = 0; pub var last_bail_builtin: ?[]const u8 = null; + /// Free-text explanation of WHY the bail happened — set at sites + /// that currently can't handle a specific Value/op combination + /// (raw-pointer loads, struct_gep through `*void`, etc.). The host + /// diagnostic renderer surfaces this so users see "load through + /// raw pointer not supported" instead of a bare `CannotEvalComptime`. + pub var last_bail_detail: ?[]const u8 = null; + + /// Set `last_bail_detail` to a static message and return the error. + /// Use at sites where a specific raw-pointer Value tag isn't handled + /// so users get a clear explanation instead of guessing. + fn bailDetail(comptime msg: []const u8) InterpError { + if (last_bail_detail == null) last_bail_detail = msg; + return error.CannotEvalComptime; + } + + /// Like `bailDetail` but returns a `TypeError` — for foreign-arg + /// marshalling sites that previously erased the reason. + fn typeErrorDetail(comptime msg: []const u8) InterpError { + if (last_bail_detail == null) last_bail_detail = msg; + return error.TypeError; + } pub fn init(module: *const Module, alloc: Allocator) Interpreter { var hooks = compiler_hooks.Registry.init(alloc); @@ -181,6 +202,15 @@ pub const Interpreter = struct { /// protocol-dispatch chain bottoms out at a foreign-libc-malloc /// pointer and sx code stores through it. Comptime safety is the /// caller's responsibility — wild writes will fault. + /// + /// **Width assumption.** `.int` and `.float` always write 8 bytes. + /// The Store IR op doesn't currently thread val's TypeId into the + /// interp, so we can't tell s32/s64 or f32/f64 apart from the + /// Value tag. Real-world comptime paths (protocol erasure heap + /// copies, Context aggregate stores) hit 8-byte fields, so this + /// works in practice. If a comptime store ever hits a smaller + /// destination through a raw pointer, neighbors get clobbered — + /// add `val_ty` to `inst.Store` and switch on it here. fn storeAtRawPtr(self: *Interpreter, addr: i64, val: Value) InterpError!void { _ = self; const dst: [*]u8 = @ptrFromInt(@as(usize, @bitCast(addr))); @@ -201,7 +231,13 @@ pub const Interpreter = struct { const bytes = std.mem.toBytes(zero); @memcpy(dst[0..bytes.len], &bytes); }, - else => return error.CannotEvalComptime, + .aggregate => return bailDetail("comptime store of aggregate through raw pointer not supported (struct field layout not threaded into Store IR op)"), + .heap_ptr => return bailDetail("comptime store of interp-heap pointer through raw pointer not supported"), + .byte_ptr => return bailDetail("comptime store of byte pointer through raw pointer not supported"), + .slot_ptr => return bailDetail("comptime store of slot pointer through raw pointer not supported (frame-local slot indices aren't meaningful as memory contents)"), + .func_ref => return bailDetail("comptime store of func_ref through raw pointer not supported"), + .closure => return bailDetail("comptime store of closure value through raw pointer not supported"), + .string, .type_tag, .void_val, .undef => return bailDetail("comptime store: unsupported Value kind at raw destination"), } } @@ -360,12 +396,12 @@ pub const Interpreter = struct { tmp.append(self.alloc, buf) catch return error.TypeError; break :blk @intFromPtr(buf.ptr); }, - else => return error.TypeError, + else => return typeErrorDetail("comptime foreign call: unsupported aggregate data-field kind (expected heap_ptr/string/int)"), } } - return error.TypeError; + return typeErrorDetail("comptime foreign call: aggregate arg must be a {ptr, len} fat-pointer pair"); }, - else => error.TypeError, + else => return typeErrorDetail("comptime foreign call: unsupported arg Value kind"), }; } @@ -639,7 +675,14 @@ pub const Interpreter = struct { // materializeCtxArg dereferences the caller's slot_ptr. // `load(ref_0)` then naturally yields the Context value. .aggregate => return .{ .value = ptr }, - else => return error.CannotEvalComptime, + // Comptime load through a raw host pointer needs the + // target IR type to know byte width — currently not + // threaded into the .load op. Add it when a comptime + // path hits this. + .int => return bailDetail("comptime load through raw host pointer not supported (IR type width not threaded)"), + .byte_ptr => return bailDetail("comptime load through raw byte pointer not supported"), + .heap_ptr => return bailDetail("comptime load through interp heap pointer not supported"), + else => return bailDetail("comptime load: unsupported pointer kind"), } }, .store => |s| { @@ -674,7 +717,7 @@ pub const Interpreter = struct { const dst: [*]u8 = @ptrFromInt(addr); dst[0] = byte; }, - else => return error.CannotEvalComptime, + else => return bailDetail("comptime store: unsupported pointer kind"), } return .{ .value = .void_val }; }, @@ -923,7 +966,14 @@ pub const Interpreter = struct { frame.storeSlot(field_slot, .{ .aggregate = field_ref }); return .{ .value = .{ .slot_ptr = field_slot } }; }, - else => return error.CannotEvalComptime, + // struct_gep through a raw host pointer requires the + // struct's field-offset table — feasible via + // `fa.base_type` but not currently wired. Add when a + // comptime path hits this. + .int => return bailDetail("comptime struct_gep through raw host pointer not supported"), + .byte_ptr => return bailDetail("comptime struct_gep through raw byte pointer not supported"), + .heap_ptr => return bailDetail("comptime struct_gep through interp heap pointer not supported"), + else => return bailDetail("comptime struct_gep: unsupported pointer kind"), } }, @@ -1016,6 +1066,16 @@ pub const Interpreter = struct { const val = frame.getRef(u.operand); switch (val) { .slot_ptr => |slot| return .{ .value = frame.loadSlot(slot) }, + // Real raw-memory deref needs val's IR type for byte + // width — not yet threaded. Erroring is safer than + // returning the pointer-as-int unchanged, which + // silently looks like a successful deref. + .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 }, } }, @@ -1207,7 +1267,7 @@ pub const Interpreter = struct { else => {}, } } - return error.CannotEvalComptime; + return bailDetail("comptime index_gep: unsupported aggregate-base shape (expected {data_ptr, len} with heap_ptr or int data field)"); }, .string => |s| { // String literal — copy to heap and return heap_ptr at offset @@ -1219,13 +1279,17 @@ pub const Interpreter = struct { .offset = @intCast(offset), } } }; }, - // Raw host pointer base — same byte-addressed offset - // semantics as the aggregate{int_ptr, ...} branch. + // Raw host pointer base — byte-addressed offset. + // Element size > 1 would silently mis-index; document + // the assumption. Callers stride past byte granularity + // must wrap the pointer in an aggregate so the + // {data_ptr, len} branch fires (which is also + // byte-addressed today — fix here when needed). .int => |p| { const offset = idx.asInt() orelse return error.TypeError; return .{ .value = .{ .int = p + offset } }; }, - else => return error.CannotEvalComptime, + else => return bailDetail("comptime index_gep: unsupported base kind"), } }, diff --git a/src/main.zig b/src/main.zig index 0bd85cd..769e809 100644 --- a/src/main.zig +++ b/src/main.zig @@ -428,16 +428,18 @@ fn printInterpBailDiag(comp: *const sx.core.Compilation, label: []const u8, err: return; }; const op_detail: []const u8 = if (sx.ir.Interpreter.last_bail_builtin) |b| b else op; + const explanation = sx.ir.Interpreter.last_bail_detail orelse ""; + const sep: []const u8 = if (explanation.len > 0) ": " else ""; if (sx.ir.Interpreter.last_bail_file) |file| { if (comp.import_sources.get(file)) |source| { const loc = sx.errors.SourceLoc.compute(source, sx.ir.Interpreter.last_bail_offset); - std.debug.print("error: {s} failed: {s} (op={s}/{s}) at {s}:{d}:{d}\n", .{ label, @errorName(err), op, op_detail, file, loc.line, loc.col }); + std.debug.print("error: {s} failed: {s} (op={s}/{s}{s}{s}) at {s}:{d}:{d}\n", .{ label, @errorName(err), op, op_detail, sep, explanation, file, loc.line, loc.col }); return; } - std.debug.print("error: {s} failed: {s} (op={s}/{s}) at {s}:+{d}\n", .{ label, @errorName(err), op, op_detail, file, sx.ir.Interpreter.last_bail_offset }); + std.debug.print("error: {s} failed: {s} (op={s}/{s}{s}{s}) at {s}:+{d}\n", .{ label, @errorName(err), op, op_detail, sep, explanation, file, sx.ir.Interpreter.last_bail_offset }); return; } - std.debug.print("error: {s} failed: {s} (op={s}/{s})\n", .{ label, @errorName(err), op, op_detail }); + std.debug.print("error: {s} failed: {s} (op={s}/{s}{s}{s})\n", .{ label, @errorName(err), op, op_detail, sep, explanation }); } fn readSource(allocator: std.mem.Allocator, io: std.Io, input_path: []const u8) ![:0]const u8 {