Skip to content

Make all Regexp object frozen - #15762

Closed
niku wants to merge 6 commits into
ruby:masterfrom
niku:frozen_regexp
Closed

Make all Regexp object frozen#15762
niku wants to merge 6 commits into
ruby:masterfrom
niku:frozen_regexp

Conversation

@niku

@niku niku commented Dec 29, 2025

Copy link
Copy Markdown
Contributor
  • Make all Regexp instances frozen across every creation path (new, compile, dup, clone, literals, union, Marshal.load).
  • Marshal.load always returns frozen Regexp (even with freeze: false), relying on rb_reg_new_str to carry encoding.
  • Remove the Enumerable#grep Regexp-specific optimization that depended on mutable Regexp/Regexp#===.
  • Consolidate freeze checks in test_regexp_object_is_frozen; drop incompatible tests.

Breaking Changes:

  • Regexp cannot hold instance variables or singleton methods (FrozenError).
  • Regexp subclass state is effectively unsupported.
  • //.dup.instance_eval { initialize_copy(nil) } now raises FrozenError (was TypeError).
  • Marshal.load(..., freeze: false) still returns frozen Regexp.

refs: https://bugs.ruby-lang.org/issues/8948

niku added 6 commits December 29, 2025 13:27
Add test to verify Regexp.new returns frozen objects.
Implement rb_obj_freeze(self) in rb_reg_initialize_m (re.c).

Breaking changes:
- Regexp subclassing with instance variables is no longer supported
- Singleton methods cannot be defined on Regexp objects

Remove incompatible test cases:
- test_string.rb: test_match_method (singleton method definition)
- test_symbol.rb: test_match_method (singleton method definition)
- test_enum.rb: test_grep_optimization (=== override test)
- marshaltestlib.rb: test_regexp_subclass and MyRegexp class

Remove Enumerable#grep/grep_v Regexp optimization:
- enum.c: Remove grep_regexp_i function and optimization branch
- This optimization checked if Regexp#=== was overridden
- No longer possible since Regexp objects are frozen
Breaking changes:
- //.dup.instance_eval { initialize_copy(nil) } raises FrozenError instead of TypeError.
Support Regexp deserialization while keeping objects frozen.
Encoding information is transferred from the intermediate string
to the Regexp object through rb_reg_new_str's internal mechanisms,
eliminating the need for instance variable copying.

Add Marshal.load tests to test_regexp_object_is_frozen:
- Standard Marshal.load returns frozen Regexp
- Marshal.load with freeze: false also returns frozen Regexp
Verify that Regexp literals created via rb_reg_new_str are frozen.
This is part of the Marshal.load testing that now includes literals.
Verify that Regexp.union returns frozen objects.
rb_reg_new_ary internally calls rb_reg_new_str which now freezes objects.
@cloudbees-smart-tests

cloudbees-smart-tests Bot commented Dec 29, 2025

Copy link
Copy Markdown

17/67033 Tests Failed

