Files
sx/issues/0137-jit-run-no-main-segfault.md
agra f8e029d719 feat(asm): Phase A.1 — parse asm { … } into AsmExpr; loud lowering bail
`asm volatile? { "tmpl", [name]? "constraint" (-> Type | = expr), …,
clobbers(.…) }` now parses into a flat-operand AsmExpr/AsmOperand (ast.zig +
parser.zig parseAsmExpr, dispatched from parsePrimary on .kw_asm). `volatile`
and `clobbers` are recognized contextually (not reserved). `-> @place`
write-through is rejected with a clear "Phase 2" parse error.

Codegen is not implemented yet (IR op + LLVM emit are Phases C–E), so lowering
bails LOUD + named via an explicit .asm_expr arm in lower/expr.zig (not the
generic unknown_expr else) — emitPlaceholder makes hasErrors() abort the build
on the message.

The new asm_expr tag forced (and got) arms in three exhaustive Node.Data
switches: sema.zig analyzeNode + findNodeAtOffset, semantic_diagnostics.zig
checkBindingNames — each recurses into template + operand payloads.

Design: adopted the operand auto-naming rule (design §II.5) — name auto-derived
from a {reg} pin, explicit [name] only when it differs or for register-class
operands, echo form rejected. Typing-stage rule; parser stores name: ?[]const u8.

Locked with examples/1640-platform-asm-parse.sx (multi-output divmod: named
operands, register pins, clobbers — parses then bails, called from main).

Also files issue 0137 (pre-existing, orthogonal: `sx run` with no `main`
segfaults via an unguarded JIT entry lookup in target.zig — not an asm bug).

zig build test green (648 corpus, 445 unit).
2026-06-15 20:21:25 +03:00

3.0 KiB
Raw Permalink Blame History

0137 — sx run on a program with no main segfaults (JIT entry lookup unguarded)

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 main entry 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 256273. 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 run on a program with no main segfaults instead of diagnosing. The JIT run path in src/target.zig (~lines 256273) looks up "main" via LLVMOrcLLJITLookup, then unconditionally casts main_addr to a function pointer and calls it. When the program defines no main, main_addr is 0 (or the lookup degenerately "succeeds"), so the call dereferences null and crashes.

Fix: after the lookup's err check, add if (main_addr == 0) { … } that emits a clean user-facing error ("no main function found" / "program has no entry point") and returns error.CompileError (matching the existing JIT lookup error style), BEFORE the @ptrFromInt + call. Consider whether a pre-JIT check (the module/program already knows whether a main decl exists — e.g. emit_llvm.zig:631 already null-checks LLVMGetNamedFunction(.., "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 under issues/ (or an examples/11xx-diagnostics-* once the message is settled) asserting the diagnostic on stderr + the exit code.