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

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

View File

@@ -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`", .{});