spec/ruby/core/marshal/dump_spec.rb#Marshal.dump with a Regexp dumps an extended Regexp
FrozenError: can't modify frozen Regexp: //
/home/runner/work/ruby/ruby/src/spec/ruby/core/marshal/dump_spec.rb:483:in 'Module#extend_object'
/home/runner/work/ruby/ruby/src/spec/ruby/core/marshal/dump_spec.rb:483:in 'Kernel#extend'
/home/runner/work/ruby/ruby/src/spec/ruby/core/marshal/dump_spec.rb:483:in 'block (3 levels) in <top (required)>'
/home/runner/work/ruby/ruby/src/spec/ruby/core/marshal/dump_spec.rb:6:in '<top (required)>'
spec/ruby/core/marshal/dump_spec.rb#Marshal.dump with a Regexp dumps a Regexp with instance variables
FrozenError: can't modify frozen Regexp: //
/tmp/_actions-runner-working-dir/ruby/ruby/src/spec/ruby/core/marshal/dump_spec.rb:478:in 'Kernel#instance_variable_set'
/tmp/_actions-runner-working-dir/ruby/ruby/src/spec/ruby/core/marshal/dump_spec.rb:478:in 'block (3 levels) in <top (required)>'
/tmp/_actions-runner-working-dir/ruby/ruby/src/spec/ruby/core/marshal/dump_spec.rb:6:in '<top (required)>'
spec/ruby/core/marshal/load_spec.rb#Marshal.load for a Regexp loads an extended Regexp
FrozenError: can't modify frozen Regexp: /[a-z]/
/Users/runner/work/ruby/ruby/src/spec/ruby/core/marshal/shared/load.rb:911:in 'Module#extend_object'
/Users/runner/work/ruby/ruby/src/spec/ruby/core/marshal/shared/load.rb:911:in 'Kernel#extend'
/Users/runner/work/ruby/ruby/src/spec/ruby/core/marshal/shared/load.rb:911:in 'block (3 levels) in <top (required)>'
/Users/runner/work/ruby/ruby/src/spec/ruby/core/marshal/load_spec.rb:4:in '<top (required)>'
spec/ruby/core/marshal/load_spec.rb#Marshal.load for a Regexp loads a Regexp subclass instance variables when it is extended with a module
FrozenError: can't modify frozen UserRegexp: //
/home/runner/work/ruby/ruby/src/spec/ruby/core/marshal/shared/load.rb:921:in 'Module#extend_object'
/home/runner/work/ruby/ruby/src/spec/ruby/core/marshal/shared/load.rb:921:in 'Kernel#extend'
/home/runner/work/ruby/ruby/src/spec/ruby/core/marshal/shared/load.rb:921:in 'block (3 levels) in <top (required)>'
/home/runner/work/ruby/ruby/src/spec/ruby/core/marshal/load_spec.rb:4:in '<top (required)>'
spec/ruby/core/marshal/load_spec.rb#Marshal.load for a Regexp restore the regexp instance variables
FrozenError: can't modify frozen Regexp: /hello/
/home/runner/work/ruby/ruby/src/spec/ruby/core/marshal/shared/load.rb:936:in 'Kernel#instance_variable_set'
/home/runner/work/ruby/ruby/src/spec/ruby/core/marshal/shared/load.rb:936:in 'block (4 levels) in <top (required)>'
/home/runner/work/ruby/ruby/src/spec/ruby/core/marshal/load_spec.rb:4:in '<top (required)>'
12 more failed tests
spec/ruby/core/marshal/load_spec.rb#Marshal.load loads a Regexp subclass /i

spec/ruby/core/marshal/restore_spec.rb#Marshal.restore for a Regexp loads an extended Regexp
FrozenError: can't modify frozen Regexp: /[a-z]/
/home/runner/work/ruby/ruby/src/spec/ruby/core/marshal/shared/load.rb:911:in 'Module#extend_object'
/home/runner/work/ruby/ruby/src/spec/ruby/core/marshal/shared/load.rb:911:in 'Kernel#extend'
/home/runner/work/ruby/ruby/src/spec/ruby/core/marshal/shared/load.rb:911:in 'block (3 levels) in <top (required)>'
/home/runner/work/ruby/ruby/src/spec/ruby/core/marshal/restore_spec.rb:4:in '<top (required)>'
spec/ruby/core/marshal/restore_spec.rb#Marshal.restore for a Regexp loads a Regexp subclass instance variables when it is extended with a module
FrozenError: can't modify frozen UserRegexp: //
/home/runner/work/ruby/ruby/src/spec/ruby/core/marshal/shared/load.rb:921:in 'Module#extend_object'
/home/runner/work/ruby/ruby/src/spec/ruby/core/marshal/shared/load.rb:921:in 'Kernel#extend'
/home/runner/work/ruby/ruby/src/spec/ruby/core/marshal/shared/load.rb:921:in 'block (3 levels) in <top (required)>'
/home/runner/work/ruby/ruby/src/spec/ruby/core/marshal/restore_spec.rb:4:in '<top (required)>'
spec/ruby/core/marshal/restore_spec.rb#Marshal.restore for a Regexp restore the regexp instance variables
FrozenError: can't modify frozen Regexp: /hello/
/Users/runner/work/ruby/ruby/src/spec/ruby/core/marshal/shared/load.rb:936:in 'Kernel#instance_variable_set'
/Users/runner/work/ruby/ruby/src/spec/ruby/core/marshal/shared/load.rb:936:in 'block (4 levels) in <top (required)>'
/Users/runner/work/ruby/ruby/src/spec/ruby/core/marshal/restore_spec.rb:4:in '<top (required)>'
spec/ruby/core/marshal/restore_spec.rb#Marshal.restore loads a Regexp subclass /i

