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:
@@ -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
|
||||
|
||||
@@ -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`", .{});
|
||||
|
||||
Reference in New Issue
Block a user