diff --git a/examples/1037-errors-comptime-run-escape.sx b/examples/1037-errors-comptime-run-escape.sx new file mode 100644 index 0000000..2e81624 --- /dev/null +++ b/examples/1037-errors-comptime-run-escape.sx @@ -0,0 +1,18 @@ +// Comptime `#run` of a failable whose error ESCAPES (no `catch` / `or`): the +// compiler reports the raised tag name + the return trace at the `#run` site and +// halts with a non-zero exit (E5.2). Before this, a bare failable `#run` +// segfaulted (const form) or silently succeeded (statement form). + +#import "modules/std.sx"; + +E :: error { Bad, Empty } + +parse :: (n: s32) -> (s32, !E) { + if n < 0 { raise error.Bad; } + if n == 0 { raise error.Empty; } + return n * 2; +} + +x :: #run parse(-1); // error.Bad escapes → comptime diagnostic + halt + +main :: () -> s32 { return x; } diff --git a/examples/1038-errors-comptime-run-handled.sx b/examples/1038-errors-comptime-run-handled.sx new file mode 100644 index 0000000..73a4128 --- /dev/null +++ b/examples/1038-errors-comptime-run-handled.sx @@ -0,0 +1,31 @@ +// Comptime `#run` of a failable composes with the handlers exactly as at +// runtime: `catch` absorbs, `or` terminates, a successful bare `#run` yields the +// value (error channel stripped), and an `onfail` in the evaluated body still +// runs during comptime unwinding (E5.2). + +#import "modules/std.sx"; + +E :: error { Bad, Empty } + +parse :: (n: s32) -> (s32, !E) { + if n < 0 { raise error.Bad; } + if n == 0 { raise error.Empty; } + return n * 2; +} + +guard :: (ok: bool) -> !E { + onfail print("comptime cleanup\n"); + if !ok { raise error.Bad; } + return; +} + +ok_v :: #run parse(5); // success → 10 (value, error stripped) +caught :: #run parse(-1) catch e 99; // Bad → 99 +ored :: #run parse(0) or 55; // Empty → 55 + +#run guard(false) catch e { }; // onfail fires during the comptime unwind + +main :: () -> s32 { + print("ok={} caught={} ored={}\n", ok_v, caught, ored); + return ok_v + caught + ored; // 10 + 99 + 55 = 164 +} diff --git a/examples/expected/1037-errors-comptime-run-escape.exit b/examples/expected/1037-errors-comptime-run-escape.exit new file mode 100644 index 0000000..d00491f --- /dev/null +++ b/examples/expected/1037-errors-comptime-run-escape.exit @@ -0,0 +1 @@ +1 diff --git a/examples/expected/1037-errors-comptime-run-escape.stderr b/examples/expected/1037-errors-comptime-run-escape.stderr new file mode 100644 index 0000000..896c4f5 --- /dev/null +++ b/examples/expected/1037-errors-comptime-run-escape.stderr @@ -0,0 +1,4 @@ +error: comptime `#run` (x) raised an unhandled error: error.Bad +error return trace (most recent call last): + parse at 1037-errors-comptime-run-escape.sx:11:17 +help: handle it at the `#run` site — `#run catch e { ... }` or `#run or ` diff --git a/examples/expected/1037-errors-comptime-run-escape.stdout b/examples/expected/1037-errors-comptime-run-escape.stdout new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/examples/expected/1037-errors-comptime-run-escape.stdout @@ -0,0 +1 @@ + diff --git a/examples/expected/1038-errors-comptime-run-handled.exit b/examples/expected/1038-errors-comptime-run-handled.exit new file mode 100644 index 0000000..4e9bdff --- /dev/null +++ b/examples/expected/1038-errors-comptime-run-handled.exit @@ -0,0 +1 @@ +164 diff --git a/examples/expected/1038-errors-comptime-run-handled.stderr b/examples/expected/1038-errors-comptime-run-handled.stderr new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/examples/expected/1038-errors-comptime-run-handled.stderr @@ -0,0 +1 @@ + diff --git a/examples/expected/1038-errors-comptime-run-handled.stdout b/examples/expected/1038-errors-comptime-run-handled.stdout new file mode 100644 index 0000000..3dfce0d --- /dev/null +++ b/examples/expected/1038-errors-comptime-run-handled.stdout @@ -0,0 +1,3 @@ +comptime cleanup +--- build done --- +ok=10 caught=99 ored=55 diff --git a/src/core.zig b/src/core.zig index 782295c..ffb524e 100644 --- a/src/core.zig +++ b/src/core.zig @@ -152,6 +152,9 @@ pub const Compilation = struct { // callbacks can re-enter the interpreter via `invokeByName`. self.ir_module = ir_mod_ptr; self.ir_emitter = emitter; + // A comptime `#run` raised an unhandled error — the diagnostic + trace + // were already printed to stderr; abort before JIT/link (E5.2). + if (emitter.comptime_failed) return error.ComptimeError; } /// Re-enter the IR interpreter after `generateCode` (and after linking, diff --git a/src/ir/emit_llvm.zig b/src/ir/emit_llvm.zig index 11b05c9..45c8be5 100644 --- a/src/ir/emit_llvm.zig +++ b/src/ir/emit_llvm.zig @@ -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 catch e {{ ... }}` or `#run or `\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) { diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 1c5b5d5..61024c9 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -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.