ERR/E4.2: failable-main wrapper (report + exit 1 on escaping error)

A pure-failable `main` (`-> !` / `-> !Named`) that lets an error reach the
function boundary now exits 1 and prints `error: unhandled error reached
main: error.<tag>` + 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.
This commit is contained in:
agra
2026-06-01 09:48:32 +03:00
parent bb20339691
commit 210cf91e37
6 changed files with 123 additions and 2 deletions

View File

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

View File

@@ -22,6 +22,7 @@
#include <stddef.h> #include <stddef.h>
#include <stdint.h> #include <stdint.h>
#include <stdio.h>
#define SX_TRACE_CAP 32 #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; uint32_t base = (sx_trace_count == SX_TRACE_CAP) ? sx_trace_head : 0u;
return sx_trace_frames[(base + i) % SX_TRACE_CAP]; 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 ("<location pending DWARF>")
// 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: <location pending DWARF> (raw %llu)\n",
i, (unsigned long long)frame);
}
}

View File

@@ -2468,11 +2468,20 @@ pub const LLVMEmitter = struct {
// ── Terminators ──────────────────────────────────────── // ── Terminators ────────────────────────────────────────
.ret => |un| { .ret => |un| {
var val = self.resolveRef(un.operand); 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 // sret-shaped function: declared return-type-in-IR is
// the struct, but the LLVM signature is void with a // the struct, but the LLVM signature is void with a
// prepended ptr sret param. Store the value through // prepended ptr sret param. Store the value through
// the sret slot and emit ret void. // 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 needs_c_abi = func.is_extern or func.call_conv == .c;
const raw_ret = self.toLLVMType(func.ret); const raw_ret = self.toLLVMType(func.ret);
if (needs_c_abi and self.needsByval(func.ret, raw_ret)) { if (needs_c_abi and self.needsByval(func.ret, raw_ret)) {
@@ -4681,6 +4690,50 @@ pub const LLVMEmitter = struct {
return global; 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 /// 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 /// 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 /// the reserved "" no-error name). `error_tag_name_get` GEPs into it at the

View File

@@ -389,7 +389,14 @@ pub const Lowering = struct {
// void / integer, and a pure failable `-> !` (a bare u32 error tag). // void / integer, and a pure failable `-> !` (a bare u32 error tag).
if (rt == .void or self.isIntEx(rt)) return; if (rt == .void or self.isIntEx(rt)) return;
if (self.errorChannelOf(rt)) |chan| { 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. // `-> (T..., !)` — a multi-slot tuple return; not yet wired.
if (self.diagnostics) |diags| { 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`", .{}); 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`", .{});

View File

@@ -0,0 +1 @@
1

View File

@@ -0,0 +1,5 @@
v = 10
error: unhandled error reached main: error.Empty
error return trace (most recent call last):
frame 0: <location pending DWARF> (raw 1)
frame 1: <location pending DWARF> (raw 1)