From 94335f94d7683e5de169b273471c153a6edb232b Mon Sep 17 00:00:00 2001 From: agra Date: Mon, 1 Jun 2026 01:42:53 +0300 Subject: [PATCH] ERR/E4.2: truncate integer main's return to u8 for the JIT exit code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A non-failable integer `main :: () -> T` must exit with its return value truncated to u8 (matching C main / the OS exit-status byte), so `sx run` (JIT) and an AOT binary agree. runJITMain clamped instead: any value outside 0..255 returned exit 1, so `return 1105` exited 1 (not 81), `return -1` exited 1 (not 255), and `return 256` exited 1 (not 0). Fix: bit-cast the i32 return to u32 and @truncate to u8 — negatives wrap as their two's-complement low byte rather than being clamped. The AOT path already gets OS truncation, so it was already correct; this makes JIT match. examples/238-main-exit-truncation.sx returns 1105 -> exit 81. Values <=255 (42, 200) still pass through unchanged. Gates: zig build, zig build test, bash tests/run_examples.sh (275 passed; the lone failure is the user's uncommitted 213-canonical-map pack WIP). --- examples/238-main-exit-truncation.sx | 14 ++++++++++++++ src/target.zig | 8 ++++++-- tests/expected/238-main-exit-truncation.exit | 1 + tests/expected/238-main-exit-truncation.txt | 1 + 4 files changed, 22 insertions(+), 2 deletions(-) create mode 100644 examples/238-main-exit-truncation.sx create mode 100644 tests/expected/238-main-exit-truncation.exit create mode 100644 tests/expected/238-main-exit-truncation.txt diff --git a/examples/238-main-exit-truncation.sx b/examples/238-main-exit-truncation.sx new file mode 100644 index 0000000..ea00d47 --- /dev/null +++ b/examples/238-main-exit-truncation.sx @@ -0,0 +1,14 @@ +// Entry-point exit-code truncation (ERR step E4.2, non-failable integer main). +// `main :: () -> T` (integer) exits with the return value truncated to u8 — +// matching C `main` / the OS exit-status byte, so the JIT (`sx run`) and an AOT +// binary agree. Here 1105 & 0xFF == 81, so this program exits 81 (NOT 1, which +// was the old buggy "out of 0..255 range -> failure" behavior). +// +// Run: ./zig-out/bin/sx run examples/238-main-exit-truncation.sx ; echo $? # 81 + +#import "modules/std.sx"; + +main :: () -> s32 { + print("returning 1105 -> exit {}\n", 1105 & 0xFF); // 81 + return 1105; +} diff --git a/src/target.zig b/src/target.zig index 68f2615..ab6b56b 100644 --- a/src/target.zig +++ b/src/target.zig @@ -235,10 +235,14 @@ pub fn runJITFromObject(obj_buf: c.LLVMMemoryBufferRef) !u8 { return error.CompileError; } - // Cast to function pointer and call + // Cast to function pointer and call. The exit code is main's integer + // return truncated to u8 — matching the OS truncation an AOT binary's + // exit status already gets, so JIT and AOT agree (e.g. 1105 -> 81, + // -1 -> 255, 256 -> 0). Bit-cast i32 -> u32 first so negatives wrap + // as two's-complement low byte rather than being clamped. const main_fn: *const fn () callconv(.c) i32 = @ptrFromInt(main_addr); const result = main_fn(); - return if (result >= 0 and result <= 255) @intCast(result) else 1; + return @truncate(@as(u32, @bitCast(result))); } // Android APK bundling (createApk, compileJniMainSources, diff --git a/tests/expected/238-main-exit-truncation.exit b/tests/expected/238-main-exit-truncation.exit new file mode 100644 index 0000000..d88e313 --- /dev/null +++ b/tests/expected/238-main-exit-truncation.exit @@ -0,0 +1 @@ +81 diff --git a/tests/expected/238-main-exit-truncation.txt b/tests/expected/238-main-exit-truncation.txt new file mode 100644 index 0000000..e249d3b --- /dev/null +++ b/tests/expected/238-main-exit-truncation.txt @@ -0,0 +1 @@ +returning 1105 -> exit 81