From 6e32e6c63c7d333546610fc92f61aee97e702234 Mon Sep 17 00:00:00 2001 From: agra Date: Mon, 1 Jun 2026 07:23:31 +0300 Subject: [PATCH] ERR/E4.2: entry-point signature gate for main MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add validateMainSignature (lowerRoot Pass 4a). main must take no parameters and have a single-slot return — void, an integer (POSIX exit code), or `-> !` / `-> !Named` (the error tag rides the single return register, which the JIT's `() -> i32` main call handles directly). Other shapes are now clean diagnostics instead of silent miscompiles: - `main :: () -> string` previously SEGFAULTED (the i32 return register was read as a string) — now a clear "return type must be void, an integer, or `!`" error. - `main :: (x: ...)` previously ran silently (param ignored) — now rejected. - `main :: () -> f64` / non-failable tuple / etc. — rejected. The value-carrying failable `-> (T, !)` is rejected for now: its multi-slot {value, error} return ABI-mismatches the entry-point call and segfaults. That shape needs the E4.2 entry-point wrapper (gated on E3 return traces); rejecting loudly beats miscompiling. `-> !` (no value) IS accepted — single-slot, works today (success exits 0; a raise exits nonzero, trace/tag story pending E3). examples/239-main-signature-reject.sx covers the `-> string` rejection (exit 1). Accepted shapes are exercised elsewhere (238 for integer-exit truncation; the existing suite for void/int main). Gates: zig build, zig build test, bash tests/run_examples.sh (276 passed; lone failure is the user's uncommitted 213-canonical-map pack WIP). --- examples/239-main-signature-reject.sx | 17 ++++++++ src/ir/lower.zig | 39 +++++++++++++++++++ tests/expected/239-main-signature-reject.exit | 1 + tests/expected/239-main-signature-reject.txt | 5 +++ 4 files changed, 62 insertions(+) create mode 100644 examples/239-main-signature-reject.sx create mode 100644 tests/expected/239-main-signature-reject.exit create mode 100644 tests/expected/239-main-signature-reject.txt diff --git a/examples/239-main-signature-reject.sx b/examples/239-main-signature-reject.sx new file mode 100644 index 0000000..4363de9 --- /dev/null +++ b/examples/239-main-signature-reject.sx @@ -0,0 +1,17 @@ +// 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 `-> !` +// (the error tag rides the single return register). Anything else is a clean +// diagnostic — previously `main :: () -> string` SEGFAULTED (the JIT calls main +// as `() -> i32`, so a string return is read as garbage). The value-carrying +// failable `-> (T, !)` is also rejected for now: its multi-slot return ABI- +// mismatches the entry-point call (lands with the E4.2 wrapper). Accepted +// 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 + +#import "modules/std.sx"; + +main :: () -> string { // ERROR: return type must be void, an integer, or `!` + return "not an exit code"; +} diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 0842d43..c8e4cea 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -343,6 +343,8 @@ pub const Lowering = struct { self.lowerDeferredTypeFns(); // Pass 4: target-specific entry-point sanity checks self.checkRequiredEntryPoints(); + // Pass 4a: validate main's signature (ERR E4.2 entry-point gate). + self.validateMainSignature(); // Pass 4b: eagerly lower bodied methods on sx-defined `#objc_class` // declarations. The Obj-C runtime calls these via IMP pointers // registered in M1.2 A.4 — no sx-side call path drives lazy @@ -360,6 +362,43 @@ pub const Lowering = struct { self.synthesizeJniMainStubs(); } + /// ERR E4.2: the entry-point signature gate. `main` must take no parameters + /// and have a SINGLE-slot return: void (`()` / `-> ()` / `-> void`), an + /// integer (POSIX exit code, truncated to u8), or `-> !` / `-> !Named` (the + /// error tag rides the single return register). The multi-slot + /// `-> (T, !)` tuple return is NOT yet supported — the JIT calls main as + /// `() -> i32`, so a 2-slot `{value, error}` return ABI-mismatches and + /// segfaults; that shape lands with the E4.2 entry-point wrapper. Any other + /// shape (`-> string`, `-> f64`, a non-failable tuple, …) is a clean + /// diagnostic rather than a silent miscompile. + fn validateMainSignature(self: *Lowering) void { + const fd = self.fn_ast_map.get("main") orelse return; + + if (fd.params.len != 0) { + if (self.diagnostics) |diags| { + diags.addFmt(.err, fd.params[0].name_span, "main: parameters must be empty; return type must be void, an integer, or `!`", .{}); + } + return; + } + + const rt = self.resolveReturnType(fd); + // Single-slot returns the JIT's `() -> i32` ABI handles directly: + // 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` + // `-> (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`", .{}); + } + return; + } + + if (self.diagnostics) |diags| { + diags.addFmt(.err, if (fd.return_type) |rtn| rtn.span else null, "main: return type must be void, an integer, or `!`; got '{s}'", .{self.formatTypeName(rt)}); + } + } + /// On Android, the OS loads the .so via a Java-side Activity declared /// with `#jni_main #jni_class("...")`. The Java class drives the /// lifecycle (onCreate / onPause / etc.) and sx provides the native diff --git a/tests/expected/239-main-signature-reject.exit b/tests/expected/239-main-signature-reject.exit new file mode 100644 index 0000000..d00491f --- /dev/null +++ b/tests/expected/239-main-signature-reject.exit @@ -0,0 +1 @@ +1 diff --git a/tests/expected/239-main-signature-reject.txt b/tests/expected/239-main-signature-reject.txt new file mode 100644 index 0000000..d1361a8 --- /dev/null +++ b/tests/expected/239-main-signature-reject.txt @@ -0,0 +1,5 @@ +error: main: return type must be void, an integer, or `!`; got 'string' + --> /Users/agra/projects/sx/examples/239-main-signature-reject.sx:15:15 + | +15 | main :: () -> string { // ERROR: return type must be void, an integer, or `!` + | ^^^^^^