ERR/E5.2: comptime #run of an escaping failable → diagnostic + halt
A bare failable `#run` (no catch/or) whose error escapes used to segfault (const form `x :: #run f()`) or silently succeed (statement form `#run f();`). Now the compiler reports the raised tag name + the resolved return trace at the #run site and halts with a non-zero exit. - lower.zig: a failable #run's comptime function returns the full failable tuple (so the error slot is inspectable) while the global is typed as the success value; failable side-effects return the tuple instead of void. - emit_llvm.zig: read the always-on comptime trace buffer (extern sx_trace_*); comptimeErrChannel + checkComptimeFailable split the result (non-zero tag → reportComptimeEscape + comptime_failed flag; success → value part). Wired into emitGlobals (const) and runComptimeSideEffects (statement, now filtered by the __run name; buffer cleared before each eval). - core.zig: generateCode returns error.ComptimeError when comptime_failed, so the driver aborts before JIT/link. catch / or / onfail compose at comptime exactly as at runtime; a successful bare #run yields the value. Regressions: examples/1037-errors-comptime-run-escape (diagnostic, exit 1) + 1038-errors-comptime-run-handled (exit 164). Suite: 326.
This commit is contained in:
@@ -27,6 +27,14 @@ const interp_mod = @import("interp.zig");
|
||||
const Interpreter = interp_mod.Interpreter;
|
||||
const Value = interp_mod.Value;
|
||||
|
||||
// The vendored error-trace ring buffer (library/vendors/sx_trace_runtime/sx_trace.c)
|
||||
// is linked into the compiler. Comptime `#run` evaluation pushes frames to it via
|
||||
// foreign `sx_trace_push` calls; after a `#run` we read it here to render the
|
||||
// return trace for an escaping comptime error (E5.2).
|
||||
extern fn sx_trace_len() u32;
|
||||
extern fn sx_trace_frame_at(i: u32) u64;
|
||||
extern fn sx_trace_clear() void;
|
||||
|
||||
fn isIdentByte(b: u8) bool {
|
||||
return (b >= 'a' and b <= 'z') or (b >= 'A' and b <= 'Z') or (b >= '0' and b <= '9') or b == '_';
|
||||
}
|
||||
@@ -92,6 +100,10 @@ pub const LLVMEmitter = struct {
|
||||
// IR Module being emitted
|
||||
ir_mod: *const Module,
|
||||
|
||||
// Set when a comptime `#run` raised an unhandled error (E5.2). The driver
|
||||
// (core.generateCode) aborts with a non-zero exit after emit() when set.
|
||||
comptime_failed: bool = false,
|
||||
|
||||
// Allocator for temporary bookkeeping
|
||||
alloc: Allocator,
|
||||
|
||||
@@ -1329,36 +1341,123 @@ pub const LLVMEmitter = struct {
|
||||
}
|
||||
}
|
||||
|
||||
/// The error-set channel of a (possibly failable) type: the set itself for
|
||||
/// a pure `-> !` result, or the last tuple slot for `-> (T..., !)`. null if
|
||||
/// the type carries no error channel. Mirror of lower.errorChannelOf.
|
||||
fn comptimeErrChannel(self: *LLVMEmitter, ty: TypeId) ?TypeId {
|
||||
if (ty.isBuiltin()) return null;
|
||||
switch (self.ir_mod.types.get(ty)) {
|
||||
.error_set => return ty,
|
||||
.tuple => |t| {
|
||||
if (t.fields.len == 0) return null;
|
||||
const last = t.fields[t.fields.len - 1];
|
||||
if (last.isBuiltin()) return null;
|
||||
return if (self.ir_mod.types.get(last) == .error_set) last else null;
|
||||
},
|
||||
else => return null,
|
||||
}
|
||||
}
|
||||
|
||||
/// Inspect a failable `#run` result. On a non-zero error tag, print the
|
||||
/// comptime-error diagnostic + return trace, flag compilation failed, and
|
||||
/// return null. On success, return the value part (error channel stripped):
|
||||
/// `void_val` for a pure failable, the lone value for `(T, !)`, the
|
||||
/// value-tuple for multi-value. (E5.2)
|
||||
fn checkComptimeFailable(self: *LLVMEmitter, result: Value, fail_ty: TypeId, label: []const u8) ?Value {
|
||||
const channel = self.comptimeErrChannel(fail_ty) orelse return result;
|
||||
var tag: u32 = 0;
|
||||
var success: Value = .void_val;
|
||||
if (channel == fail_ty) {
|
||||
// pure failable — the result IS the error tag (u32)
|
||||
tag = switch (result) {
|
||||
.int => |v| @truncate(@as(u64, @bitCast(v))),
|
||||
else => 0,
|
||||
};
|
||||
} else {
|
||||
// value-carrying — the result is the `{values..., tag}` aggregate
|
||||
const fields = switch (result) {
|
||||
.aggregate => |f| f,
|
||||
else => return result,
|
||||
};
|
||||
if (fields.len == 0) return result;
|
||||
tag = switch (fields[fields.len - 1]) {
|
||||
.int => |v| @truncate(@as(u64, @bitCast(v))),
|
||||
else => 0,
|
||||
};
|
||||
success = if (fields.len == 2) fields[0] else .{ .aggregate = fields[0 .. fields.len - 1] };
|
||||
}
|
||||
if (tag == 0) {
|
||||
sx_trace_clear();
|
||||
return success;
|
||||
}
|
||||
self.reportComptimeEscape(label, tag);
|
||||
sx_trace_clear();
|
||||
self.comptime_failed = true;
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Print the locked comptime-escape diagnostic: the raised tag name, the
|
||||
/// resolved return trace from the thread-local buffer, and a help line.
|
||||
fn reportComptimeEscape(self: *LLVMEmitter, label: []const u8, tag: u32) void {
|
||||
const tname = self.ir_mod.types.tags.getName(tag);
|
||||
std.debug.print("error: comptime `#run` ({s}) raised an unhandled error: error.{s}\n", .{ label, tname });
|
||||
const n = sx_trace_len();
|
||||
if (n > 0) {
|
||||
std.debug.print("error return trace (most recent call last):\n", .{});
|
||||
var i: u32 = 0;
|
||||
while (i < n) : (i += 1) {
|
||||
const packed_frame = sx_trace_frame_at(i);
|
||||
const fid: u32 = @intCast(packed_frame >> 32);
|
||||
const offset: u32 = @truncate(packed_frame);
|
||||
if (fid >= self.ir_mod.functions.items.len) continue;
|
||||
const func = self.ir_mod.getFunction(FuncId.fromIndex(fid));
|
||||
const fname = self.ir_mod.types.getString(func.name);
|
||||
const file_full = func.source_file orelse "";
|
||||
const file = std.fs.path.basename(file_full);
|
||||
var line: usize = 1;
|
||||
var col: usize = 1;
|
||||
if (self.import_sources) |sm| {
|
||||
if (sm.get(file_full)) |src| {
|
||||
const loc = errors.SourceLoc.compute(src, offset);
|
||||
line = loc.line;
|
||||
col = loc.col;
|
||||
}
|
||||
}
|
||||
std.debug.print(" {s} at {s}:{d}:{d}\n", .{ fname, file, line, col });
|
||||
}
|
||||
}
|
||||
std.debug.print("help: handle it at the `#run` site — `#run <expr> catch e {{ ... }}` or `#run <expr> or <default>`\n", .{});
|
||||
}
|
||||
|
||||
/// Run comptime side-effect functions (e.g., `#run main();` at top level).
|
||||
/// These are functions marked `is_comptime = true` with void return that
|
||||
/// aren't associated with any global. They produce compile-time output.
|
||||
fn runComptimeSideEffects(self: *LLVMEmitter) void {
|
||||
for (self.ir_mod.functions.items, 0..) |func, i| {
|
||||
if (!func.is_comptime or func.ret != .void) continue;
|
||||
// Skip functions that are global initializers (already run by emitGlobals)
|
||||
var is_global_init = false;
|
||||
for (self.ir_mod.globals.items) |global| {
|
||||
if (global.comptime_func) |gf| {
|
||||
if (@intFromEnum(gf) == i) {
|
||||
is_global_init = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (is_global_init) continue;
|
||||
// `#run expr;` side-effects are the `__run_N` wrappers (global
|
||||
// initializers are named after their global and run by emitGlobals;
|
||||
// inline `__ct`/`__insert` wrappers run from their call sites). A
|
||||
// failable side-effect carries the error channel on `func.ret`.
|
||||
if (!func.is_comptime) continue;
|
||||
const fname = self.ir_mod.types.getString(func.name);
|
||||
if (!std.mem.startsWith(u8, fname, "__run")) continue;
|
||||
|
||||
// Run the side-effect function via interpreter
|
||||
const func_id = ir_inst.FuncId.fromIndex(@intCast(i));
|
||||
var interp_inst = Interpreter.init(self.ir_mod, self.alloc);
|
||||
interp_inst.build_config = &self.build_config;
|
||||
if (self.import_sources) |sm| interp_inst.setSourceMap(sm);
|
||||
_ = interp_inst.call(func_id, &.{}) catch {};
|
||||
sx_trace_clear();
|
||||
const result = interp_inst.call(func_id, &.{}) catch Value.void_val;
|
||||
// Route #run `print` output to fd 1 so it joins the
|
||||
// JIT-executed runtime's stream. Same call site shape as
|
||||
// `core.flushInterpOutput` — see issue-0047.
|
||||
if (interp_inst.output.items.len > 0) {
|
||||
_ = std.c.write(1, interp_inst.output.items.ptr, interp_inst.output.items.len);
|
||||
}
|
||||
// A bare failable `#run f();` whose error escapes → diagnostic + halt.
|
||||
if (self.comptimeErrChannel(func.ret) != null) {
|
||||
_ = self.checkComptimeFailable(result, func.ret, "top-level statement");
|
||||
}
|
||||
interp_inst.deinit();
|
||||
}
|
||||
}
|
||||
@@ -1394,6 +1493,7 @@ pub const LLVMEmitter = struct {
|
||||
Interpreter.last_bail_op = null;
|
||||
Interpreter.last_bail_builtin = null;
|
||||
Interpreter.last_bail_detail = null;
|
||||
sx_trace_clear();
|
||||
const result = interp_inst.call(func_id, &.{}) catch |err| blk: {
|
||||
// Surface the bail loudly instead of silently filling
|
||||
// the const with zero. Stale state from a previous
|
||||
@@ -1405,7 +1505,22 @@ pub const LLVMEmitter = struct {
|
||||
std.debug.print("error: comptime init of '{s}' failed: {s} (op={s}{s}{s})\n", .{ gname, @errorName(err), op, sep, detail });
|
||||
break :blk .void_val;
|
||||
};
|
||||
const init_val = self.valueToLLVMConst(result, global.ty, &interp_inst, self.ir_mod.types.getString(global.name));
|
||||
// A bare failable `NAME :: #run f();`: the comptime function
|
||||
// returns the failable tuple; split it. Escaping error →
|
||||
// diagnostic + halt (leave the global undef); success → the
|
||||
// value part materializes into the global's success type (E5.2).
|
||||
const cf_ret = self.ir_mod.getFunction(func_id).ret;
|
||||
var init_value = result;
|
||||
if (self.comptimeErrChannel(cf_ret) != null) {
|
||||
if (self.checkComptimeFailable(result, cf_ret, self.ir_mod.types.getString(global.name))) |succ| {
|
||||
init_value = succ;
|
||||
} else {
|
||||
c.LLVMSetInitializer(llvm_global, c.LLVMGetUndef(llvm_ty));
|
||||
self.global_map.put(@intCast(i), llvm_global) catch {};
|
||||
continue;
|
||||
}
|
||||
}
|
||||
const init_val = self.valueToLLVMConst(init_value, global.ty, &interp_inst, self.ir_mod.types.getString(global.name));
|
||||
c.LLVMSetInitializer(llvm_global, init_val);
|
||||
} else if (global.init_val) |iv| {
|
||||
const init_val = switch (iv) {
|
||||
|
||||
@@ -8486,30 +8486,46 @@ pub const Lowering = struct {
|
||||
// infer the global's type from the comptime expression's return
|
||||
// shape. `resolveType(null)` returns `.s64` for legacy reasons —
|
||||
// good for primitive helpers, silently wrong for anything else.
|
||||
const ret_ty: TypeId = if (type_ann) |n|
|
||||
const expr_ty = self.inferExprType(expr);
|
||||
// A failable `#run` (bare, no `catch`/`or`): the comptime function
|
||||
// returns the full failable tuple so the #run site can inspect the
|
||||
// error slot, but the GLOBAL is typed as the success value. On a
|
||||
// comptime error the global never materializes — emit halts with a
|
||||
// diagnostic + trace (E5.2). A handled `#run … catch/or …` already
|
||||
// strips the error channel, so it lands here as non-failable.
|
||||
const is_failable = self.errorChannelOf(expr_ty) != null;
|
||||
const func_ret: TypeId = if (is_failable)
|
||||
expr_ty
|
||||
else if (type_ann) |n|
|
||||
self.resolveTypeWithBindings(n)
|
||||
else
|
||||
self.inferExprType(expr);
|
||||
const func_id = self.createComptimeFunction(name, expr, ret_ty);
|
||||
expr_ty;
|
||||
const global_ty: TypeId = if (is_failable) self.failableSuccessType(expr_ty) else func_ret;
|
||||
const func_id = self.createComptimeFunction(name, expr, func_ret);
|
||||
|
||||
// Add a global constant whose initializer will be filled by the interpreter.
|
||||
const name_id = self.module.types.internString(name);
|
||||
const gid = self.module.addGlobal(.{
|
||||
.name = name_id,
|
||||
.ty = ret_ty,
|
||||
.ty = global_ty,
|
||||
.init_val = null, // will be filled by interpreter at emit time
|
||||
.is_const = true,
|
||||
.comptime_func = func_id,
|
||||
});
|
||||
|
||||
// Register for runtime lookup: identifier resolution emits global_get
|
||||
self.global_names.put(name, .{ .id = gid, .ty = ret_ty }) catch {};
|
||||
self.global_names.put(name, .{ .id = gid, .ty = global_ty }) catch {};
|
||||
}
|
||||
|
||||
/// Lower a standalone `#run expr;` at the top level (side-effect only).
|
||||
/// Creates a comptime function that the interpreter should execute.
|
||||
fn lowerComptimeSideEffect(self: *Lowering, expr: *const Node) void {
|
||||
_ = self.createComptimeFunction("__run", expr, .void);
|
||||
// A failable side-effect `#run f();` returns the failable tuple so the
|
||||
// emit-time runner can detect an escaping error and halt (E5.2);
|
||||
// non-failable side effects stay `void`.
|
||||
const expr_ty = self.inferExprType(expr);
|
||||
const ret: TypeId = if (self.errorChannelOf(expr_ty) != null) expr_ty else .void;
|
||||
_ = self.createComptimeFunction("__run", expr, ret);
|
||||
}
|
||||
|
||||
/// Lower a `#run expr` that appears inline within an expression.
|
||||
|
||||
Reference in New Issue
Block a user