Found by the iteration-192 evaluator while reviewing #607 (PR #690), and confirmed first-party.
The gap
exit() raises a *eval.EvalExitCode sentinel panic (internal/effects/io.go:145). Two consumers recover it:
internal/embed — the sanctioned dashboard → compiler bridge — does not. Engine.Call (embed.go:237) and Engine.CallPreserveFloats (embed.go:314) call runtime.CallEntrypoint directly.
Measured:
grep -c 'recover()' internal/embed/embed.go -> 0
grep -c 'recover()' cmd/ailang/run_helpers.go -> 2 # known-positive control, same instrument
test -f internal/embed/embed.go -> YES # scope control
So an embedded module that calls exit() panics the host process with a raw Go stack — the same defect class as #607, one layer down. For a long-lived host (the dashboard/observatory), that is a crash rather than a bad exit code.
Why it was not fixed in #690
#607 is specifically about ailang run --batch, and the call-site census there was correctly scoped to executeModuleEntrypoint (exactly two sites, both now recovered). runtime.CallEntrypoint is the broader primitive, and widening the fix into the embed layer needs its own decision: an embedding host has no os.Exit semantics to map onto, so the right contract is probably to surface exit(N) as a typed error to the caller rather than to recover-and-ignore it.
Suggested acceptance
- Decide the contract: typed error (e.g.
ErrProgramExit{Code}) returned from Call/CallPreserveFloats, not a panic.
- A test that embeds a module calling
exit(1) and asserts the host survives with that typed error.
- Per the mutation discipline: neutering the new recover must red that test.
Related: #607, PR #690.
Found by the iteration-192 evaluator while reviewing #607 (PR #690), and confirmed first-party.
The gap
exit()raises a*eval.EvalExitCodesentinel panic (internal/effects/io.go:145). Two consumers recover it:cmd/ailang/main_run_exec.go:552-568— the single-file run pathcmd/ailang/run_helpers.go(recoverBatchItemExit) — the batch path, added by [cli] Batch mode: a module calling exit() inside a ba... #607internal/embed— the sanctioned dashboard → compiler bridge — does not.Engine.Call(embed.go:237) andEngine.CallPreserveFloats(embed.go:314) callruntime.CallEntrypointdirectly.Measured:
So an embedded module that calls
exit()panics the host process with a raw Go stack — the same defect class as #607, one layer down. For a long-lived host (the dashboard/observatory), that is a crash rather than a bad exit code.Why it was not fixed in #690
#607 is specifically about
ailang run --batch, and the call-site census there was correctly scoped toexecuteModuleEntrypoint(exactly two sites, both now recovered).runtime.CallEntrypointis the broader primitive, and widening the fix into the embed layer needs its own decision: an embedding host has noos.Exitsemantics to map onto, so the right contract is probably to surfaceexit(N)as a typed error to the caller rather than to recover-and-ignore it.Suggested acceptance
ErrProgramExit{Code}) returned fromCall/CallPreserveFloats, not a panic.exit(1)and asserts the host survives with that typed error.Related: #607, PR #690.