From 210cf91e378ee12f57bbc0ad276657960b400d27 Mon Sep 17 00:00:00 2001 From: agra Date: Mon, 1 Jun 2026 09:48:32 +0300 Subject: [PATCH] ERR/E4.2: failable-`main` wrapper (report + exit 1 on escaping error) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A pure-failable `main` (`-> !` / `-> !Named`) that lets an error reach the function boundary now exits 1 and prints `error: unhandled error reached main: error.` + the return trace to stderr, instead of returning the raw tag id truncated as the exit code with no diagnostic. Success exits 0; a `catch`-absorbed error exits 0 (buffer cleared). Codegen wrapper so JIT and AOT behave identically (no host-side special- casing): - emit_llvm.zig: the `.ret` arm detects a failable main and routes to new `emitFailableMainRet` — `icmp ne tag, 0` → success block `ret i32 0` / error block GEPs the tag name out of the always-linked tag-name table, calls `sx_trace_report_unhandled`, `ret i32 1`. main's bare-u32 returns (success `ret(0)` + each raise's `ret(tag)`) all funnel through it. - sx_trace.c: new `sx_trace_report_unhandled(tag, name, name_len)` prints the header + surviving frames to stderr (placeholder frame format mirrors trace.sx until DWARF/E3.0). Lives next to the buffer it reads. - lower.zig validateMainSignature: the pure-failable arm sets needs_trace_runtime so the AOT path auto-links sx_trace.c even when the body emits no other push/clear. Value-carrying `-> (T, !)` main stays gate-rejected (multi-slot wrapper is a separate slice). examples/244-failable-main.sx. --- examples/244-failable-main.sx | 29 +++++++++++ library/vendors/sx_trace_runtime/sx_trace.c | 26 ++++++++++ src/ir/emit_llvm.zig | 55 ++++++++++++++++++++- src/ir/lower.zig | 9 +++- tests/expected/244-failable-main.exit | 1 + tests/expected/244-failable-main.txt | 5 ++ 6 files changed, 123 insertions(+), 2 deletions(-) create mode 100644 examples/244-failable-main.sx create mode 100644 tests/expected/244-failable-main.exit create mode 100644 tests/expected/244-failable-main.txt diff --git a/examples/244-failable-main.sx b/examples/244-failable-main.sx new file mode 100644 index 0000000..62442ae --- /dev/null +++ b/examples/244-failable-main.sx @@ -0,0 +1,29 @@ +// Failable `-> !` main entry-point wrapper (ERR step E4.2). A pure-failable +// main that lets an error reach the function boundary exits 1 and prints the +// unhandled-error header (with the tag name, via the always-linked tag-name +// table) plus the return trace to stderr — instead of the old behavior of +// returning the raw tag id as the exit code with no diagnostic. A successful +// run (no escaping error) exits 0. +// +// Note: the header + trace go to stderr. The test runner merges stderr+stdout, +// so the snapshot shows them interleaved with the `print` (stdout) lines. +// Frame locations are placeholders until DWARF (ERR E3.0); count + ordering + +// the tag name are already meaningful. Expected exit code: 1. + +#import "modules/std.sx"; + +ParseErr :: error { Empty, BadDigit }; + +inner :: (n: s32) -> (s32, !ParseErr) { + if n == 0 { raise error.Empty; } // pushes a frame + if n < 0 { raise error.BadDigit; } + return n * 2; +} + +main :: () -> !ParseErr { + v := try inner(5); // succeeds → v = 10 + print("v = {}\n", v); + w := try inner(0); // raises Empty → propagates to main + print("w = {}\n", w); // never reached + return; +} diff --git a/library/vendors/sx_trace_runtime/sx_trace.c b/library/vendors/sx_trace_runtime/sx_trace.c index c7d05cd..e359827 100644 --- a/library/vendors/sx_trace_runtime/sx_trace.c +++ b/library/vendors/sx_trace_runtime/sx_trace.c @@ -22,6 +22,7 @@ #include #include +#include #define SX_TRACE_CAP 32 @@ -67,3 +68,28 @@ uint64_t sx_trace_frame_at(uint32_t i) { uint32_t base = (sx_trace_count == SX_TRACE_CAP) ? sx_trace_head : 0u; return sx_trace_frames[(base + i) % SX_TRACE_CAP]; } + +// The failable-`main` entry-point reporter (ERR step E4.2). Called by the +// emitted main wrapper when an error reaches the function boundary: prints the +// unhandled-error header (with the tag name passed in — the compiler resolves +// it from the always-linked tag-name table) followed by the surviving trace +// frames, all to stderr. `name` is borrowed (a `string` slice, not NUL- +// terminated), so `name_len` bounds the print. The frame format mirrors +// trace.sx's `to_string`; both stay placeholder ("") +// until DWARF line-info (E3.0) lands, after which both gain real file:line:col. +void sx_trace_report_unhandled(uint32_t tag, const char *name, size_t name_len) { + (void)tag; + dprintf(2, "error: unhandled error reached main: error.%.*s\n", + (int)name_len, name ? name : ""); + uint32_t n = sx_trace_len(); + if (n == 0u) return; + dprintf(2, "error return trace (most recent call last):\n"); + if (sx_trace_truncated() != 0u) { + dprintf(2, " ... older frames omitted (buffer full)\n"); + } + for (uint32_t i = 0u; i < n; i++) { + uint64_t frame = sx_trace_frame_at(i); + dprintf(2, " frame %u: (raw %llu)\n", + i, (unsigned long long)frame); + } +} diff --git a/src/ir/emit_llvm.zig b/src/ir/emit_llvm.zig index c885f6a..c177b05 100644 --- a/src/ir/emit_llvm.zig +++ b/src/ir/emit_llvm.zig @@ -2468,11 +2468,20 @@ pub const LLVMEmitter = struct { // ── Terminators ──────────────────────────────────────── .ret => |un| { var val = self.resolveRef(un.operand); + const func = &self.ir_mod.functions.items[self.current_func_idx]; + // Failable `-> !` main: `val` is the bare u32 error tag + // (0 = success). Wrap it in the entry-point reporter (ERR E4.2) + // — exit 0 on success, else print the trace + tag to stderr and + // exit 1 — instead of returning the tag as the raw exit code. + if (self.current_func_is_main and self.ir_mod.types.get(func.ret) == .error_set) { + self.emitFailableMainRet(val); + self.advanceRefCounter(); + return; + } // sret-shaped function: declared return-type-in-IR is // the struct, but the LLVM signature is void with a // prepended ptr sret param. Store the value through // the sret slot and emit ret void. - const func = &self.ir_mod.functions.items[self.current_func_idx]; const needs_c_abi = func.is_extern or func.call_conv == .c; const raw_ret = self.toLLVMType(func.ret); if (needs_c_abi and self.needsByval(func.ret, raw_ret)) { @@ -4681,6 +4690,50 @@ pub const LLVMEmitter = struct { return global; } + /// Failable `-> !` main entry-point wrapper (ERR E4.2). At the LLVM level + /// main returns i32; for a pure-failable main the IR `ret` carries the u32 + /// error tag (0 = "no error"). Emit the branch: tag == 0 → `ret i32 0` + /// (success); else resolve the tag name from the always-linked tag-name + /// table, hand it + the tag to `sx_trace_report_unhandled` (prints the + /// header + return trace to stderr), and `ret i32 1`. + fn emitFailableMainRet(self: *LLVMEmitter, tag_val: c.LLVMValueRef) void { + const llvm_func = c.LLVMGetBasicBlockParent(c.LLVMGetInsertBlock(self.builder)); + const tag_i32 = self.coerceArg(tag_val, self.cached_i32); + + const is_err = c.LLVMBuildICmp(self.builder, c.LLVMIntNE, tag_i32, c.LLVMConstInt(self.cached_i32, 0, 0), "main.iserr"); + const ok_bb = c.LLVMAppendBasicBlockInContext(self.context, llvm_func, "main.ok"); + const err_bb = c.LLVMAppendBasicBlockInContext(self.context, llvm_func, "main.err"); + _ = c.LLVMBuildCondBr(self.builder, is_err, err_bb, ok_bb); + + // Success: exit 0. + c.LLVMPositionBuilderAtEnd(self.builder, ok_bb); + _ = c.LLVMBuildRet(self.builder, c.LLVMConstInt(self.cached_i32, 0, 0)); + + // Error: resolve the tag name, report to stderr, exit 1. + c.LLVMPositionBuilderAtEnd(self.builder, err_bb); + const global = self.getOrBuildTagNameArray(); + const idx = c.LLVMBuildZExt(self.builder, tag_i32, self.cached_i64, "main.tagidx"); + const string_ty = self.getStringStructType(); + const n: u32 = @intCast(self.ir_mod.types.tags.names.items.len); + const array_ty = c.LLVMArrayType(string_ty, n); + const zero = c.LLVMConstInt(self.cached_i64, 0, 0); + var indices = [2]c.LLVMValueRef{ zero, idx }; + const gep = c.LLVMBuildInBoundsGEP2(self.builder, array_ty, global, &indices, 2, "main.tag.gep"); + const name_struct = c.LLVMBuildLoad2(self.builder, string_ty, gep, "main.tag.name"); + const name_ptr = c.LLVMBuildExtractValue(self.builder, name_struct, 0, "main.tag.ptr"); + const name_len = c.LLVMBuildExtractValue(self.builder, name_struct, 1, "main.tag.len"); + + const reporter, const reporter_ty = self.lazyDeclareCRuntime( + "sx_trace_report_unhandled", + &[_]c.LLVMTypeRef{ self.cached_i32, self.cached_ptr, self.cached_i64 }, + self.cached_void, + 0, + ); + var args = [3]c.LLVMValueRef{ tag_i32, name_ptr, name_len }; + _ = c.LLVMBuildCall2(self.builder, reporter_ty, reporter, &args, 3, ""); + _ = c.LLVMBuildRet(self.builder, c.LLVMConstInt(self.cached_i32, 1, 0)); + } + /// The always-linked tag-name table: a `[N x {ptr, i64}]` global of tag /// names indexed by global tag id (the `TagRegistry` namespace; slot 0 is /// the reserved "" no-error name). `error_tag_name_get` GEPs into it at the diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 1af0285..6f272ce 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -389,7 +389,14 @@ pub const Lowering = struct { // void / integer, and a pure failable `-> !` (a bare u32 error tag). if (rt == .void or self.isIntEx(rt)) return; if (self.errorChannelOf(rt)) |chan| { - if (rt == chan) return; // pure `-> !` / `-> !Named` + if (rt == chan) { + // pure `-> !` / `-> !Named`. The emitted entry-point wrapper + // (emit_llvm `emitFailableMainRet`) calls `sx_trace_report_unhandled` + // on an escaping error, so the AOT path must auto-link the trace + // runtime even when the body emits no other push/clear. + self.needs_trace_runtime = true; + return; + } // `-> (T..., !)` — a multi-slot tuple return; not yet wired. if (self.diagnostics) |diags| { diags.addFmt(.err, if (fd.return_type) |rtn| rtn.span else null, "a value-carrying failable `main` (`-> (T, !)`) is not yet supported — its multi-slot return ABI-mismatches the entry-point call; use `-> !` (no value) or a non-failable integer return, or absorb errors with `catch`", .{}); diff --git a/tests/expected/244-failable-main.exit b/tests/expected/244-failable-main.exit new file mode 100644 index 0000000..d00491f --- /dev/null +++ b/tests/expected/244-failable-main.exit @@ -0,0 +1 @@ +1 diff --git a/tests/expected/244-failable-main.txt b/tests/expected/244-failable-main.txt new file mode 100644 index 0000000..61323b6 --- /dev/null +++ b/tests/expected/244-failable-main.txt @@ -0,0 +1,5 @@ +v = 10 +error: unhandled error reached main: error.Empty +error return trace (most recent call last): + frame 0: (raw 1) + frame 1: (raw 1)