A program with no 'main' reached the JIT entry-point call with a garbage address (ORC reports lookup success but leaves main_addr degenerate), then called it -> SIGSEGV. Add a pre-JIT entry-point check in main.zig that emits 'error: no main function found' and exits non-zero before codegen, plus a defensive main_addr==0 guard in target.zig runJITFromObject as a backstop. Regression: examples/1188-diagnostics-run-no-main.sx
3.5 KiB
0137 — sx run on a program with no main segfaults (JIT entry lookup unguarded)
RESOLVED. A pre-JIT entry-point check in
main.zignow emits a cleanerror: no 'main' function found …diagnostic and exits non-zero before any codegen/JIT, so a no-main program never reaches the garbage-pointer call. A defensivemain_addr == 0guard intarget.zig'srunJITFromObject(ORC reports lookup success but leaves the address degenerate) remains as a backstop. Regression test:examples/1188-diagnostics-run-no-main.sx.
Symptom
sx run <file> on a program that defines no main function crashes
(SIGSEGV/abort, "Segmentation fault at address 0x60") instead of emitting a clean
diagnostic like error: no 'main' function found.
- Observed: process crash, exit 134 (abort) / 139 (SIGSEGV); no diagnostic.
- Expected: a normal compile-style error ("no
mainentry point") and a clean non-zero exit, the same way any other missing-entry condition reports.
Independent of inline assembly — surfaced while writing an ASM-stream probe that
omitted main, but reproduces with an ordinary, asm-free program (see below).
Reproduction
A file with only an (uncalled) function and no main:
foo :: (n: u64) -> u64 { return n + 1; }
sx run that.sx
# => "Segmentation fault at address 0x60", exit 134
# expected: "error: no 'main' function found" (or similar), clean non-zero exit
Root cause (suspected)
src/target.zig JIT-run path, ~lines 256–273. After the ORC lookup:
var main_addr: c.LLVMOrcExecutorAddress = 0;
err = c.LLVMOrcLLJITLookup(jit, &main_addr, "main");
if (err != null) { /* prints "JIT lookup error" and returns error.CompileError */ }
// no guard for main_addr == 0 here:
const main_fn: *const fn () callconv(.c) i32 = @ptrFromInt(main_addr);
const result = main_fn(); // <- calls a null/garbage pointer when no main
When the module has no main symbol, the lookup leaves main_addr at 0 (or
ORC returns a degenerate success), so @ptrFromInt(main_addr) + main_fn()
calls into null → the crash. There is no main_addr == 0 check.
Investigation prompt (paste into a fresh session)
sx runon a program with nomainsegfaults instead of diagnosing. The JIT run path insrc/target.zig(~lines 256–273) looks up"main"viaLLVMOrcLLJITLookup, then unconditionally castsmain_addrto a function pointer and calls it. When the program defines nomain,main_addris0(or the lookup degenerately "succeeds"), so the call dereferences null and crashes.Fix: after the lookup's
errcheck, addif (main_addr == 0) { … }that emits a clean user-facing error ("nomainfunction found" / "program has no entry point") and returnserror.CompileError(matching the existingJIT lookup errorstyle), BEFORE the@ptrFromInt+ call. Consider whether a pre-JIT check (the module/program already knows whether amaindecl exists — e.g. emit_llvm.zig:631 already null-checksLLVMGetNamedFunction(.., "main")) is the better choke point so the diagnostic carries a source span rather than a bare message. Either is acceptable; the hard requirement is no crash.Verification:
printf 'foo :: (n: u64) -> u64 { return n + 1; }\n' > /tmp/x.sx && sx run /tmp/x.sx— expect a clean error message + non-zero exit, NOT a segfault. Add a pinned repro underissues/(or anexamples/11xx-diagnostics-*once the message is settled) asserting the diagnostic on stderr + the exit code.