Skip to content

Array.new's size conversion, and ~ expansion's HOME rule (closes #901) - #908

Open
tannevaled wants to merge 1 commit into
mainfrom
expand-path-and-array-new
Open

tannevaled wants to merge 1 commit into
mainfrom
expand-path-and-array-new

Conversation

@tannevaled

Copy link
Copy Markdown
Contributor

Closes #901 — both items — plus a third divergence that fixing the second one exposed.

1. Array.new(2**63) raises RangeError, not ArgumentError

Array.new(10**40)
# MRI:  RangeError: bignum too big to convert into 'long'
# rbgo: ArgumentError: array size too big

rb_ary_new2 takes a long, so NUM2LONG runs before any size test; ary_new's ArgumentError is only for a value that converted and is still out of range. The class is the point — rescue RangeError could not catch it.

The boundary is exactly LONG_MAX, measured on 4.0.7 rather than reasoned from the width of a long:

size MRI
2**62 ArgumentError: array size too big
2**63-1 ArgumentError: array size too big
2**63 RangeError
10**40 RangeError

Array#first already had the identical *object.Bignum check with the right class 200 lines below (builtins.go:3331). One idiom, applied in one of the two places that needed it.

⛔ An existing row pinned the divergence. array_residuals_wave10_test.go asserted ArgumentError for Array.new(10**40) — under a comment reading "Verified against MRI Ruby 4.0". I found it by measuring the boundary, not by trusting the test. It is moved with its citation, and both sides of the boundary are now asserted so the row says where the line is.

2. expand_path("~") answers when HOME is unset

ENV.delete("HOME"); File.expand_path("~")
# MRI:  "/Users/…"   (the passwd entry for the current uid)
# rbgo: ArgumentError: couldn't find HOME environment -- expanding '~'

dirHomeStr (dir.go) already encoded MRI's rb_default_home_dir for Dir.home — $HOME, then getpwuid, raise only when neither answers. expand_path kept its own HOME-only copy, so Dir.home was right and expand_path was wrong for the same input. The fix is a call to the existing helper.

#901 said the passwd fallback was a guess and had to be established first. It is: Etc.getpwuid(Process.uid).dir is byte-identical to what MRI returns, and HOME is re-read on every call ("/first/home" then "/second/home" answer differently in one process), which is the control that makes the fallback attributable rather than a value captured at startup.

3. And HOME="" is a VALUE, not an absence — found by fixing 2

My first version sent an empty HOME to the passwd database as well, and the repository's own TestFileExpandPathHomeGuards caught it. MRI branches on if (!dir) — getenv returning NULL — so only an absent HOME reaches the fallback:

HOME Dir.home File.expand_path("~")
"" "" ArgumentError: non-absolute home
"relative/home" "relative/home" ArgumentError: non-absolute home
unset passwd home passwd home

Dir.home answers getenv verbatim — empty or relative, unchecked — and expand_path is the one that demands an absolute path.

⛔ The seam could not express that rule. osUserHomeDir wrapped os.UserHomeDir, which reports an error for unset and empty alike, so the two were folded together. It is now osLookupHome, with LookupEnv's (value, set) shape. The two seam tests moved with it rather than being relaxed, and a third row asserts that a set HOME never reaches the passwd stub.

⛔ This also fixes a divergence nobody had filed: Dir.home with HOME="" answered the passwd home where MRI answers "". Same helper, same distinction as item 2 — it could not be separated out, so it is here and said out loud rather than slipped in.

⛔ TestFileExpandPathHomeGuards passed before this change for the wrong reason — right class, wrong message (couldn't find HOME environment where MRI says non-absolute home). It only checked the class. The new test asserts the messages.

Measured

cmd/oracle against /opt/homebrew/opt/ruby@4.0/bin/ruby (ruby 4.0.7 (2026-09-15 revision 229531a6cf) +PRISM [arm64-darwin25]): six probes, every one 2 engines AGREED, byte for byte.

⛔ The new test mutates the process environment. ENV["HOME"] = … in Ruby is setenv, and runSrc builds a VM inside the test process, so a row leaving HOME empty or deleted would hand that to every test running after it in the same package. It pins and restores HOME with t.Setenv, which also makes the test refuse to run in parallel — correct for something that writes a global.

Closes #901

🤖 Generated with Claude Code

Both items of #901, plus a third divergence the second one exposed.

## 1. Array.new(2**63) raises RangeError, not ArgumentError

    Array.new(10**40)
    # MRI:  RangeError: bignum too big to convert into 'long'
    # rbgo: ArgumentError: array size too big

rb_ary_new2 takes a long, so NUM2LONG runs BEFORE any size test; ary_new's
ArgumentError is only for a value that converted and is still out of range. The
class matters: `rescue RangeError` could not catch it.

The boundary is exactly LONG_MAX, measured on 4.0.7:

    2**62     ArgumentError: array size too big
    2**63-1   ArgumentError: array size too big
    2**63     RangeError: bignum too big to convert into 'long'
    10**40    RangeError

Array#first already had the identical *object.Bignum check with the right class
200 lines below (builtins.go:3331). One idiom, applied in one of the two places
that needed it.

⛔ An existing row PINNED the divergence: array_residuals_wave10_test.go
asserted ArgumentError for Array.new(10**40), under a comment reading "Verified
against MRI Ruby 4.0". It is moved with its citation, and both sides of the
boundary are now asserted so the row says WHERE the line is.

## 2. expand_path("~") answers when HOME is unset

    ENV.delete("HOME"); File.expand_path("~")
    # MRI:  "/Users/…"  (the passwd entry for the current uid)
    # rbgo: ArgumentError: couldn't find HOME environment -- expanding '~'

dirHomeStr (dir.go) ALREADY encoded MRI's rule for Dir.home — $HOME, then
getpwuid, raise only when neither answers. expand_path kept its own HOME-only
copy, so Dir.home was right and expand_path was wrong for the same input. The
fix is a call to the existing helper.

## 3. And HOME="" is a VALUE, not an absence (found by fixing 2)

The first version of this change sent an EMPTY HOME to the passwd database too,
and the repository's own TestFileExpandPathHomeGuards caught it. MRI's
rb_default_home_dir branches on `if (!dir)` — getenv returning NULL — so only
an ABSENT HOME reaches the fallback. Measured on 4.0.7:

    HOME                 Dir.home          File.expand_path("~")
    ""                   ""                ArgumentError: non-absolute home
    "relative/home"      "relative/home"   ArgumentError: non-absolute home
    unset                passwd home       passwd home

So Dir.home answers getenv VERBATIM — empty or relative, unchecked — and
expand_path is the one that demands an absolute path.

⛔ The seam could not express that rule: osUserHomeDir wrapped os.UserHomeDir,
which reports an error for unset AND empty alike. It is now osLookupHome, with
LookupEnv's (value, set) shape. The two seam tests moved with it rather than
being relaxed, and a third row asserts that a SET HOME never reaches the passwd
stub.

⛔ That also fixes a divergence nobody had filed: Dir.home with HOME="" answered
the passwd home where MRI answers "". It is the same helper and the same
distinction as item 2, so it could not be separated out.

⛔ TestFileExpandPathHomeGuards passed BEFORE this change for the wrong reason —
right class, wrong message ("couldn't find HOME environment" where MRI says
"non-absolute home"). The new test asserts the messages.

## Measured

cmd/oracle against /opt/homebrew/opt/ruby@4.0/bin/ruby (ruby 4.0.7, +PRISM):
six probes, all "2 engines AGREED, byte for byte".

⛔ The new test mutates the PROCESS environment (ENV["HOME"] in Ruby is setenv,
and runSrc builds a VM in this process), so it pins and restores HOME with
t.Setenv: a row leaving HOME empty would hand that to every later test in the
package.

Closes #901

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

Copy link
Copy Markdown
Contributor Author

The corpus sweep: 0 moved, and that is the expected answer

PROVENANCE
  control   rbgo-ctl908 — built from e8d3abe38eb2   (main, with #907)
  treatment rbgo-trt908 — built from 6c4f691ea4e1   (this branch)
MOVE none declared: both engines read the same corpus, so every delta below is the ENGINE

2206 file(s): control 23703 passing, treatment 23703 passing, +0
0 file(s) moved, 0 regressed

Clean on every channel: KILLED on timeout 0, regressed 0, example(s) WENT RED 0, UNSTABLE 0, tally-mismatch 0 — under a 1-minute load average of 37, which is why the kill count is quoted rather than the quiet.

Why +0 is right rather than disappointing

I checked whether the corpus covers either fix instead of assuming it does not, and the answer is split:

covered by core,language?
Array.new(2**63) → RangeError no — nothing in core/array/ mentions either message
expand_path("~") with unset HOME no
expand_path("~") with empty HOME no
expand_path("~") with a relative HOME yes — core/file/expand_path_spec.rb:260

Both defects were found by probing MRI with rubydiff/cmd/oracle, not by a failing example, so no example can move when they are fixed. The sweep's job here was the other one — proving the blast radius is zero for a helper (dirHomeStr) that Dir.home, Dir.chdir and every ~ expansion go through.

⭐ And the corpus did supply one control I did not have to write

core/file/expand_path_spec.rb   control: pass=27 fail=0    treatment: pass=27 fail=0

That file asserts ENV["HOME"] = "non-absolute" → ArgumentError, 'non-absolute home' — the one branch of the three that this change deliberately leaves alone. It passed before and passes after, which is exactly the row I would have had to invent to show the relative-HOME path is untouched.

⭐ Incidentally it also guards the hazard this PR's own test had to handle: its after :each restores ENV["HOME"]. Upstream treats a spec that mutates HOME as something to clean up after, for the same reason t.Setenv is in the new test — a leaked HOME is handed to everything that runs next.

🤖 Generated with Claude Code

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.

Two parity divergences: Array.new(2**128) raises the wrong class, and expand_path("~") raises where MRI does not

1 participant