ERR/E4.2: value-carrying -> (int, !) main wrapper

Extends the failable-main entry-point wrapper to a value-carrying main.
`main :: () -> (int, !)` now exits the integer value on success (truncated
to u8, like a plain integer main) and reports the header + trace to stderr
+ exits 1 on an escaping error (same reporter as the pure `-> !` form).

- lower.zig validateMainSignature: accept a 2-field `{int, error_set}`
  tuple return (set needs_trace_runtime) instead of rejecting it. Multi-
  value `-> (T1, T2, !)` and non-integer value slots still reject — there's
  no single integer exit code to map them to (sharpened diagnostic).
- emit_llvm.zig: the `.ret` arm detects a value-carrying main (tuple ending
  in `.error_set`) and extracts `{value, tag}` (extractvalue 0/1) before
  calling emitFailableMainRet, now generalized to take an optional `value`
  (null → pure `-> !`, success exits 0; present → success exits the value).
  C reporter unchanged.

All E4.2 entry-point shapes (void / int / `-> !` / `-> (int, !)`) now done.
examples/245-failable-main-value.sx (exit 64); 239 comment refreshed.
This commit is contained in:
agra
2026-06-01 10:00:03 +03:00
parent 210cf91e37
commit e898effb4b
7 changed files with 78 additions and 29 deletions

View File

@@ -2469,14 +2469,28 @@ pub const LLVMEmitter = struct {
.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;
// Failable main: wrap the return in the entry-point reporter
// (ERR E4.2) — exit 0 (or the value) on success, else print the
// trace + tag to stderr and exit 1 — instead of returning the
// tag/tuple as the raw exit code. Two shapes:
// `-> !` → `val` is the bare u32 error tag.
// `-> (int, !)` → `val` is a `{value, tag}` tuple; extract both.
if (self.current_func_is_main) {
const rinfo = self.ir_mod.types.get(func.ret);
if (rinfo == .error_set) {
self.emitFailableMainRet(null, val);
self.advanceRefCounter();
return;
}
if (rinfo == .tuple and rinfo.tuple.fields.len == 2 and
self.ir_mod.types.get(rinfo.tuple.fields[1]) == .error_set)
{
const value = c.LLVMBuildExtractValue(self.builder, val, 0, "main.ret.val");
const tag = c.LLVMBuildExtractValue(self.builder, val, 1, "main.ret.tag");
self.emitFailableMainRet(value, tag);
self.advanceRefCounter();
return;
}
}
// sret-shaped function: declared return-type-in-IR is
// the struct, but the LLVM signature is void with a
@@ -4690,13 +4704,15 @@ 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 {
/// Failable main entry-point wrapper (ERR E4.2). At the LLVM level main
/// returns i32. `tag_val` is the u32 error tag (0 = "no error"); `value` is
/// the integer value slot for a value-carrying `-> (int, !)` main, or null
/// for a pure `-> !` main. Emit the branch: tag == 0 → `ret i32 <value-or-0>`
/// (success — exit code truncated to u8 downstream); 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, value: ?c.LLVMValueRef, tag_val: c.LLVMValueRef) void {
const llvm_func = c.LLVMGetBasicBlockParent(c.LLVMGetInsertBlock(self.builder));
const tag_i32 = self.coerceArg(tag_val, self.cached_i32);
@@ -4705,9 +4721,10 @@ pub const LLVMEmitter = struct {
const err_bb = c.LLVMAppendBasicBlockInContext(self.context, llvm_func, "main.err");
_ = c.LLVMBuildCondBr(self.builder, is_err, err_bb, ok_bb);
// Success: exit 0.
// Success: exit the value (truncated to u8 by the JIT/OS) or 0.
c.LLVMPositionBuilderAtEnd(self.builder, ok_bb);
_ = c.LLVMBuildRet(self.builder, c.LLVMConstInt(self.cached_i32, 0, 0));
const ok_ret = if (value) |v| self.coerceArg(v, self.cached_i32) else c.LLVMConstInt(self.cached_i32, 0, 0);
_ = c.LLVMBuildRet(self.builder, ok_ret);
// Error: resolve the tag name, report to stderr, exit 1.
c.LLVMPositionBuilderAtEnd(self.builder, err_bb);

View File

@@ -397,9 +397,19 @@ pub const Lowering = struct {
self.needs_trace_runtime = true;
return;
}
// `-> (T..., !)` — a multi-slot tuple return; not yet wired.
// `-> (T, !)` — value-carrying failable. Accepted only for a single
// **integer** value slot (`{int, error_set}`): the wrapper extracts
// the value + tag from the returned tuple, exits `value as u8` on
// success / reports + exits 1 on error. Multi-value `-> (T1, T2, !)`
// or a non-integer value slot stays rejected — there's no single
// integer exit code to map it to.
const ti = self.module.types.get(rt);
if (ti == .tuple and ti.tuple.fields.len == 2 and self.isIntEx(ti.tuple.fields[0])) {
self.needs_trace_runtime = true;
return;
}
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` must be `-> (int, !)` (one integer value slot); got '{s}'. Use `-> !` (no value), `-> (int, !)`, or a non-failable integer return", .{self.formatTypeName(rt)});
}
return;
}