spec/ruby/core/regexp/compile_spec.rb#Regexp.compile works by default for subclasses with overridden #initialize
FrozenError: can't modify frozen RegexpSpecsSubclass: /hi/
/Users/runner/work/ruby/ruby/src/spec/ruby/core/regexp/shared/new.rb:12:in 'RegexpSpecsSubclass#initialize'
/Users/runner/work/ruby/ruby/src/spec/ruby/core/regexp/shared/new.rb:20:in 'Regexp.compile'
/Users/runner/work/ruby/ruby/src/spec/ruby/core/regexp/shared/new.rb:20:in 'block (2 levels) in <top (required)>'
/Users/runner/work/ruby/ruby/src/spec/ruby/core/regexp/compile_spec.rb:4:in '<top (required)>'
spec/ruby/core/regexp/initialize_spec.rb#Regexp#initialize raises a TypeError on an initialized non-literal Regexp
Expected TypeError
but got: FrozenError (can't modify frozen Regexp: //)
/Users/runner/work/ruby/ruby/src/spec/ruby/core/regexp/initialize_spec.rb:13:in 'Regexp#initialize'
/Users/runner/work/ruby/ruby/src/spec/ruby/core/regexp/initialize_spec.rb:13:in 'block (3 levels) in <top (required)>'
/Users/runner/work/ruby/ruby/src/spec/ruby/core/regexp/initialize_spec.rb:13:in 'block (2 levels) in <top (required)>'
/Users/runner/work/ruby/ruby/src/spec/ruby/core/regexp/initialize_spec.rb:3:in '<top (required)>'
spec/ruby/core/regexp/new_spec.rb#Regexp.new works by default for subclasses with overridden #initialize
FrozenError: can't modify frozen RegexpSpecsSubclass: /hi/
/home/runner/work/ruby/ruby/src/spec/ruby/core/regexp/shared/new.rb:12:in 'RegexpSpecsSubclass#initialize'
/home/runner/work/ruby/ruby/src/spec/ruby/core/regexp/shared/new.rb:20:in 'Class#new'
/home/runner/work/ruby/ruby/src/spec/ruby/core/regexp/shared/new.rb:20:in 'block (2 levels) in <top (required)>'
/home/runner/work/ruby/ruby/src/spec/mspec/lib/mspec/runner/mspec.rb:117:in 'BasicObject#instance_exec'
/home/runner/work/ruby/ruby/src/spec/mspec/lib/mspec/runner/mspec.rb:117:in 'MSpec.protect'
/home/runner/work/ruby/ruby/src/spec/mspec/lib/mspec/runner/context.rb:176:in 'block in ContextState#protect'
/home/runner/work/ruby/ruby/src/spec/mspec/lib/mspec/runner/context.rb:176:in 'Array#all?'
/home/runner/work/ruby/ruby/src/spec/mspec/lib/mspec/runner/context.rb:176:in 'ContextState#protect'
/home/runner/work/ruby/ruby/src/spec/mspec/lib/mspec/runner/context.rb:212:in 'block (2 levels) in ContextState#process'
/home/runner/work/ruby/ruby/src/spec/mspec/lib/mspec/runner/mspec.rb:285:in 'MSpec.repeat'
/home/runner/work/ruby/ruby/src/spec/mspec/lib/mspec/runner/context.rb:204:in 'block in ContextState#process'
/home/runner/work/ruby/ruby/src/spec/mspec/lib/mspec/runner/context.rb:203:in 'Array#each'
/home/runner/work/ruby/ruby/src/spec/mspec/lib/mspec/runner/context.rb:203:in 'ContextState#process'
/home/runner/work/ruby/ruby/src/spec/mspec/lib/mspec/runner/mspec.rb:56:in 'MSpec.describe'
/home/runner/work/ruby/ruby/src/spec/mspec/lib/mspec/runner/object.rb:11:in 'Object#describe'
/home/runner/work/ruby/ruby/src/spec/ruby/core/regexp/new_spec.rb:4:in '<top (required)>'
/home/runner/work/ruby/ruby/src/spec/mspec/lib/mspec/runner/mspec.rb:100:in 'Kernel.load'
/home/runner/work/ruby/ruby/src/spec/mspec/lib/mspec/runner/mspec.rb:100:in 'block (2 levels) in MSpec.files'
/home/runner/work/ruby/ruby/src/spec/mspec/lib/mspec/runner/mspec.rb:117:in 'BasicObject#instance_exec'
/home/runner/work/ruby/ruby/src/spec/mspec/lib/mspec/runner/mspec.rb:117:in 'MSpec.protect'
/home/runner/work/ruby/ruby/src/spec/mspec/lib/mspec/runner/mspec.rb:100:in 'block in MSpec.files'
/home/runner/work/ruby/ruby/src/spec/mspec/lib/mspec/runner/mspec.rb:91:in 'Array#each'
/home/runner/work/ruby/ruby/src/spec/mspec/lib/mspec/runner/mspec.rb:91:in 'MSpec.each_file'
/home/runner/work/ruby/ruby/src/spec/mspec/lib/mspec/runner/mspec.rb:96:in 'MSpec.files'
/home/runner/work/ruby/ruby/src/spec/mspec/lib/mspec/runner/mspec.rb:64:in 'MSpec.process'
/home/runner/work/ruby/ruby/src/spec/mspec/lib/mspec/commands/mspec-run.rb:84:in 'MSpecRun#run'
/home/runner/work/ruby/ruby/src/spec/mspec/lib/mspec/utils/script.rb:297:in 'MSpecScript.main'
/home/runner/work/ruby/ruby/src/spec/mspec/bin/mspec-run:7:in '<main>'
spec/ruby/core/string/match_spec.rb#String#match calls match on the regular expression
FrozenError: can't modify frozen Regexp: /./
/home/runner/work/ruby/ruby/src/spec/ruby/core/string/match_spec.rb:141:in 'block (2 levels) in <top (required)>'
/home/runner/work/ruby/ruby/src/spec/ruby/core/string/match_spec.rb:52:in '<top (required)>'
spec/ruby/optional/capi/encoding_spec.rb#C-API Encoding function rb_enc_associate_index sets the encoding of a Regexp to the encoding
FrozenError: can't modify frozen Regexp: /regexp/
/home/runner/work/ruby/ruby/src/spec/ruby/optional/capi/encoding_spec.rb:551:in 'CApiEncodingSpecs#rb_enc_associate_index'
/home/runner/work/ruby/ruby/src/spec/ruby/optional/capi/encoding_spec.rb:551:in 'block (3 levels) in <top (required)>'
/home/runner/work/ruby/ruby/src/spec/mspec/lib/mspec/runner/mspec.rb:117:in 'BasicObject#instance_exec'
/home/runner/work/ruby/ruby/src/spec/mspec/lib/mspec/runner/mspec.rb:117:in 'MSpec.protect'
/home/runner/work/ruby/ruby/src/spec/mspec/lib/mspec/runner/context.rb:176:in 'block in ContextState#protect'
/home/runner/work/ruby/ruby/src/spec/mspec/lib/mspec/runner/context.rb:176:in 'Array#all?'
/home/runner/work/ruby/ruby/src/spec/mspec/lib/mspec/runner/context.rb:176:in 'ContextState#protect'
/home/runner/work/ruby/ruby/src/spec/mspec/lib/mspec/runner/context.rb:212:in 'block (2 levels) in ContextState#process'
/home/runner/work/ruby/ruby/src/spec/mspec/lib/mspec/runner/mspec.rb:285:in 'MSpec.repeat'
/home/runner/work/ruby/ruby/src/spec/mspec/lib/mspec/runner/context.rb:204:in 'block in ContextState#process'
/home/runner/work/ruby/ruby/src/spec/mspec/lib/mspec/runner/context.rb:203:in 'Array#each'
/home/runner/work/ruby/ruby/src/spec/mspec/lib/mspec/runner/context.rb:203:in 'ContextState#process'
/home/runner/work/ruby/ruby/src/spec/mspec/lib/mspec/runner/context.rb:235:in 'block in ContextState#process'
/home/runner/work/ruby/ruby/src/spec/mspec/lib/mspec/runner/context.rb:235:in 'Array#each'
/home/runner/work/ruby/ruby/src/spec/mspec/lib/mspec/runner/context.rb:235:in 'ContextState#process'
/home/runner/work/ruby/ruby/src/spec/mspec/lib/mspec/runner/mspec.rb:56:in 'MSpec.describe'
/home/runner/work/ruby/ruby/src/spec/mspec/lib/mspec/runner/object.rb:11:in 'Object#describe'
/home/runner/work/ruby/ruby/src/spec/ruby/optional/capi/encoding_spec.rb:43:in '<top (required)>'
/home/runner/work/ruby/ruby/src/spec/mspec/lib/mspec/runner/mspec.rb:100:in 'Kernel.load'
/home/runner/work/ruby/ruby/src/spec/mspec/lib/mspec/runner/mspec.rb:100:in 'block (2 levels) in MSpec.files'
/home/runner/work/ruby/ruby/src/spec/mspec/lib/mspec/runner/mspec.rb:117:in 'BasicObject#instance_exec'
/home/runner/work/ruby/ruby/src/spec/mspec/lib/mspec/runner/mspec.rb:117:in 'MSpec.protect'
/home/runner/work/ruby/ruby/src/spec/mspec/lib/mspec/runner/mspec.rb:100:in 'block in MSpec.files'
/home/runner/work/ruby/ruby/src/spec/mspec/lib/mspec/runner/mspec.rb:91:in 'Array#each'
/home/runner/work/ruby/ruby/src/spec/mspec/lib/mspec/runner/mspec.rb:91:in 'MSpec.each_file'
/home/runner/work/ruby/ruby/src/spec/mspec/lib/mspec/runner/mspec.rb:96:in 'MSpec.files'
/home/runner/work/ruby/ruby/src/spec/mspec/lib/mspec/runner/mspec.rb:64:in 'MSpec.process'
/home/runner/work/ruby/ruby/src/spec/mspec/lib/mspec/commands/mspec-run.rb:84:in 'MSpecRun#run'
/home/runner/work/ruby/ruby/src/spec/mspec/lib/mspec/utils/script.rb:297:in 'MSpecScript.main'
/home/runner/work/ruby/ruby/src/spec/mspec/bin/mspec-run:7:in '<main>'
spec/ruby/optional/capi/encoding_spec.rb#C-API Encoding function rb_enc_associate sets the encoding of a Regexp to the encoding
FrozenError: can't modify frozen Regexp: /regexp/
D:/a/ruby/ruby/src/spec/ruby/optional/capi/encoding_spec.rb:534:in 'CApiEncodingSpecs#rb_enc_associate'
D:/a/ruby/ruby/src/spec/ruby/optional/capi/encoding_spec.rb:534:in 'block (3 levels) in <top (required)>'
D:/a/ruby/ruby/src/spec/ruby/optional/capi/encoding_spec.rb:43:in '<top (required)>'
spec/ruby/optional/capi/encoding_spec.rb#C-API Encoding function rb_enc_copy sets the encoding of a Regexp to that of the second argument
FrozenError: can't modify frozen Regexp: /regexp/
/home/runner/work/ruby/ruby/src/spec/ruby/optional/capi/encoding_spec.rb:483:in 'CApiEncodingSpecs#rb_enc_copy'
/home/runner/work/ruby/ruby/src/spec/ruby/optional/capi/encoding_spec.rb:483:in 'block (3 levels) in <top (required)>'
/home/runner/work/ruby/ruby/src/spec/ruby/optional/capi/encoding_spec.rb:43:in '<top (required)>'

[-> View Test suite health in main branch]

@niku

niku commented Dec 29, 2025

Copy link
Copy Markdown
Contributor Author

I found another #14547 with the same goal, so I'm closing this one.

@niku niku closed this Dec 29, 2025
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