ERR/E4.2: entry-point signature gate for main

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).
This commit is contained in:
agra
2026-06-01 07:23:31 +03:00
parent 94335f94d7
commit 6e32e6c63c
4 changed files with 62 additions and 0 deletions

View File

@@ -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