Skip to content

The separator globals accept a String subclass, as MRI's T_STRING test does - #897

Merged
tannevaled merged 2 commits into
mainfrom
string-subclass-separators
Nov 5, 2026
Merged

tannevaled merged 2 commits into
mainfrom
string-subclass-separators

Conversation

@tannevaled

Copy link
Copy Markdown
Contributor

The separator globals accept a String subclass, as MRI's T_STRING test does

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

…t does

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>
@tannevaled
tannevaled merged commit 06d605e into main Nov 5, 2026
26 checks passed
@tannevaled
tannevaled deleted the string-subclass-separators branch November 5, 2026 20:31
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.

A String subclass is refused by $/, $-0, $\ and $, where MRI accepts it

1 participant