Skip to content

core/process: resource limits, real spawn/exec argument handling, the wait family (fail+error 193 -> 40) - #617

Merged
tannevaled merged 3 commits into
mainfrom
wave25/proc
Sep 20, 2026
Merged

tannevaled merged 3 commits into
mainfrom
wave25/proc

Conversation

@tannevaled

@tannevaled tannevaled commented Sep 20, 2026 •

Copy link
Copy Markdown
Contributor

What this does

core/process was the largest unimplemented block left in the suite: pass=20, fail+error=193 on the pinned corpus. grep -rn RLIMIT internal/vm/*.go found nothing at all, and Process.spawn scored pass=0 across 45 examples because it peeled its arguments by hand and ran everything through a combined capture.

After: pass=165, fail+error=40.

area before after
setrlimit_spec 0 / 30 30 / 30
spawn_spec 0 / 45 38 / 39 (1 unreachable)
constants_spec 2 / 18 18 / 18
getrlimit_spec 2 / 9 9 / 9
exec_spec 0 / 6 6 / 6
kill_spec 0 / 12 4 / 12
clock_getres_spec 0 / 5 5 / 5
times_spec + tms/* 0 / 10 10 / 10
wait/wait2/waitall/waitpid/waitpid2/last_status/Status.wait 0 / 11 10 / 11
argv0 / warmup / maxgroups / getpgid / getpriority / uid / gid / egid / euid 6 / 31 24 / 31

Methods implemented, and the C source read for each

Everything below was read before it was written, not reconstructed.

Resource limits — process.c rlimit_resource_name2int, rlimit_resource_type, rlimit_resource_value, proc_getrlimit, proc_setrlimit.
Process::RLIMIT_*, Process::RLIM_INFINITY / RLIM_SAVED_MAX / RLIM_SAVED_CUR, Process.getrlimit, Process.setrlimit. The C gave: the uppercase-only name rule (rlimit_resource_name2int matches case-insensitively and then rejects any name that is not all-uppercase, so "core" is an ArgumentError); rb_check_string_type before NUM2INT, so #to_str is tried before #to_int and an Integer never reaches #to_str at all (T_FIXNUM/T_BIGNUM are their own switch cases); the INFINITY / SAVED_MAX / SAVED_CUR limit names matched by exact strcmp; and that proc_setrlimit converts both limits before the resource. The resource numbers come from the host kernel headers through x/sys/unix rather than being written out, because they differ per OS (RLIMIT_NPROC is 6 on Linux and 7 on Darwin).

Scheduling priority — process.c proc_getpriority / proc_setpriority. Process::PRIO_PROCESS / PRIO_PGRP / PRIO_USER, Process.getpriority, Process.setpriority.

Process groups and sessions — process.c proc_getpgid / proc_setpgid / proc_getsid. Process.getpgid, getpgrp, setpgid, setpgrp, getsid. The C also settled the message shape: all of these fail with rb_sys_fail(0) — no operation name — while the rlimit pair pass one, so Process.getpgid(2**30) says "No such process" and Process.getrlimit(-7) says "Invalid argument - getrlimit".

Signals — signal.c signm2signo and rb_f_kill. Process.kill, including the leading-- process-group form, the optional SIG prefix, "bad signal type <class>" for anything that is neither Integer nor #to_str, "unsupported signal 'SIGFOO'" (MRI prints the same text for both "FOO" and "SIGFOO"), and the return value of argc-1.

spawn / exec argument handling — process.c rb_exec_getargs, rb_check_argv, check_exec_env_i, rb_execarg_addopt, check_exec_options_i, check_exec_redirect, rb_f_spawn, rb_f_exec. The reading is what produced the behaviour:

  • the options Hash comes off the end first and the environment Hash off the front afterwards, which is why Process.spawn({}) is an ArgumentError about the command rather than a lone-Hash command;
  • check_hash rules String and Array out of the environment slot but lets anything with #to_hash in;
  • the [prog, argv0] command array's first element is the file to execute and its second is the name the child is told — getting that backwards is what made spawn(["/bin/sh", "argv_zero"], "-c", "echo $0") fail in the first draft;
  • every argument goes through StringValue then StringValueCStr, giving #to_str coercion then "string contains null byte";
  • "environment name contains a equal : FOO=", and a nil environment value unsetting the variable;
  • :pgroup's false/nil/true/0/pgid shapes, with "negative process group ID : -1", and the two different errors for a bad option key: bare "wrong exec option" for a String key, "wrong exec option symbol: nonesuch" for an unknown Symbol;
  • rb_spawn_internal's error path: a command that cannot be executed raises the errno its child's execve failed with, names the command as written rather than as resolved, and leaves $? reporting the 127 that child exited with — already reaped, so Process.wait must not find it.

Process.exec is now a module function (it was only reachable as the private Kernel#exec) and shares that argument handling.

The wait family — process.c proc_wait / proc_wait2 / proc_waitall, rb_last_status_get, rb_process_status_wait. Process.wait, wait2, waitall, last_status, Process::Status.wait, and waitpid / waitpid2 re-pointed at wait / wait2 as MRI's aliases (one C function registered twice, so Process.method(:waitpid) == Process.method(:wait) holds). Process::Status.wait answers with pid −1 for "no children" instead of raising.

Residuals — process.c rb_proc_times and the rb_cProcessTms Struct, rb_clock_getres, proc_setmaxgroups, proc_setproctitle, rb_proc_warmup, and the Process::UID / Process::GID / Process::Sys module definitions at the foot of InitVM_process. Process.times + Process::Tms, Process.clock_getres, Process.argv0 (frozen, and the same object every call, as rb_progname is), Process.setproctitle, Process.warmup, and Process.maxgroups= — which MRI keeps in a static that reads back, not the accepted-and-ignored no-op this had.

What is now real rather than modelled

The environment the child sees (including :unsetenv_others), its working directory, the program lookup against the child's PATH — os/exec would otherwise search the parent's, which is a different search and is what made the env['PATH'] example fail — the files :out and :err name, and the errno of a command that cannot be run. The execution itself stays synchronous, which is the model the rest of spawn.go has always had.

What I judged unreachable, and why

These are left failing on purpose. An honest 165 is worth more than a fabricated 193.

  • kill_spec, 8 examples — every one drives ProcessSpecs::Signalizer, which starts a real ruby in a thread and reads its pid from a file. ruby_exe is unsupported by the corpus shim, so the thread dies and the pid stays nil; the examples then error on Process.kill(sig, nil) whatever kill does. They need a real fork.
  • abort_spec, 6 examples — blocked on IOStub, an mspec helper the corpus shim does not define. Nothing to do with Process.abort; spec_helper.rb is another agent's file this wave.
  • uid= / euid= / gid= / egid= / groups= / initgroups, 9 examples — these mutate this interpreter's own credentials. There is no child process to confine the change to in a synchronous model, and a spec run must not change the identity of the shell it was started from. Implementing them would also mean a test that calls setuid/setgroups for real on every CI run. I deliberately did not implement a version that raises EPERM without attempting the syscall: that would pass the specs by fabricating the result.
  • detach_spec, 4 examples — Process.detach must return a Process::Waiter, a live Thread subclass carrying the child's status as its value. Two of the four are pure argument validation, but implementing half of it to collect those would leave a Process.detach that answers with the wrong kind of object. Left for a wave that can do the thread properly — and the previous wave's File.mkfifo deadlock is a standing reason not to add thread machinery in a hurry.
  • fork_spec 2, _fork_spec 1, daemon_spec 1 — (corrected: an earlier revision of this section had the direction of this backwards.) Measured, not inferred: rbgo answers Process.respond_to?(:fork) == false and MRI answers true, so rbgo takes the specs' guard_not branch. What fails inside it is that rbgo does define Kernel#fork as a private method — the Puppet fork/exec idiom this file is built on — so respond_to?(:fork, true) is true where the spec wants false, and Process.fork raises NoMethodError: private method 'fork' called where it wants NotImplementedError. Defining Process._fork / Process.daemon to raise NotImplementedError would fix one example in each file and break the respond_to? example beside it — net zero. Closing them properly needs MRI's rb_f_notimplement: a method that is callable but invisible to respond_to?, which lives in object_model.go, another agent's file this wave.
  • spawn_spec 1, wait_spec 1 — need ruby_cmd / a genuinely concurrent child.
  • last_status_spec 1 — $? is thread-local in MRI; it is one global here.
  • set_proctitle_spec 1 — rewriting the real process title needs the argv area the C runtime owns, which a CGO_ENABLED=0 Go program cannot reach. Process.setproctitle returns the title and leaves $0 alone, as documented, but ps keeps showing the original command.
  • clock_gettime_spec 2 — an unrelated missing unit check and a missing kernel_version_is guard in the shim.

Judgement calls worth a second pair of eyes

getpriorityFailed (process.go). getpriority(2) legitimately returns −1 — a nice value of −1 — which at Darwin's libc boundary is indistinguishable from failure: Go's wrapper decides by the return value and then reports whatever errno happened to hold, so Process.getpriority(PRIO_USER, uid) came back with a stale Errno::ETIMEDOUT on this host. MRI has the same call and the same −1 and resolves it by clearing errno before and testing it after (proc_getpriority); Go cannot reach errno, so a −1 result is believed unless the reported errno is one getpriority(2) is documented to set. The rule lives in shared code, not behind a build tag, so both POSIX lanes cover all four of its branches.

Process.clock_getres(CLOCK_MONOTONIC) prints 1e-06 where MRI prints 1.0e-06. Same value; this VM's Float#inspect drops the .0 before an exponent (p 0.000001 diverges the same way). That is a core Float-formatting divergence in internal/object, not a clock one, and outside this lane's files.

Linux fix after the first CI run (third commit)

The first push was red on both Linux arch lanes and green on macOS and wasm. Process.setrlimit(:CORE, *Process.getrlimit(:CORE)) raised TypeError: can't convert Object to Integer (Object#to_int gives Object).

Established from the source rather than guessed: configure.ac's RUBY_REPLACE_TYPE(rlim_t, [int long "long long"], RLIM, ...) resolves rlim_t to unsigned long long on both platforms, so RLIM2NUM is ULL2NUM and NUM2RLIM is NUM2ULL — and ULL2NUM of anything above LONG_MAX is a Bignum. That is why process.c's own doc example reads Process.getrlimit(:CORE) # => [0, 18446744073709551615]. Darwin cannot see it: its RLIM_INFINITY is 0x7fff_ffff_ffff_ffff, which fits an int64.

I reproduced it on macOS by feeding the Linux value through the getrlimit seam, which printed the identical error — and that showed the diagnosis is narrower than "an element is not an Integer": getrlimit was already correct, returning [0, 18446744073709551615] with both elements answering is_a?(Integer), byte-identical to MRI on Linux. The defect was entirely on the way back in — rlimitResourceValue went through procToInt (NUM2INT, int64), which rejected the *object.Bignum; the Object in the message is classNameOf not knowing that type.

  • rlimToUint now implements NUM2RLIM: a Bignum within uint64 taken at full width, one beyond it a RangeError naming the C type MRI names (bignum too big to convert into 'unsigned long long'), and a negative limit wrapping as the cast does — MRI takes setrlimit(:CORE, -1, -1) to the kernel, which answers EPERM.
  • procToInt reports a Bignum as RangeError: bignum too big to convert into 'long', which is what NUM2INT/NUM2PIDT do, rather than a TypeError. Any Bignum reaching it is out of int64 range by construction, since object.NormInt demotes everything that fits.

All three messages checked against MRI 4.0.5. TestProcessRlimitInfinityRoundTrip pins the Linux representation on every platform through the seam and asserts the round trip is a no-op, so a Darwin-only run cannot miss this again; TestProcessRlimitSymbolicMatchesTable keeps the real host table checked separately, since a faked getrlimit cannot speak for it. This also unblocks setrlimit_spec's 30 examples on Linux, every one of which is a getrlimit -> setrlimit round trip.

Gates

  • CGO_ENABLED=0 go build ./..., plus GOOS=wasip1 GOARCH=wasm, GOOS=windows, GOOS=linux GOARCH=amd64 and GOOS=linux GOARCH=arm64 — all clean; and go vet for both Linux arches, which type-checks the test files for the lanes that failed. The platform split follows the existing spawn_native / spawn_windows / spawn_wasm pattern: the kernel tables and the syscall wrappers are the only things behind a build tag (procPosix is a const the non-POSIX variants set to false, so Process.respond_to?(:getrlimit) is false there exactly as MRI's rb_f_notimplement makes it), and every coercion and error-shaping branch is in the shared file.
  • CGO_ENABLED=0 GOWORK=off go test ./... — full suite green.
  • gofmt -l internal/vm/ — empty. go vet clean.
  • internal/vm coverage unchanged at 99.9%, with every new function at 100% and the below-100% list byte-identical to the pre-change one. The one piece of dead code the rewrite left behind (writeSpawnOutput) is deleted rather than left uncovered.
  • No spec file's runtime changed materially and nothing times out: the whole core/process sweep runs in the same ~45s it did before.

🤖 Generated with Claude Code

tannevaled and others added 2 commits September 20, 2026 18:27
Implements the POSIX half of the Process module that rbgo had no trace of
(`grep RLIMIT internal/vm` found nothing before this change):

  - Process::RLIMIT_*, Process::RLIM_INFINITY / RLIM_SAVED_MAX /
    RLIM_SAVED_CUR and Process::PRIO_PROCESS / PRIO_PGRP / PRIO_USER,
    taken from the host kernel headers through x/sys/unix rather than
    written out, since the numbers differ per OS (RLIMIT_NPROC is 6 on
    Linux and 7 on Darwin).
  - Process.getrlimit / setrlimit, with the full designator coercion.
  - Process.getpriority / setpriority.
  - Process.getpgid / getpgrp / setpgid / setpgrp / getsid.
  - Process.kill.

Read for this (ruby/ruby v3_4_0):
  - process.c rlimit_resource_name2int, rlimit_resource_type,
    rlimit_resource_value, proc_getrlimit, proc_setrlimit - the
    uppercase-only name rule, the #to_str-before-#to_int order, the
    INFINITY / SAVED_MAX / SAVED_CUR limit names, and the fact that
    proc_setrlimit converts both limits before the resource.
  - process.c proc_getpgid / proc_setpgid / proc_getsid /
    proc_getpriority / proc_setpriority, including that all of them fail
    with rb_sys_fail(0) (no operation name in the message) while the
    rlimit pair pass one.
  - signal.c signm2signo and rb_f_kill - the leading '-' process-group
    form, the optional SIG prefix, "bad signal type <class>", and the
    return value of argc-1.
  - the ruby/spec expectations under core/process/{constants,getrlimit,
    setrlimit,kill,getpgid,getpriority}_spec.rb.

Platform split follows the existing spawn_native/spawn_windows/spawn_wasm
pattern: the kernel tables and the syscall wrappers are the only things
behind a build tag, every coercion and error-shaping branch is in the
shared process.go so the POSIX lanes cover all of it.

One judgement call is documented in getpriorityFailed: getpriority(2)
legitimately returns -1, which Darwin's libc boundary cannot distinguish
from failure, so Go's wrapper reports a stale errno. MRI clears errno
before the call and tests it after (proc_getpriority); Go cannot read
errno, so a -1 result is believed unless the reported errno is one
getpriority(2) documents.

core/process on the pinned corpus: constants 2 -> 18 passing, getrlimit
2 -> 9, setrlimit 0 -> 30, kill 0 -> 4, getpgid 0 -> 3, getpriority
0 -> 4. Verified byte-for-byte against MRI 4.0.5 for every method and
every error message.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…d argv0

Process.spawn scored pass=0 on 45 examples because it peeled its
arguments by hand and ran everything through a combined capture. This
replaces that with MRI's own argument model and adds the surface the
rest of core/process was blocked on.

Read for this (ruby/ruby v3_4_0 process.c unless noted):

  - rb_exec_getargs: the options Hash comes off the END first, the
    environment Hash off the FRONT afterwards - which is why
    `Process.spawn({})` is an ArgumentError about the COMMAND, and why
    check_hash rules String and Array out of the environment slot while
    letting anything with #to_hash in.
  - rb_check_argv: the [prog, argv0] command array - its FIRST element
    is the file to execute and its second is the name the child is told,
    it must hold exactly two elements, and every argument goes through
    StringValue then StringValueCStr (#to_str, then "string contains
    null byte").
  - check_exec_env_i: "environment name contains a equal", the null-byte
    rejection on keys and values, and a nil value unsetting a variable.
  - rb_execarg_addopt / check_exec_options_i: the option keys, :pgroup's
    false/nil/true/0/pgid shapes with "negative process group ID : -1",
    and the two distinct errors - "wrong exec option" for a String key,
    "wrong exec option symbol: x" for an unknown Symbol.
  - check_exec_redirect: :out/:err as a String, a [name, mode] Array, an
    IO, an object with #to_io, a descriptor number, :close and
    [:child, :out].
  - rb_f_spawn / rb_f_exec, and rb_spawn_internal's error path: a
    command that cannot be executed reports the errno its child's execve
    failed with, naming the command as WRITTEN, and leaves $? at the 127
    that child exited with - already reaped, so Process.wait must not
    find it.
  - proc_wait / proc_wait2 / proc_waitall and the waitpid/waitpid2
    aliases (one C function registered twice, so Method#== holds),
    rb_last_status_get, and rb_process_status_wait's pid -1 for "no
    children".
  - rb_proc_times and the rb_cProcessTms Struct; rb_clock_getres;
    proc_setmaxgroups, which is a process-local tunable, not the
    accepted-and-ignored no-op this had; proc_setproctitle;
    rb_proc_warmup; and the Process::UID / Process::GID / Process::Sys
    identity modules.
  - the ruby/spec expectations under core/process/.

What is now real rather than modelled: the environment the child sees
(including :unsetenv_others), its working directory, the program lookup
against the CHILD's PATH rather than the parent's - os/exec would
otherwise search the wrong one - the files :out and :err name, and the
errno of a command that cannot be run. The execution itself stays
synchronous, as the rest of this file has always been.

core/process on the pinned corpus, this commit and the previous one
together: pass 20 -> 165, fail+error 193 -> 40. Every method and every
error message verified byte-for-byte against MRI 4.0.5. Full suite green
on darwin; builds clean for GOOS=wasip1, windows and linux; internal/vm
coverage unchanged at 99.9% with every new function at 100%.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both Linux arch lanes failed TestProcessRlimitCoercion with

    TypeError: can't convert Object to Integer (Object#to_int gives Object)

on `Process.setrlimit(:CORE, *Process.getrlimit(:CORE))`. Darwin cannot
see it: its RLIM_INFINITY is 0x7fff_ffff_ffff_ffff, which fits an int64.

What it actually is, established rather than guessed. configure.ac's
RUBY_REPLACE_TYPE(rlim_t, [int long "long long"], RLIM, ...) resolves
rlim_t to "unsigned long long" on both platforms, so RLIM2NUM is ULL2NUM
and NUM2RLIM is NUM2ULL. ULL2NUM of a value above LONG_MAX yields a
BIGNUM, which is why process.c's own example reads

    Process.getrlimit(:CORE) # => [0, 18446744073709551615]

Reproduced here by feeding the Linux value through the getrlimit seam on
Darwin, which printed the same error. That also showed the diagnosis is
narrower than "an element is not an Integer": getrlimit was already
right, returning [0, 18446744073709551615] with BOTH elements answering
is_a?(Integer) — byte-identical to MRI on Linux. The defect was entirely
on the way back in. rlimitResourceValue went through procToInt (NUM2INT,
int64), which rejected the *object.Bignum as a non-integer; the "Object"
in the message is classNameOf not knowing that type.

Fixes:

  - rlimToUint implements NUM2RLIM properly: a Bignum within uint64 is
    taken at full width, one beyond it is a RangeError naming the C type
    MRI names ("bignum too big to convert into 'unsigned long long'"),
    and a negative limit wraps as the cast does — MRI hands
    setrlimit(:CORE, -1, -1) to the kernel, which answers EPERM.
  - procToInt reports a Bignum as a RangeError ("bignum too big to
    convert into 'long'"), which is what NUM2INT/NUM2PIDT do, instead of
    a TypeError. Any Bignum reaching it is out of int64 range by
    construction, since object.NormInt demotes everything that fits.

All three messages checked against MRI 4.0.5.

TestProcessRlimitInfinityRoundTrip pins the Linux representation on
every platform through the seam and asserts the round trip is a no-op,
so this cannot regress from a Darwin-only run again.
TestProcessRlimitSymbolicMatchesTable keeps the real host table checked
separately, since a faked getrlimit cannot speak for it.

This also unblocks setrlimit_spec's 30 examples on Linux, every one of
which is a getrlimit -> setrlimit round trip.

Gates: full go test ./... green; gofmt clean; go vet clean for darwin,
linux/amd64 and linux/arm64 (which type-checks the test files for the
lanes that failed); builds clean for wasip1, windows, linux/amd64,
linux/arm64. internal/vm coverage 99.9%, unchanged, every new function
at 100%.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@tannevaled
tannevaled merged commit 81d2498 into main Sep 20, 2026
7 checks passed
@tannevaled
tannevaled deleted the wave25/proc branch September 20, 2026 18:12
tannevaled added a commit that referenced this pull request Sep 21, 2026
#617 pinned `p Process.clock_getres(Process::CLOCK_MONOTONIC)` to "1e-06" and
its comment said why: the value matched MRI but the spelling did not, because
Float#inspect dropped the ".0" before the exponent. That is the divergence this
branch fixes, so the pinned value becomes MRI's — `ruby -e 'p
Process.clock_getres(Process::CLOCK_MONOTONIC)'` prints 1.0e-06 here.

The case only surfaced after #617 merged: this branch was cut before it, so the
full suite was green when the float change was written and red once the two met.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
tannevaled added a commit that referenced this pull request Sep 21, 2026
…11) (#622)

* object: lay out a Float the way MRI does, not the way Go's %g does

Float#to_s formatted with strconv.FormatFloat(v, 'g', -1, 64) and appended
".0" only when the result held no '.', 'e' or 'E'. That diverged from MRI in
two independent ways.

The mantissa lost its fraction in exponential notation, because the ".0" was
only ever appended in fixed notation:

    1e-06    MRI 1.0e-06     rbgo 1e-06
    1e+20    MRI 1.0e+20     rbgo 1e+20
    5e-324   MRI 5.0e-324    rbgo 5e-324

And the fixed/exponential threshold was Go's, not Ruby's. Go's 'g' verb
compares the exponent against the number of significant digits; MRI's flo_to_s
(numeric.c) switches on where the decimal point falls — exponential iff
decpt < -3 || decpt > DBL_DIG (15). Measured against ruby 4.0.5, the
boundaries are exactly:

    1e14   100000000000000.0        decpt 15, fixed
    1e15   1.0e+15                  decpt 16, exponential
    1e-4   0.0001                   decpt -3, fixed
    1e-5   1.0e-05                  decpt -4, exponential

The visible symptom was 1e6: MRI prints 1000000.0, rbgo printed 1e+06.

Take the shortest round-tripping digits and the exponent from 'e' with
precision -1, decide the layout by MRI's rule, and in exponential notation
give the mantissa a fractional digit. Go's exponent padding already matches
(e+06, e-05, e+100, e-323), so nothing there changes.

Verified beyond the pinned cases: 200 pseudorandom floats spanning 10^-20 to
10^20 render byte-identically to ruby 4.0.5. Sweeping core/float, core/kernel
and language with the pre- and post-fix binaries against the same corpus:
3580 -> 3591 passing, 542 -> 531 failing.

Closes #619.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test: clock_getres now prints MRI's spelling

#617 pinned `p Process.clock_getres(Process::CLOCK_MONOTONIC)` to "1e-06" and
its comment said why: the value matched MRI but the spelling did not, because
Float#inspect dropped the ".0" before the exponent. That is the divergence this
branch fixes, so the pinned value becomes MRI's — `ruby -e 'p
Process.clock_getres(Process::CLOCK_MONOTONIC)'` prints 1.0e-06 here.

The case only surfaced after #617 merged: this branch was cut before it, so the
full suite was green when the float change was written and red once the two met.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
tannevaled added a commit that referenced this pull request Sep 23, 2026
Seven units since the last bump:

  #617  core/process: spawn, exec, the rlimits, priorities, process groups,
        kill, the wait family                     (pass 20 -> 165)
  #618  core/kernel + core/string: require_relative with an absolute path,
        the require/load argument contract, LoadError#path, byteindex/
        byterindex, split, chop/chars/each_char   (kernel 233 -> 184,
                                                   string 179 -> 153 failing)
  #620  language: compound assignment evaluation order, the flip-flop
        operator, magic-comment encodings, loop break values
                                                  (pass 1308 -> 1443)
  #622  object: Float#to_s laid out by MRI's rule rather than Go's %g  (+11)
  #624  the mspec shim: a nested describe can see the helpers of the one
        around it                                                     (+28)
  #625  deps: parser v0.2.0                        (language +333)
  #626  compiler: for-loop targets, block-local variables, what defined?
        declares                                   (language +382)

Measured with scripts/conformance/rubyspec/run.sh against the pinned corpus at
87b1631, 16-way, TIMEOUT=25, three consecutive runs: 21749, 21750, 21750. The
floor is set nine below the lowest, the same margin rule as the last bump: it
absorbs the run-to-run jitter without being wide enough to hide the loss of a
whole spec file, which is what the ratchet exists to catch. #615 (an
intermittent frame-pop crash) is still open and can cost a file when it fires.

TWO OF THESE ARE NOT VM CONFORMANCE, and the number should not be read as if
they were. #624 fixed our own mspec shim: core/string/valid_encoding/utf_8_spec
scored 0/28 under MRI 4.0.5 too, so those 28 were harness noise counted as VM
failure. #625 is a dependency bump; the VM did not change, but thirteen
language spec files became parseable for the first time and brought 333 already-
passing examples into view. Both are real gains in what the measurement can
see, not in what the VM can do.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant