Repository navigation
kernel: core/kernel conformance wave 23 — the ARGF/IO/Process global functions, raise's backtrace argument, Class#methods, exit status (+26) - #607
Merged
Conversation
…rocess (core/kernel +20)
MRI declares a set of Kernel methods with rb_define_global_function, which
makes each a private instance method callable without a receiver AND — via
the Kernel module_function split — a public method on the Kernel module.
rbgo was missing ten of them outright, and defined #test without reflecting
the split, so `Kernel.private_instance_methods(false)` and
`Kernel.public_methods(false)` did not list them.
New, each forwarding exactly where CRuby forwards:
#gets / #readline / #readlines -> ARGF (io.c rb_f_gets, rb_f_readline,
rb_f_readlines: all `forward(argf, …)` when the receiver is not ARGF,
so a stubbed ARGF.gets is what runs)
#select -> IO.select (io.c rb_f_select)
#spawn -> Process.spawn (process.c rb_f_spawn)
#` -> runShellCommand (io.c rb_f_backquote:
arity 1, StringValue so a non-String goes through #to_str). It uses the
same runner the `%x{…}` literal uses (OpXStr) so the two spellings
cannot drift apart.
#syscall -> NotImplementedError. A CGO-free runtime
has no portable syscall(2); this is the situation CRuby is in without
HAVE_SYSCALL, where io.c ends `#define rb_f_syscall rb_f_notimplement`.
MRI 4.0.5 on darwin answers with exactly this message.
#global_variables -> the global table as Symbols (variable.c
rb_f_global_variables walks rb_global_tbl). The specials specialGvar
answers from VM state rather than the table ($!, $0, $$) are always
defined, so they are listed too; the result is sorted because Go map
iteration is randomised and MRI's own order is unspecified.
and "`", gets, global_variables, readline, readlines, select, spawn,
syscall and test added to registerKernelModuleFunctions' name list so the
visibility split covers them.
Measured differentially — every core/kernel spec run under BOTH rbgo and MRI
4.0.5 with the same shim, counting only failures unique to rbgo:
core/kernel 1847 -> 1867 passing, unique-to-rbgo failures 268 -> 248.
…it/abort status (+6) Three Kernel defects found by running every core/kernel spec under BOTH rbgo and MRI 4.0.5 with the same shim and keeping only the failures unique to rbgo. Kernel#raise(class, message, backtrace) The third argument was read and discarded, so the raised exception carried the current stack instead. MRI's make_exception ends `if (argc == 3) set_backtrace(mesg, argv[2])` (eval.c), applying it before the raise; set_backtrace routes the value through the exception's own #set_backtrace (so an override runs) and accepts Thread::Backtrace::Location values, converting them with rb_backtrace_to_str_ary when it cannot store locations. rbgo never stores locations — a backtrace is an Array of String and #backtrace_locations re-derives Locations by parsing them back — so Location elements are converted the same way here, and because a Location's #to_s is the line it was parsed from the round trip is exact. Other non-String elements are passed through so #set_backtrace still raises MRI's TypeError. Kernel#methods on a class or module rb_obj_methods lists the instance methods of CLASS_OF(obj) (object.c). For a class that is its SINGLETON class, so `C.methods` carries C's class methods and its superclasses' as well as Class's instance methods. rbgo's classOf answers Class for an RClass and cannot see the singleton chain, so `C.methods` listed no class method at all — `C.methods & C.methods(false)` was empty. Both halves are now gathered and merged. Kernel#exit / #exit! / #abort The status argument was discarded: every call raised SystemExit with status 0, and a bad argument raised nothing. MRI's rb_f_exit (process.c) takes 0..1 arguments through exit_status_code — true is EXIT_SUCCESS, false is EXIT_FAILURE, anything else NUM2INT, so a Float truncates and a String/nil/Array is a TypeError — and raises SystemExit.new(status, "exit"). exit! differs only in defaulting to EXIT_FAILURE. abort puts its message (through StringValue, so #to_str converts) on stderr and raises SystemExit.new(EXIT_FAILURE, message). This was a real loss, not only a spec gap: the code's own comment names Puppet's exit_on_fail, which reads the code back off the SystemExit. ruby/spec, whole language+core suite, per file against origin/main: 20093 -> 20119 passing, no file losing an example. core/kernel 1847 -> 1870; also core/exception/status +1, core/exception/success +1 (exit status), core/module/include +1 (Class#methods).
Covers every branch added by the two preceding commits, each expectation
read off ruby 4.0.5:
registerKernelDelegates - the module_function visibility split for all
nine names; ARGF forwarding for #gets/#readline/#readlines (stubbed,
so no real input stream); #select against IO.pipe (nil on nothing
ready, the [r, w, e] triple otherwise); #spawn forwarding without
starting a subprocess; #syscall's NotImplementedError; #` through
#send (rbgo's parser rejects a backquote after a dot) including the
#to_str, arity and TypeError arms; #global_variables including the
alwaysDefinedGvars branch, the +1 on a fresh global and the sort.
exitStatusArg / raiseSystemExit - true/false/Integer/Float/#to_int,
exit vs exit! defaults, Kernel.exit, the three TypeError arms and the
arity error; abort's status, message, #to_str and stderr write.
applyRaiseBacktrace - String, Array of String, empty, nil, a Location
array round-tripping through #backtrace_locations, coexistence with
cause:, and the TypeError for a non-String element.
mergeMethodNames - a class's own, meta and inherited class methods plus
Class's instance methods, each listed once and sorted, with private
class methods and instance methods excluded, and the module and
plain-object receivers.
The backquote test is guarded by runtime.GOOS (it needs a POSIX shell).
tannevaled
added a commit
that referenced
this pull request
Sep 12, 2026
…611) Three units landed in this wave: #607 core/kernel: the Kernel global functions that forward to ARGF/IO/ Process, raise's backtrace argument, Class#methods' singleton chain, exit/abort status (+26) #608 deps: go-ruby-parser v0.1.9, which honours a backslash inside a %w/%W/%i/%I word list — an escaped space no longer splits a word, an escaped delimiter no longer ends the list (+30 measured over core/dir + core/file alone) #609 core/io: IO#reopen, IO.popen, IO#fcntl and Fcntl, IO.select argument checks, a faithful IO#read_nonblock with WaitReadable/WaitWritable, set_encoding(nil, nil) (+92) Measured with scripts/conformance/rubyspec/run.sh against the pinned corpus at 87b1631, 16-way, TIMEOUT=25: 20244 passing (2173 files loaded, 33 failed to load). CI runs the same ratchet with TIMEOUT=60, so its total can only be greater than or equal to this one. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Wave-23
core/kernelconformance against MRI 4.0.5. Three commits, confined tointernal/vm/builtins.go(Kernel methods only) andinternal/vm/kernel_introspect.go,plus one new whitebox test file.
ruby/spec, whole
language+coresuite, per file againstorigin/main:20093 → 20119 passing, no file losing an example.
core/kernelalone: 1847 → 1870.Method
Everything here was found with a differential harness: each
core/kernelspecruns under BOTH
rbgoand the realruby4.0.5 with the same MSpec shim, and onlythe failures unique to rbgo are treated as defects. The pinned corpus reports
Ruby 3.4.1, so a number of specs expect messages MRI 4.0.5 no longer produces —
those show up as failures under both engines and were skipped rather than
"fixed" into a divergence from MRI.
Unique-to-rbgo failing examples in
core/kernel: 268 → 245.Every behavioural claim below was read out of the CRuby source (cited per commit)
and then checked byte-for-byte against
ruby4.0.5 on the same input.1. The Kernel global functions that forward to ARGF / IO / Process (+20)
MRI declares a set of Kernel methods with
rb_define_global_function, which makeseach a private instance method callable without a receiver AND — through the
Kernel
module_functionsplit — a public method on the Kernel module. rbgo wasmissing ten of them outright and defined
#testwithout reflecting the split, soneither
Kernel.private_instance_methods(false)norKernel.public_methods(false)listed them.
New, each forwarding exactly where CRuby forwards:
#gets#readline#readlinesrb_f_gets,rb_f_readline,rb_f_readlines— allforward(argf, …)ARGF, by dispatch, so a stubbedARGF.getsis what runs#selectrb_f_selectIO.select#spawnrb_f_spawnProcess.spawn#`rb_f_backquote— arity 1,StringValueso a non-String goes through#to_strrunShellCommandthe%x{…}/backtick literal uses (OpXStr), so the two spellings cannot drift apart#syscall#define rb_f_syscall rb_f_notimplementwithoutHAVE_SYSCALLNotImplementedErrorwith MRI's exact message — which is what MRI 4.0.5 on darwin answers#global_variablesrb_f_global_variableswalksrb_global_tblspecialGvarserves from VM state rather than the table ($!,$0,$$), sorted because Go map iteration is randomised and MRI's own order is unspecifiedand
"`",gets,global_variables,readline,readlines,select,spawn,syscall,testadded toregisterKernelModuleFunctions' name list sothe visibility split covers them.
#selectand#spawnreach rbgo's single implementation by dispatch, whereMRI has a separate C entry point over a shared body. The trade is deliberate: a
program that redefines
IO.selectalso changesKernel#select, in exchange forthe two never being able to diverge. The tests pin that property.
2. Three Kernel defects (+6)
Kernel#raise(class, message, backtrace)— the third argument was read anddiscarded, so the exception carried the current stack instead. MRI's
make_exceptionendsif (argc == 3) set_backtrace(mesg, argv[2])(eval.c),applying it before the raise;
set_backtraceroutes the value through theexception's own
#set_backtrace(so an override runs) and acceptsThread::Backtrace::Locationvalues, converting them withrb_backtrace_to_str_arywhen it cannot store locations. rbgo never stores locations — a backtrace is an
Array of String and
#backtrace_locationsre-derives Locations by parsing themback — so Location elements are converted the same way, and because a Location's
#to_sis the line it was parsed from the round trip is exact. Other non-Stringelements pass through so
#set_backtracestill raises MRI'sTypeError.Kernel#methodson a class or module —rb_obj_methodslists the instancemethods of
CLASS_OF(obj)(object.c). For a class that is its singleton class,whose ancestry runs through the singleton classes of its superclasses and only then
reaches
Class/Module/Object. rbgo'sclassOfanswersClassfor anRClassand cannot see the singleton chain, so
C.methodslisted no class method atall —
C.methods & C.methods(false)was empty. Both halves are now gathered andmerged (de-duplicated, sorted).
Kernel#exit/#exit!/#abort— the status argument was discarded: everycall raised
SystemExitwith status 0, and a bad argument raised nothing. MRI'srb_f_exit(process.c) takes 0..1 arguments throughexit_status_code—trueisEXIT_SUCCESS,falseisEXIT_FAILURE, anything elseNUM2INT, so a Floattruncates and a String/nil/Array is a
TypeError— and raisesSystemExit.new(status, "exit").exit!differs only in defaulting toEXIT_FAILURE.abortputs its message (throughStringValue, so#to_strconverts) on stderr and raises
SystemExit.new(EXIT_FAILURE, message).This one was a real loss, not only a spec gap: the code's own comment names
Puppet's
exit_on_fail, which callsexit(code)and reads the code back off theSystemExit. It also pays off outsidecore/kernel—core/exception/status_specandcore/exception/success_speceach gain anexample.
3. Tests
One new file,
internal/vm/kernel_delegates_wave23_internal_test.go, coveringevery branch added above; each expectation was read off
ruby4.0.5. Thebackquote test is guarded by
runtime.GOOS(it needs a POSIX shell).Gates
go build ./...,go vet ./..., fullgo test ./...— green.(
-coverpkg=$(go list ./internal/...) ./...) on this branch and onorigin/main: the list of functions below 100% is byte-identical betweenthe two, and every function added here is at 100%. Nothing that was 100% before
is below it now.
gofmt -l internal/vm/— empty.GOOS=wasip1 GOARCH=wasmbuild, vet andgo test -c— all succeed.FLOORdeliberately not raised here— left for whoever lands the wave.
Measurement notes
Two files in the corpus are pre-existing flakes, on this branch and on
origin/mainalike — sampled repeatedly on both binaries before concludinganything from them:
core/random/bytes_spec.rbflips 8 ↔ 9,core/kernel/require_spec.rbflips 26 ↔ 27.Neither is touched by this change.
Left on the table — found, located, out of this PR's file scope
Reported rather than fixed, because each lives outside the files this wave owns.
All were localised precisely:
Kernel#sleepwith no argument deadlocks (thread.go:766— a channelreceive nothing ever sends to). It is not a hang in the spec's logic: Go's
runtime aborts with
fatal error: all goroutines are asleep, so noRBGO_RESULTline is printed and the whole file scores 0. This alone costs~20 examples:
core/kernel/exit_spec.rb(12 under MRI) andcore/process/exit_spec.rb(8) both fail to load for this single reason.Fixing it would make this PR's
exitstatus work visible as well.trace_var/untrace_var/set_trace_func(~10 examples): the hookpoint is
setGVaringlobals.go, the single chokepointvm.go:1578calls.Exception#to_sfor an anonymous class (1 example, one word): it usesclass.name, which is""forClass.new(Exception); MRI usesclass.to_s→
#<Class:0x…>.toIntCoerce(converter_impl.go): MRI has a special message for nil —no implicit conversion from nil to integer, notof nil into Integer. Thatis the only line on which this PR's
exitdiffers from MRI.`echo #{ip}`runsecho— thecompiler puts
v.Commandin the name pool verbatim (compiler.go:360), soOpXStrnever sees the interpolated parts.Kernel.` is unparsable:parse error: expected method name after '.'.binding/block_given?/local_variables(~14 examples): the first twoare compiler intrinsics with no method record, and natives get no access to the
caller's env, so none can be given a real body without a VM change.
require/require_relative/load/evalare 117 of the 245 remainingunique-to-rbgo failures, almost all path-resolution in
require.go. That iswhere the next large
core/kernelwin is.kind_of?was left as two method records on purpose: the code carries aprior wave's note that sharing one record to satisfy the
==alias examplewould regress four reflection examples.
🤖 Generated with Claude Code