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:
29
examples/244-failable-main.sx
Normal file
29
examples/244-failable-main.sx
Normal 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;
|
||||||
|
}
|
||||||
26
library/vendors/sx_trace_runtime/sx_trace.c
vendored
26
library/vendors/sx_trace_runtime/sx_trace.c
vendored
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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`", .{});
|
||||||
|
|||||||
1
tests/expected/244-failable-main.exit
Normal file
1
tests/expected/244-failable-main.exit
Normal file
@@ -0,0 +1 @@
|
|||||||
|
1
|
||||||
5
tests/expected/244-failable-main.txt
Normal file
5
tests/expected/244-failable-main.txt
Normal 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)
|
||||||
Reference in New Issue
Block a user