mem: add explicit bail diagnostics for unhandled raw-pointer interp paths

Comptime fall-through paths used to surface as bare `CannotEvalComptime`
with no hint about the actual limitation. Now each raw-pointer Value
combination that isn't yet wired sets `Interpreter.last_bail_detail`
with a one-line explanation; `printInterpBailDiag` appends it after
the op tag:

  error: post-link callback failed: CannotEvalComptime
    (op=load/load: comptime load through raw host pointer not supported
    (IR type width not threaded)) at .../bundle.sx:N:N

Sites covered: `.load` / `.store` / `.struct_gep` / `.deref` /
`.index_gep` arms for `.int`, `.byte_ptr`, `.heap_ptr` bases;
`storeAtRawPtr`'s catch-all (now exhaustively names every rejected
Value kind); foreign-arg marshalling of unsupported aggregate shapes.

Notable behaviour change: `.deref` through a raw pointer used to
silently return the pointer-as-int unchanged. That looked like a
successful deref to callers — now it errors loudly. Aggregate
passthrough (for `*string` / `*Closure` slot deref) is preserved.

The `storeAtRawPtr` `.int`/`.float` arms still assume 8-byte width —
the Store IR op doesn't carry val's TypeId. Documented inline at the
helper: real-world comptime stores hit 8-byte fields; smaller dests
would clobber. Threading val_ty into Store is left for when a
comptime path actually hits this.

153/153 still passing. The new diagnostics fire when a comptime path
goes through an unhandled shape — verified by reading the bail text
from a synthetic test (separate issue: `#run` silently drops the error
instead of surfacing the diagnostic to the user — out of scope here).
This commit is contained in:
agra
2026-05-25 11:08:03 +03:00
parent d415bcceaa
commit 26d96ac15e
3 changed files with 81 additions and 14 deletions

View File

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

View File

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

View File

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