Repository navigation
Ruby 4.0: $/ and $-0 stop storing the object they were handed - #886
Merged
Merged
Conversation
Ruby 4.0's deprecated_rs_setter (io.c v4.0.9:8734) replaces the assigned String
unless it is already a frozen, bare one:
rb_deprecated_str_setter(val, id, &val); // TypeError, and the warning
if (!NIL_P(val)) {
if (rb_str_equal(val, rb_default_rs)) val = rb_default_rs;
else val = rb_str_frozen_bare_string(val);
}
## Three things reading the source prevented
1. ⛔ $\ AND $, DO NOT TAKE THIS SETTER. Init_IO hooks them to
rb_deprecated_str_setter and only $/ and $-0 to deprecated_rs_setter. rbgo
handled all four through one checkStrGvar, so the obvious edit -- the one
that does not read Init_IO -- would have frozen four variables where MRI
freezes two. Both are now controls, and both were checked against a real
MRI 4.0.7 rather than against my reading.
2. ⛔ THE EQUALITY BRANCH RUNS FIRST, so a frozen plain "\n" -- which satisfies
"frozen and has no instance variables" word for word -- is still replaced,
by the canonical separator object. No example in ruby/spec reaches it:
language/predefined_spec.rb uses "abc" throughout, which can only take the
other branch. Proposed upstream as ruby/spec#1410.
3. BARE_STRING_P (string.c:372) is BOTH halves -- exact class String AND no
instance variables -- which is what makes the three existing examples
distinguish each other.
## Measured against the oracle
rbgo and ruby 4.0.7 now agree byte for byte on every case, including the two
controls that assert $\ and $, were left alone:
ors equal?=true frozen?=false <- $\, unchanged
ofs equal?=true frozen?=false <- $,, unchanged
ret true <- assignment returns the value GIVEN
dash0 true <- $-0 and $/ stay one object
## A limit this does not fix, named rather than papered over
predefined_spec.rb's "makes a new frozen String from the assigned String"
assigns a String SUBCLASS, and rbgo refuses that one step earlier --
checkStrGvar decides with a Go type assertion where MRI tests T_STRING. Filed
as #884. Widening the assertion touches every "$x must be String" global, so it
is a separate change and not a rider on this one.
Refs #799
Refs #884
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
tannevaled
added a commit
that referenced
this pull request
Nov 4, 2026
… setter (#886) (#888) Two sentences had fallen behind the code they describe. **NDArray.** The line read "(axis, keepdims: true)", so the only documented way to reduce along an axis was the positional one. #883 taught the reductions and cumsum NumPy's `axis:` spelling, and cumsum was not in the sentence at all. Verified against rbgo built at 6eb7e4a: a.sum => 15.0 a.sum(axis: 0).to_a => [3.0, 5.0, 7.0] a.sum(0).to_a => [3.0, 5.0, 7.0] a.cumsum(axis: 1).to_a => [0.0, 1.0, 3.0, 3.0, 7.0, 12.0] a.argmax(axis: 1).to_a => [2.0, 2.0] a.sum(0, axis: 0) => ArgumentError: axis given both positionally and as a keyword **Predefined globals.** The list claims each global gets "the setter MRI's variable table gives it rather than one untyped slot", then groups `$;`, `$,` and `$/` as though one setter covered them. Init_IO wires `$,` and `$\` to rb_str_setter and `$/`/`$-0` to deprecated_rs_setter, and #886 made that split real: `$/` now stores a frozen plain String of the bytes it was given instead of the object itself. The README now says so, because that difference is exactly the claim the sentence makes. rubydiff against MRI 4.0.7, same program, five lines of output: AGREE exit 0 A rbgo-main ruby 4.0.9 [arm64-darwin] B ruby@4.0 ruby 4.0.7 (2026-09-15 revision 229531a6cf) +PRISM The same program under the system ruby 2.6.10 diverges on exactly the two `$/` lines and agrees on the two `$,`/`$\` lines, which is the control: the freezing is 4.0 behaviour, not something rbgo invented. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
tannevaled
added a commit
that referenced
this pull request
Nov 5, 2026
…t does (#897) Closes #884. `language/predefined_spec.rb` goes 143 → 145, errors 6 → 4. ## How this was found By counting, not by reading the backlog. #799's table credits #886 with 4 examples of `language/predefined_spec.rb`; running the file against current main says otherwise: RBGO_RESULT pass=143 fail=16 error=6 RBGO_DETAIL error TypeError Predefined global $/ | makes a new frozen String from the assigned String value of $/ must be String RBGO_DETAIL error TypeError Predefined global $-0 | makes a new frozen String from the assigned String value of $-0 must be String #886 closed TWO of the four, not four, and said so at the time — the String subclass example was named in its body as a known limitation and filed as #884. The tally in the issue kept the old number, and a pass count cannot tell the difference. ## The cause is one assertion, and it was in five places if _, ok := v.(*object.String); !ok { raise("TypeError", ...) } An exact-type assertion. MRI's are RB_TYPE_P(val, T_STRING) in rb_str_setter and rb_check_string_type in rb_fs_check, and both are TRUE for a subclass. rbgo represents an instance of `Class.new(String)` as an RObject wrapping a String, so the assertion missed every one of them. stringTyped is that test. ## The five globals do not do the same thing afterwards Measured on MRI 4.0.7, assigning the same `Class.new(String).new("abc")` with an @ivar: | global | setter | result | |---|---|---| | `$/`, `$-0` | deprecated_rs_setter | a frozen BARE String — class String, no ivar | | `$,`, `$\` | rb_str_setter | the object as handed over, unfrozen | | `$;` | rb_fs_setter | the object as handed over, unfrozen | A fix that froze and copied everywhere would pass the two spec examples and silently rewrite the other three. The test table asserts each separately. ## $; was wrong differently, and worse It did not refuse the subclass — it fell through to #to_str, which a String subclass answers. The assignment SUCCEEDED and quietly replaced the object with a plain String. The others at least raised. ## frozenBareString's comment had outlived its reason It said only the ivar half of BARE_STRING_P was tested, because a subclass "never reaches this function at all". That was true while checkStrGvar rejected it. Now it does reach it, and without the class test the subclass instance would be returned AS IS — ivar and all. The function takes the original value as well as the String inside it, and tests both halves. ## Measured rubydiff against MRI 4.0.7 on all five globals plus the non-String refusals: AGREE. specdelta, provenanced, over language, core/string, core/io and core/kernel: 442 files, control 9666, treatment 9668, +2, 1 file moved, 0 regressed. 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.
Ruby 4.0's
deprecated_rs_setter(io.cv4.0.9:8734) stops storing the String it was handed, unless that String is already frozen and bare:Four of the 27 examples from the 4.0 move (#799).
⛔
$\and$,do NOT take this setterInit_IOhooks them torb_deprecated_str_setter, and only$/and$-0todeprecated_rs_setter:rbgo handled all four through one
checkStrGvar. The obvious edit — the one that does not readInit_IO— would have frozen four variables where MRI freezes two, silently. Both are now controls, and I checked them against a real MRI rather than against my reading of the C:⛔ The equality branch runs first
A frozen, plain
"\n"satisfies "frozen and has no instance variables" word for word and is still replaced — by the canonical separator object. Every example inlanguage/predefined_spec.rbuses"abc", which can only reach the other branch, so the suite passes either way and the identity is wrong.Measured on
ruby 4.0.7before being written here, and proposed upstream as ruby/spec#1410 so the next implementation does not have to find it the same way.BARE_STRING_P(string.c:372) is both halves — exact classStringand no instance variables — which is exactly what makes the three existing examples distinguish each other.A limit named rather than papered over
predefined_spec.rb's "makes a new frozen String from the assigned String" assigns a String subclass, and rbgo refuses that one step earlier:checkStrGvardecides with a Go type assertion where MRI testsT_STRING. So that example still fails, for a reason that has nothing to do with freezing.Filed as #884 rather than fixed here — widening the assertion touches every
"$x must be String"global, which is a different change and should not ride along inside this one.Tests
Eleven cases, including the two
$\/$,controls, the canonicalisation in both frozen and unfrozen form,$-0sharing the slot,nil, theTypeErrorwithout#to_str, and the fact that assignment returns the value given rather than the value stored.Refs #799, #884
🤖 Generated with Claude Code