You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
A deliberate exit status never reaches the process: rbgo -e 'exit 3' exits 0 #676
rbgo -e 'exit 3' exits 0, where MRI exits 3. Every spelling of a
deliberate non-zero exit is lost, so rbgo cannot signal failure to a shell,
a Makefile or a CI step.
Measured against MRI 4.0.5 (ruby 4.0.5 (2026-05-20) +PRISM [arm64-darwin25])
on the branch of #675:
program
MRI status
rbgo status
exit 3
3
0
exit false
1
0
exit!(4)
4
0
abort "boom"
1
0
raise "x"
1
1 (correct)
exit 0
0
0 (correct)
Process.exit(5)
5
1, NoMethodError (see below)
Process.exit!(6)
6
1, NoMethodError (see below)
The VM half is already correct — only the front-end's last step is missing
This is not a SystemExit implementation gap. Everything inside the VM behaves
as MRI does:
$ rbgo -e 'begin; exit 3; rescue SystemExit => e; puts "status=#{e.status} success=#{e.success?}"; end'
status=3 success=false # identical to MRI
$ rbgo -e 'at_exit { puts "atexit ran" }; exit 3'
atexit ran # identical to MRI; only the status differs
$ rbgo -e 'begin; exit 3; ensure; puts "ensure ran"; end'
ensure ran # identical to MRI; only the status differs
SystemExit is raised, carries the right status and success?, is rescuable,
and at_exit and ensure both run. The defect is at exactly one boundary: the
CLI never translates a terminal SystemExit into the process's exit code. MRI
does this in main() → ruby_run_node → ruby_cleanup, which calls error_handle and returns ex from the SystemExit's status
(eval.c/ruby.c, tag v3_4_0).
abort follows from the same cause: rb_f_abort raises SystemExit with
status 1 (process.c).
Second, separate defect: Process.exit is private
MRI declares exit, exit! and abort as module functions on Process
(Init_process: rb_define_module_function(rb_mProcess, "exit", ...)), so Process.exit(5) is public. rbgo only has the private Kernel copies, so the
qualified spelling raises NoMethodError. Worth fixing together, but it is a
different cause from the status loss.
What this does NOT affect
The ruby/spec ratchet is unaffected: scripts/conformance/rubyspec/run.sh
scores each file by parsing the RBGO_RESULT pass=… fail=… line the shim
prints, not by reading the child's exit status. I checked this before filing,
because a harness that read the status would have been scoring every file as a
success. Every conformance total measured so far stands.
Found while verifying which stream Kernel#abort's message reaches, in #667.
Correction to this issue's central claim, which was mine and was wrong.
I wrote: "This is not a SystemExit implementation gap. Everything inside the VM
behaves as MRI does … The defect is at exactly one boundary: the CLI never
translates a terminal SystemExit into the process's exit code."
That localisation is false. internal/vm/vm.go:1131deliberately discards it,
and says so:
// A SystemExit (Kernel#exit/abort) is a clean program termination, not a// crash: unwind to the top, run at_exit handlers, and return without an// error — so a real CLI that ends by calling exit (e.g. Puppet's// exit_on_fail) terminates quietly rather than printing a backtrace.ifvm.isSystemExit(rerr.Class) {
So no front-end change alone could have fixed it. The status was never handed out
of the VM to be translated.
Why I got it wrong, since the reasoning error is the reusable part. I probed
what Ruby code can observe: rescue SystemExit sees the right status, at_exit
runs, ensure runs. All three are true. I inferred "everything inside is correct,
therefore only the last step outside is missing" — and that does not follow. Those
probes establish that the exception is raised and unwinds correctly; they say
nothing about whether its value survives the VM's own boundary. Observability
from inside the language is not evidence about where a value is dropped on the way
out.
The comment above is also worth reading as its own lesson: the discard was a
correct decision for the problem it was written for (a CLI ending in exit should
not print a backtrace) and became wrong when the exit status started to matter.
The premise moved; the code did not.
Fixed in #693, which had to touch vm.go for exactly this reason and flagged it as
outside its granted file set.
Two further corrections from that work, both to figures in this issue's neighbour #691:
the rate is 10–20% (3/20, 3/20, 2/20 across spellings), not the 2-in-3 I
quoted from three runs;
Process.exit being private was right, and the fix makes all nine spellings
byte-identical to MRI 4.0.5 across 20 runs each.
Activity
tannevaled commentedon Sep 26, 2026
Correction to this issue's central claim, which was mine and was wrong.
I wrote: "This is not a
SystemExitimplementation gap. Everything inside the VMbehaves as MRI does … The defect is at exactly one boundary: the CLI never
translates a terminal
SystemExitinto the process's exit code."That localisation is false.
internal/vm/vm.go:1131deliberately discards it,and says so:
So no front-end change alone could have fixed it. The status was never handed out
of the VM to be translated.
Why I got it wrong, since the reasoning error is the reusable part. I probed
what Ruby code can observe:
rescue SystemExitsees the right status,at_exitruns,
ensureruns. All three are true. I inferred "everything inside is correct,therefore only the last step outside is missing" — and that does not follow. Those
probes establish that the exception is raised and unwinds correctly; they say
nothing about whether its value survives the VM's own boundary. Observability
from inside the language is not evidence about where a value is dropped on the way
out.
The comment above is also worth reading as its own lesson: the discard was a
correct decision for the problem it was written for (a CLI ending in
exitshouldnot print a backtrace) and became wrong when the exit status started to matter.
The premise moved; the code did not.
Fixed in #693, which had to touch
vm.gofor exactly this reason and flagged it asoutside its granted file set.
Two further corrections from that work, both to figures in this issue's neighbour
#691:
quoted from three runs;
Process.exitbeing private was right, and the fix makes all nine spellingsbyte-identical to MRI 4.0.5 across 20 runs each.
signal+exit: a self-directed signal becomes a Ruby exception, and a d…