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

@@ -1,12 +1,10 @@
// Entry-point signature gate (ERR step E4.2). `main` must take no parameters // Entry-point signature gate (ERR step E4.2). `main` must take no parameters
// and have a single-slot return: void, an integer (POSIX exit code), or `-> !` // and have one of: void, an integer (POSIX exit code), `-> !` (failable, no
// (the error tag rides the single return register). Anything else is a clean // value), or `-> (int, !)` (failable + integer exit code). Anything else is a
// diagnostic — previously `main :: () -> string` SEGFAULTED (the JIT calls main // clean diagnostic — previously `main :: () -> string` SEGFAULTED (the JIT
// as `() -> i32`, so a string return is read as garbage). The value-carrying // calls main as `() -> i32`, so a string return is read as garbage). Accepted
// failable `-> (T, !)` is also rejected for now: its multi-slot return ABI- // shapes are exercised elsewhere (238 integer-exit truncation, 244 `-> !`,
// mismatches the entry-point call (lands with the E4.2 wrapper). Accepted // 245 `-> (int, !)`). This file is expected to FAIL compilation (exit 1).
// shapes are exercised elsewhere (e.g. 238 for integer-exit truncation).
// This file is expected to FAIL compilation (exit 1).
// //
// Run: ./zig-out/bin/sx run examples/239-main-signature-reject.sx // Run: ./zig-out/bin/sx run examples/239-main-signature-reject.sx

View File

@@ -0,0 +1,22 @@
// Value-carrying failable main `-> (int, !)` (ERR step E4.2). The entry-point
// wrapper extracts the `{value, error}` tuple main returns: on success it exits
// with the integer value (truncated to u8, like a plain integer main); on an
// escaping error it prints the header + trace to stderr and exits 1 (the same
// reporter as the pure `-> !` form — see 244). This run takes the success path.
// Expected exit code: 64 (the returned value).
#import "modules/std.sx";
ParseErr :: error { Empty, BadDigit };
inner :: (n: s32) -> (s32, !ParseErr) {
if n == 0 { raise error.Empty; }
if n < 0 { raise error.BadDigit; }
return n * 2;
}
main :: () -> (s32, !ParseErr) {
v := try inner(32); // succeeds → v = 64
print("v = {}\n", v);
return v; // success → exit code 64
}

View File

@@ -2469,14 +2469,28 @@ pub const LLVMEmitter = struct {
.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]; const func = &self.ir_mod.functions.items[self.current_func_idx];
// Failable `-> !` main: `val` is the bare u32 error tag // Failable main: wrap the return in the entry-point reporter
// (0 = success). Wrap it in the entry-point reporter (ERR E4.2) // (ERR E4.2) — exit 0 (or the value) on success, else print the
// — exit 0 on success, else print the trace + tag to stderr and // trace + tag to stderr and exit 1 — instead of returning the
// exit 1 — instead of returning the tag as the raw exit code. // tag/tuple as the raw exit code. Two shapes:
if (self.current_func_is_main and self.ir_mod.types.get(func.ret) == .error_set) { // `-> !` → `val` is the bare u32 error tag.
self.emitFailableMainRet(val); // `-> (int, !)` → `val` is a `{value, tag}` tuple; extract both.
self.advanceRefCounter(); if (self.current_func_is_main) {
return; 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 // 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
@@ -4690,13 +4704,15 @@ pub const LLVMEmitter = struct {
return global; return global;
} }
/// Failable `-> !` main entry-point wrapper (ERR E4.2). At the LLVM level /// Failable main entry-point wrapper (ERR E4.2). At the LLVM level main
/// main returns i32; for a pure-failable main the IR `ret` carries the u32 /// returns i32. `tag_val` is the u32 error tag (0 = "no error"); `value` is
/// error tag (0 = "no error"). Emit the branch: tag == 0 → `ret i32 0` /// the integer value slot for a value-carrying `-> (int, !)` main, or null
/// (success); else resolve the tag name from the always-linked tag-name /// for a pure `-> !` main. Emit the branch: tag == 0 → `ret i32 <value-or-0>`
/// table, hand it + the tag to `sx_trace_report_unhandled` (prints the /// (success — exit code truncated to u8 downstream); else resolve the tag
/// header + return trace to stderr), and `ret i32 1`. /// name from the always-linked tag-name table, hand it + the tag to
fn emitFailableMainRet(self: *LLVMEmitter, tag_val: c.LLVMValueRef) void { /// `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 llvm_func = c.LLVMGetBasicBlockParent(c.LLVMGetInsertBlock(self.builder));
const tag_i32 = self.coerceArg(tag_val, self.cached_i32); 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"); const err_bb = c.LLVMAppendBasicBlockInContext(self.context, llvm_func, "main.err");
_ = c.LLVMBuildCondBr(self.builder, is_err, err_bb, ok_bb); _ = 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.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. // Error: resolve the tag name, report to stderr, exit 1.
c.LLVMPositionBuilderAtEnd(self.builder, err_bb); c.LLVMPositionBuilderAtEnd(self.builder, err_bb);

View File

@@ -397,9 +397,19 @@ pub const Lowering = struct {
self.needs_trace_runtime = true; self.needs_trace_runtime = true;
return; 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| { 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; return;
} }

View File

@@ -1,5 +1,5 @@
error: main: return type must be void, an integer, or `!`; got 'string' error: main: return type must be void, an integer, or `!`; got 'string'
--> /Users/agra/projects/sx/examples/239-main-signature-reject.sx:15:15 --> /Users/agra/projects/sx/examples/239-main-signature-reject.sx:13:15
| |
15 | main :: () -> string { // ERROR: return type must be void, an integer, or `!` 13 | main :: () -> string { // ERROR: return type must be void, an integer, or `!`
| ^^^^^^ | ^^^^^^

View File

@@ -0,0 +1 @@
64

View File

@@ -0,0 +1 @@
v = 64