From 393819574014337ab475675e56e49a7896a10f79 Mon Sep 17 00:00:00 2001 From: Matt Valentine-House Date: Wed, 1 Apr 2026 17:48:31 +0100 Subject: [PATCH 1/5] Convert incremental sweep budget from slots to bytes Larger slot pools are less heavily used, so a fixed slot count over-services them relative to allocation pressure. Divide a byte budget by heap->slot_size so the effective per-step slot count tapers inversely with slot size. --- gc/default/default.c | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/gc/default/default.c b/gc/default/default.c index 1b7d109ce69a99..295a51d5d6e0cf 100644 --- a/gc/default/default.c +++ b/gc/default/default.c @@ -1046,8 +1046,18 @@ total_final_slots_count(rb_objspace_t *objspace) #define is_full_marking(objspace) ((objspace)->flags.during_minor_gc == FALSE) #define is_incremental_marking(objspace) ((objspace)->flags.during_incremental_marking != FALSE) #define will_be_incremental_marking(objspace) ((objspace)->rgengc.need_major_gc != GPR_FLAG_NONE) -#define GC_INCREMENTAL_SWEEP_SLOT_COUNT 2048 -#define GC_INCREMENTAL_SWEEP_POOL_SLOT_COUNT 1024 +/* + * Byte budget for incremental sweep steps. Each step sweeps at most + * this many bytes worth of slots before yielding. The effective slot + * count per step is GC_INCREMENTAL_SWEEP_BYTES / heap->slot_size, + * so larger slot pools (which are less heavily used) naturally get + * fewer slots swept per step. + * + * Baseline: 2048 slots * RVALUE_SLOT_SIZE = 2048 * 40 = 81920 bytes, + * preserving the historical behavior for the smallest heap. + */ +#define GC_INCREMENTAL_SWEEP_BYTES (2048 * RVALUE_SLOT_SIZE) +#define GC_INCREMENTAL_SWEEP_POOL_BYTES (1024 * RVALUE_SLOT_SIZE) #define is_lazy_sweeping(objspace) (GC_ENABLE_LAZY_SWEEP && has_sweeping_pages(objspace)) /* In lazy sweeping or the previous incremental marking finished and did not yield a free page. */ #define needs_continue_sweeping(objspace, heap) \ @@ -3877,6 +3887,8 @@ gc_sweep_step(rb_objspace_t *objspace, rb_heap_t *heap) struct heap_page *sweep_page = heap->sweeping_page; int swept_slots = 0; int pooled_slots = 0; + int sweep_budget = GC_INCREMENTAL_SWEEP_BYTES / heap->slot_size; + int pool_budget = GC_INCREMENTAL_SWEEP_POOL_BYTES / heap->slot_size; if (sweep_page == NULL) return FALSE; @@ -3922,14 +3934,14 @@ gc_sweep_step(rb_objspace_t *objspace, rb_heap_t *heap) heap->freed_slots += ctx.freed_slots; heap->empty_slots += ctx.empty_slots; - if (pooled_slots < GC_INCREMENTAL_SWEEP_POOL_SLOT_COUNT) { + if (pooled_slots < pool_budget) { heap_add_poolpage(objspace, heap, sweep_page); pooled_slots += free_slots; } else { heap_add_freepage(heap, sweep_page); swept_slots += free_slots; - if (swept_slots > GC_INCREMENTAL_SWEEP_SLOT_COUNT) { + if (swept_slots > sweep_budget) { break; } } From 7ddcaa4076c360b0a914bf660baa7b2f0af91e24 Mon Sep 17 00:00:00 2001 From: Matt Valentine-House Date: Wed, 1 Apr 2026 22:37:05 +0100 Subject: [PATCH 2/5] Double sweep byte budget to preserve heap 1 behavior Anchors the historical 2048/1024 slot counts on the 80-byte heap instead of the 40-byte heap. This isolates whether the major GC elimination seen in railsbench was caused by heap 1's halved budget in the previous commit. --- gc/default/default.c | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/gc/default/default.c b/gc/default/default.c index 295a51d5d6e0cf..a824216cb07390 100644 --- a/gc/default/default.c +++ b/gc/default/default.c @@ -1053,11 +1053,12 @@ total_final_slots_count(rb_objspace_t *objspace) * so larger slot pools (which are less heavily used) naturally get * fewer slots swept per step. * - * Baseline: 2048 slots * RVALUE_SLOT_SIZE = 2048 * 40 = 81920 bytes, - * preserving the historical behavior for the smallest heap. + * Baseline: preserves the historical sweep/pool slot counts (2048/1024) + * for the second heap (2 * RVALUE_SLOT_SIZE = 80 bytes). The smallest + * heap sweeps more per step (4096/2048 slots), larger heaps taper down. */ -#define GC_INCREMENTAL_SWEEP_BYTES (2048 * RVALUE_SLOT_SIZE) -#define GC_INCREMENTAL_SWEEP_POOL_BYTES (1024 * RVALUE_SLOT_SIZE) +#define GC_INCREMENTAL_SWEEP_BYTES (2048 * RVALUE_SLOT_SIZE * 2) +#define GC_INCREMENTAL_SWEEP_POOL_BYTES (1024 * RVALUE_SLOT_SIZE * 2) #define is_lazy_sweeping(objspace) (GC_ENABLE_LAZY_SWEEP && has_sweeping_pages(objspace)) /* In lazy sweeping or the previous incremental marking finished and did not yield a free page. */ #define needs_continue_sweeping(objspace, heap) \ From 16da5f23c29b6b6e2c464a2f2e3c41e1aba99e1e Mon Sep 17 00:00:00 2001 From: Matt Valentine-House Date: Thu, 2 Apr 2026 09:29:18 +0100 Subject: [PATCH 3/5] Revert "Double sweep byte budget to preserve heap 1 behavior" This reverts commit c617c5ec85ff69a5a8b13c56d51fcd234c00e1e2. --- gc/default/default.c | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/gc/default/default.c b/gc/default/default.c index a824216cb07390..295a51d5d6e0cf 100644 --- a/gc/default/default.c +++ b/gc/default/default.c @@ -1053,12 +1053,11 @@ total_final_slots_count(rb_objspace_t *objspace) * so larger slot pools (which are less heavily used) naturally get * fewer slots swept per step. * - * Baseline: preserves the historical sweep/pool slot counts (2048/1024) - * for the second heap (2 * RVALUE_SLOT_SIZE = 80 bytes). The smallest - * heap sweeps more per step (4096/2048 slots), larger heaps taper down. + * Baseline: 2048 slots * RVALUE_SLOT_SIZE = 2048 * 40 = 81920 bytes, + * preserving the historical behavior for the smallest heap. */ -#define GC_INCREMENTAL_SWEEP_BYTES (2048 * RVALUE_SLOT_SIZE * 2) -#define GC_INCREMENTAL_SWEEP_POOL_BYTES (1024 * RVALUE_SLOT_SIZE * 2) +#define GC_INCREMENTAL_SWEEP_BYTES (2048 * RVALUE_SLOT_SIZE) +#define GC_INCREMENTAL_SWEEP_POOL_BYTES (1024 * RVALUE_SLOT_SIZE) #define is_lazy_sweeping(objspace) (GC_ENABLE_LAZY_SWEEP && has_sweeping_pages(objspace)) /* In lazy sweeping or the previous incremental marking finished and did not yield a free page. */ #define needs_continue_sweeping(objspace, heap) \ From 8d6c676f6e97a07de8900e64b06d22c6b4f6a512 Mon Sep 17 00:00:00 2001 From: Benoit Daloze Date: Mon, 6 Apr 2026 20:42:28 +0200 Subject: [PATCH 4/5] Update to ruby/mspec@e1e5d4b --- spec/mspec/tool/sync/sync-rubyspec.rb | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/spec/mspec/tool/sync/sync-rubyspec.rb b/spec/mspec/tool/sync/sync-rubyspec.rb index 122de0decba9f6..86c43d0dc80778 100644 --- a/spec/mspec/tool/sync/sync-rubyspec.rb +++ b/spec/mspec/tool/sync/sync-rubyspec.rb @@ -207,16 +207,6 @@ def test_new_specs end end -def verify_commits(impl) - puts - Dir.chdir(SOURCE_REPO) do - puts "Manually check commit messages:" - print "Press enter >" - STDIN.gets - system "git", "log", "master..." - end -end - def fast_forward_master(impl) Dir.chdir(SOURCE_REPO) do sh "git", "checkout", "master" @@ -243,7 +233,6 @@ def main(impls) rebase_commits(impl) if new_commits?(impl) test_new_specs - verify_commits(impl) fast_forward_master(impl) check_ci else From 70e8654bc9489323eae3959ac0c9c8c3d2ddd1e3 Mon Sep 17 00:00:00 2001 From: Benoit Daloze Date: Mon, 6 Apr 2026 20:42:29 +0200 Subject: [PATCH 5/5] Update to ruby/spec@9ccd716 --- spec/ruby/core/encoding/compatible_spec.rb | 14 +++ spec/ruby/core/integer/comparison_spec.rb | 8 ++ spec/ruby/core/integer/eql_spec.rb | 25 +++++ spec/ruby/core/integer/fixtures/classes.rb | 10 ++ spec/ruby/core/integer/gt_spec.rb | 5 + spec/ruby/core/integer/gte_spec.rb | 5 + spec/ruby/core/integer/lt_spec.rb | 5 + spec/ruby/core/integer/lte_spec.rb | 5 + spec/ruby/core/integer/shared/equal.rb | 5 + spec/ruby/core/kernel/fixtures/classes.rb | 19 ++++ spec/ruby/core/kernel/respond_to_spec.rb | 27 +++++ .../core/matchdata/deconstruct_keys_spec.rb | 15 +++ spec/ruby/core/process/spawn_spec.rb | 44 +++++++- spec/ruby/core/range/step_spec.rb | 48 ++++++++- .../freeze_magic_comment_across_files.rb | 3 +- ...eze_magic_comment_across_files_diff_enc.rb | 3 +- ...e_magic_comment_across_files_no_comment.rb | 3 +- .../freeze_magic_comment_one_literal.rb | 4 +- .../fixtures/freeze_magic_comment_required.rb | 2 +- .../freeze_magic_comment_required_diff_enc.rb | 2 +- ...reeze_magic_comment_required_no_comment.rb | 2 +- spec/ruby/language/for_spec.rb | 102 +++++++++++++++++- spec/ruby/language/match_spec.rb | 8 ++ spec/ruby/library/stringscanner/getch_spec.rb | 7 ++ .../library/stringscanner/rest_size_spec.rb | 13 ++- .../stringscanner/scan_integer_spec.rb | 2 +- 26 files changed, 368 insertions(+), 18 deletions(-) create mode 100644 spec/ruby/core/integer/eql_spec.rb diff --git a/spec/ruby/core/encoding/compatible_spec.rb b/spec/ruby/core/encoding/compatible_spec.rb index 31376a3b758984..97860af8d332ef 100644 --- a/spec/ruby/core/encoding/compatible_spec.rb +++ b/spec/ruby/core/encoding/compatible_spec.rb @@ -557,6 +557,11 @@ [Encoding, "\x82\xa0".dup.force_encoding("shift_jis"), Encoding::Shift_JIS], ].should be_computed_by(:compatible?, /abc/) end + + it "returns the Regexp's Encoding if the String is ASCII only and the Regexp is not" do + r = Regexp.new("\xa4\xa2".dup.force_encoding("euc-jp")) + Encoding.compatible?("hello".dup.force_encoding("utf-8"), r).should == Encoding::EUC_JP + end end describe "Encoding.compatible? String, Symbol" do @@ -619,6 +624,15 @@ Encoding.compatible?(/abc/, str).should == Encoding::US_ASCII end + it "returns the String's Encoding when the String is ASCII only with a different encoding" do + r = Regexp.new("\xa4\xa2".dup.force_encoding("euc-jp")) + Encoding.compatible?(r, "hello".dup.force_encoding("utf-8")).should == Encoding::UTF_8 + end + + it "returns the Regexp's Encoding if the String has the same non-ASCII encoding" do + r = Regexp.new("\xa4\xa2".dup.force_encoding("euc-jp")) + Encoding.compatible?(r, "hello".dup.force_encoding("euc-jp")).should == Encoding::EUC_JP + end end describe "Encoding.compatible? Regexp, Regexp" do diff --git a/spec/ruby/core/integer/comparison_spec.rb b/spec/ruby/core/integer/comparison_spec.rb index 408c8e52ce8be3..e56f2831806965 100644 --- a/spec/ruby/core/integer/comparison_spec.rb +++ b/spec/ruby/core/integer/comparison_spec.rb @@ -157,6 +157,14 @@ end end + describe "with a Float" do + it "does not lose precision for values that don't fit in a double" do + (bignum_value(1) <=> bignum_value.to_f).should == 1 + (bignum_value <=> bignum_value.to_f).should == 0 + ((bignum_value - 1) <=> bignum_value.to_f).should == -1 + end + end + # The tests below are taken from matz's revision 23730 for Ruby trunk it "returns 1 when self is Infinity and other is a Bignum" do (infinity_value <=> Float::MAX.to_i*2).should == 1 diff --git a/spec/ruby/core/integer/eql_spec.rb b/spec/ruby/core/integer/eql_spec.rb new file mode 100644 index 00000000000000..9c801732067fed --- /dev/null +++ b/spec/ruby/core/integer/eql_spec.rb @@ -0,0 +1,25 @@ +require_relative '../../spec_helper' + +describe "Integer#eql?" do + context "bignum" do + it "returns true for the same value" do + bignum_value.eql?(bignum_value).should == true + end + + it "returns false for a different Integer value" do + bignum_value.eql?(bignum_value(1)).should == false + end + + it "returns false for a Float with the same numeric value" do + bignum_value.eql?(bignum_value.to_f).should == false + end + + it "returns false for a Rational with the same numeric value" do + bignum_value.eql?(Rational(bignum_value)).should == false + end + + it "returns false for a Fixnum-range Integer" do + bignum_value.eql?(42).should == false + end + end +end diff --git a/spec/ruby/core/integer/fixtures/classes.rb b/spec/ruby/core/integer/fixtures/classes.rb index 6ebfbd15658cff..65948efb9f1fec 100644 --- a/spec/ruby/core/integer/fixtures/classes.rb +++ b/spec/ruby/core/integer/fixtures/classes.rb @@ -1,4 +1,14 @@ module IntegerSpecs class CoerceError < StandardError end + + class CoercibleNumeric + def initialize(v) @v = v end + def coerce(other) [self.class.new(other), self] end + def >(other) @v.to_i > other.to_i end + def >=(other) @v.to_i >= other.to_i end + def <(other) @v.to_i < other.to_i end + def <=(other) @v.to_i <= other.to_i end + def to_i() @v.to_i end + end end diff --git a/spec/ruby/core/integer/gt_spec.rb b/spec/ruby/core/integer/gt_spec.rb index f0179e566d75ce..711c2a79b0acbb 100644 --- a/spec/ruby/core/integer/gt_spec.rb +++ b/spec/ruby/core/integer/gt_spec.rb @@ -39,5 +39,10 @@ -> { @bignum > "4" }.should raise_error(ArgumentError) -> { @bignum > mock('str') }.should raise_error(ArgumentError) end + + it "dispatches the correct operator after coercion" do + (bignum_value > IntegerSpecs::CoercibleNumeric.new(1)).should == true + (bignum_value > IntegerSpecs::CoercibleNumeric.new(bignum_value * 2)).should == false + end end end diff --git a/spec/ruby/core/integer/gte_spec.rb b/spec/ruby/core/integer/gte_spec.rb index 6237fc2c51494d..bce043bd5aad2d 100644 --- a/spec/ruby/core/integer/gte_spec.rb +++ b/spec/ruby/core/integer/gte_spec.rb @@ -39,5 +39,10 @@ -> { @bignum >= "4" }.should raise_error(ArgumentError) -> { @bignum >= mock('str') }.should raise_error(ArgumentError) end + + it "dispatches the correct operator after coercion" do + (bignum_value >= IntegerSpecs::CoercibleNumeric.new(1)).should == true + (bignum_value >= IntegerSpecs::CoercibleNumeric.new(bignum_value * 2)).should == false + end end end diff --git a/spec/ruby/core/integer/lt_spec.rb b/spec/ruby/core/integer/lt_spec.rb index c182f82555cbbd..6ca9697436ed92 100644 --- a/spec/ruby/core/integer/lt_spec.rb +++ b/spec/ruby/core/integer/lt_spec.rb @@ -41,5 +41,10 @@ -> { @bignum < "4" }.should raise_error(ArgumentError) -> { @bignum < mock('str') }.should raise_error(ArgumentError) end + + it "dispatches the correct operator after coercion" do + (bignum_value < IntegerSpecs::CoercibleNumeric.new(bignum_value * 2)).should == true + (bignum_value < IntegerSpecs::CoercibleNumeric.new(1)).should == false + end end end diff --git a/spec/ruby/core/integer/lte_spec.rb b/spec/ruby/core/integer/lte_spec.rb index 65b71d77f22463..d7bb86ce6f3e15 100644 --- a/spec/ruby/core/integer/lte_spec.rb +++ b/spec/ruby/core/integer/lte_spec.rb @@ -49,5 +49,10 @@ -> { @bignum <= "4" }.should raise_error(ArgumentError) -> { @bignum <= mock('str') }.should raise_error(ArgumentError) end + + it "dispatches the correct operator after coercion" do + (bignum_value <= IntegerSpecs::CoercibleNumeric.new(bignum_value * 2)).should == true + (bignum_value <= IntegerSpecs::CoercibleNumeric.new(1)).should == false + end end end diff --git a/spec/ruby/core/integer/shared/equal.rb b/spec/ruby/core/integer/shared/equal.rb index ecee17831c0cfd..c621ba3f81a0ca 100644 --- a/spec/ruby/core/integer/shared/equal.rb +++ b/spec/ruby/core/integer/shared/equal.rb @@ -54,5 +54,10 @@ @bignum.send(@method, obj).should == true @bignum.send(@method, obj).should == false end + + it "does not lose precision when comparing with a Float" do + (bignum_value(1).send(@method, bignum_value.to_f)).should == false + (bignum_value.send(@method, bignum_value.to_f)).should == true + end end end diff --git a/spec/ruby/core/kernel/fixtures/classes.rb b/spec/ruby/core/kernel/fixtures/classes.rb index 0e2b81988fdfc7..ad82f3cef571e4 100644 --- a/spec/ruby/core/kernel/fixtures/classes.rb +++ b/spec/ruby/core/kernel/fixtures/classes.rb @@ -180,6 +180,25 @@ class B < A alias aliased_pub_method pub_method end + class BasicA < BasicObject + define_method(:respond_to?, ::Kernel.instance_method(:respond_to?)) + + def pub_method; :public_method; end + + def undefed_method; :undefed_method; end + undef_method :undefed_method + + protected + def protected_method; :protected_method; end + + private + def private_method; :private_method; end + end + + class MissingA < A + undef :respond_to_missing? + end + class VisibilityChange class << self private :new diff --git a/spec/ruby/core/kernel/respond_to_spec.rb b/spec/ruby/core/kernel/respond_to_spec.rb index 5b3ea3f6517e67..f256767a5657f0 100644 --- a/spec/ruby/core/kernel/respond_to_spec.rb +++ b/spec/ruby/core/kernel/respond_to_spec.rb @@ -69,4 +69,31 @@ class KernelSpecs::Foo; def bar; 'done'; end; end KernelSpecs::Foo.new.respond_to?(:bar).should == true KernelSpecs::Foo.new.respond_to?(:invalid_and_silly_method_name).should == false end + + context "if object does not have #respond_to_missing?" do + it "returns true if object responds to the given public method" do + KernelSpecs::BasicA.new.respond_to?(:pub_method).should == true + KernelSpecs::MissingA.new.respond_to?(:pub_method).should == true + end + + it "returns false if object responds to the given protected method" do + KernelSpecs::BasicA.new.respond_to?(:protected_method).should == false + KernelSpecs::MissingA.new.respond_to?(:protected_method).should == false + end + + it "returns false if object responds to the given private method" do + KernelSpecs::BasicA.new.respond_to?(:private_method).should == false + KernelSpecs::MissingA.new.respond_to?(:private_method).should == false + end + + it "returns false if the given method was undefined" do + KernelSpecs::BasicA.new.respond_to?(:undefed_method).should == false + KernelSpecs::MissingA.new.respond_to?(:undefed_method).should == false + end + + it "returns false if the given method never existed" do + KernelSpecs::BasicA.new.respond_to?(:invalid_and_silly_method_name).should == false + KernelSpecs::MissingA.new.respond_to?(:invalid_and_silly_method_name).should == false + end + end end diff --git a/spec/ruby/core/matchdata/deconstruct_keys_spec.rb b/spec/ruby/core/matchdata/deconstruct_keys_spec.rb index bf22bc33ff7ade..2648340ee2ff8b 100644 --- a/spec/ruby/core/matchdata/deconstruct_keys_spec.rb +++ b/spec/ruby/core/matchdata/deconstruct_keys_spec.rb @@ -60,4 +60,19 @@ m = /(?foo)(?bar)/.match("foobar") m.deconstruct_keys([:f, :b, :c]).should == {} end + + it "includes non-participating captures as nil for nil argument" do + m = "hello".match(/(?hello)(?world)?/) + m.deconstruct_keys(nil).should == { a: "hello", b: nil } + end + + it "includes non-participating captures as nil for explicit keys" do + m = "hello".match(/(?hello)(?world)?/) + m.deconstruct_keys([:a, :b]).should == { a: "hello", b: nil } + end + + it "does not stop iterating at a non-participating capture" do + m = "hello!".match(/(?hello)(?world)?(?!)/) + m.deconstruct_keys([:b, :c, :a]).should == { b: nil, c: "!", a: "hello" } + end end diff --git a/spec/ruby/core/process/spawn_spec.rb b/spec/ruby/core/process/spawn_spec.rb index 283a7f033dcf18..6153d7783cdb5d 100644 --- a/spec/ruby/core/process/spawn_spec.rb +++ b/spec/ruby/core/process/spawn_spec.rb @@ -694,13 +694,53 @@ def child_pids(pid) end it "raises an Errno::ENOENT if the command does not exist" do - -> { Process.spawn "nonesuch" }.should raise_error(Errno::ENOENT) + -> { Process.spawn "nonesuch" }.should raise_error(Errno::ENOENT, "No such file or directory - nonesuch") + end + + it "sets $? to exit status 127 when the command does not exist" do + Process.spawn("nonesuch") rescue nil + $?.exitstatus.should == 127 + end + + it "raises an Errno::ENOENT if the file does not exist" do + -> { Process.spawn "./nonesuch" }.should raise_error(Errno::ENOENT, "No such file or directory - ./nonesuch") + end + + it "sets $? to exit status 127 when the file does not exist" do + Process.spawn("./nonesuch") rescue nil + $?.exitstatus.should == 127 + end + + platform_is_not :windows do + it "raises an Errno::EACCES when the path is a directory" do + -> { Process.spawn "./" }.should raise_error(Errno::EACCES, "Permission denied - ./") + end end unless File.executable?(__FILE__) # Some FS (e.g. vboxfs) locate all files executable platform_is_not :windows do it "raises an Errno::EACCES when the file does not have execute permissions" do - -> { Process.spawn __FILE__ }.should raise_error(Errno::EACCES) + -> { Process.spawn __FILE__ }.should raise_error(Errno::EACCES, "Permission denied - #{__FILE__}") + end + + it "sets $? to exit status 127 when the file does not have execute permissions" do + Process.spawn(__FILE__) rescue nil + $?.exitstatus.should == 127 + end + + it "raises an Errno::ENOENT when a non-executable file is found in PATH" do + dir = tmp("spawn_path_non_executable_dir") + mkdir_p dir + begin + exe = 'process-spawn-non-executable-in-path' + File.write("#{dir}/#{exe}", "#!/bin/sh\necho hi") + File.chmod(0644, "#{dir}/#{exe}") + env = { "PATH" => "#{dir}#{File::PATH_SEPARATOR}#{ENV['PATH']}" } + -> { Process.spawn(env, exe) }.should raise_error(Errno::ENOENT, "No such file or directory - #{exe}") + $?.exitstatus.should == 127 + ensure + rm_r dir + end end end diff --git a/spec/ruby/core/range/step_spec.rb b/spec/ruby/core/range/step_spec.rb index 0d0caf746d35ab..4fe768e61c6331 100644 --- a/spec/ruby/core/range/step_spec.rb +++ b/spec/ruby/core/range/step_spec.rb @@ -65,9 +65,15 @@ end ruby_version_is "3.4" do - it "does not raise an ArgumentError if step is 0 for non-numeric ranges" do + it "does not iterate if step is 0 for bounded non-numeric ranges" do t = Time.utc(2023, 2, 24) - -> { (t..t+1).step(0) { break } }.should_not raise_error(ArgumentError) + (t..t + 1).step(0) { |x| ScratchPad << x } + ScratchPad.recorded.should == [] + end + + it "raises an ArgumentError when iterating a beginless range" do + -> { (..10).step(1) { break } }.should raise_error(ArgumentError, + "#step iteration for beginless ranges is meaningless") end end @@ -138,6 +144,18 @@ (0.0..Float::INFINITY).step(2) { |x| ScratchPad << x; break if ScratchPad.recorded.size == 3 } ScratchPad.recorded.should eql([0.0, 2.0, 4.0]) end + + ruby_version_is "3.4" do + it "does not iterate if step is negative for forward range" do + (-1.0..1.0).step(-0.5) { |x| ScratchPad << x } + ScratchPad.recorded.should eql([]) + end + + it "iterates backward if step is negative for backward range" do + (1.0..-1.0).step(-0.5) { |x| ScratchPad << x } + ScratchPad.recorded.should eql([1.0, 0.5, 0.0, -0.5, -1.0]) + end + end end describe "and Integer, Float values" do @@ -337,6 +355,13 @@ (0.0...Float::INFINITY).step(2) { |x| ScratchPad << x; break if ScratchPad.recorded.size == 3 } ScratchPad.recorded.should eql([0.0, 2.0, 4.0]) end + + ruby_version_is "3.4" do + it "iterates backward with exclusive end if step is negative" do + (1.0...-1.0).step(-0.5) { |x| ScratchPad << x } + ScratchPad.recorded.should eql([1.0, 0.5, 0.0, -0.5]) + end + end end describe "and Integer, Float values" do @@ -461,6 +486,13 @@ ScratchPad.recorded.should eql([-1.0, -0.5, 0.0, 0.5]) end + it "computes each value independently to avoid accumulating floating-point errors" do + result = [] + (0.0..).step(0.1) { |x| result << x; break if result.size == 20 } + expected = 20.times.map { |i| i * 0.1 + 0.0 } + result.should eql(expected) + end + it "handles infinite values at the start" do (-Float::INFINITY..).step(2) { |x| ScratchPad << x; break if ScratchPad.recorded.size == 3 } ScratchPad.recorded.should eql([-Float::INFINITY, -Float::INFINITY, -Float::INFINITY]) @@ -657,7 +689,17 @@ ruby_version_is "3.4" do it "raises an ArgumentError" do - -> { Range.new(nil, nil).step(1) }.should raise_error(ArgumentError) + -> { Range.new(nil, nil).step(1) }.should raise_error(ArgumentError, + "#step for non-numeric beginless ranges is meaningless") + end + end + end + + context "when range is beginless and finite" do + ruby_version_is "3.4" do + it "raises an ArgumentError if step is non-numeric" do + -> { (..10).step("a") }.should raise_error(ArgumentError, + "#step for non-numeric beginless ranges is meaningless") end end end diff --git a/spec/ruby/language/fixtures/freeze_magic_comment_across_files.rb b/spec/ruby/language/fixtures/freeze_magic_comment_across_files.rb index 3aed2f29b6139d..f3ef666a3c4a63 100644 --- a/spec/ruby/language/fixtures/freeze_magic_comment_across_files.rb +++ b/spec/ruby/language/fixtures/freeze_magic_comment_across_files.rb @@ -2,4 +2,5 @@ require_relative 'freeze_magic_comment_required' -p "abc".object_id == $second_literal_id +p "abc".equal?($second_literal) +$second_literal = nil diff --git a/spec/ruby/language/fixtures/freeze_magic_comment_across_files_diff_enc.rb b/spec/ruby/language/fixtures/freeze_magic_comment_across_files_diff_enc.rb index 53ef959970096b..e9ca35e7c8e671 100644 --- a/spec/ruby/language/fixtures/freeze_magic_comment_across_files_diff_enc.rb +++ b/spec/ruby/language/fixtures/freeze_magic_comment_across_files_diff_enc.rb @@ -2,4 +2,5 @@ require_relative 'freeze_magic_comment_required_diff_enc' -p "abc".object_id != $second_literal_id +p !"abc".equal?($second_literal) +$second_literal = nil diff --git a/spec/ruby/language/fixtures/freeze_magic_comment_across_files_no_comment.rb b/spec/ruby/language/fixtures/freeze_magic_comment_across_files_no_comment.rb index fc6cd5bf82f6e2..c9eaab46a27784 100644 --- a/spec/ruby/language/fixtures/freeze_magic_comment_across_files_no_comment.rb +++ b/spec/ruby/language/fixtures/freeze_magic_comment_across_files_no_comment.rb @@ -2,4 +2,5 @@ require_relative 'freeze_magic_comment_required_no_comment' -p "abc".object_id != $second_literal_id +p !"abc".equal?($second_literal) +$second_literal = nil diff --git a/spec/ruby/language/fixtures/freeze_magic_comment_one_literal.rb b/spec/ruby/language/fixtures/freeze_magic_comment_one_literal.rb index d35905b3326ca3..c175b2b7a2f114 100644 --- a/spec/ruby/language/fixtures/freeze_magic_comment_one_literal.rb +++ b/spec/ruby/language/fixtures/freeze_magic_comment_one_literal.rb @@ -1,4 +1,4 @@ # frozen_string_literal: true -ids = Array.new(2) { "abc".object_id } -p ids.first == ids.last +objs = Array.new(2) { "abc" } +p objs.first.equal?(objs.last) diff --git a/spec/ruby/language/fixtures/freeze_magic_comment_required.rb b/spec/ruby/language/fixtures/freeze_magic_comment_required.rb index a4ff4459b1d040..f75acb2ce3117d 100644 --- a/spec/ruby/language/fixtures/freeze_magic_comment_required.rb +++ b/spec/ruby/language/fixtures/freeze_magic_comment_required.rb @@ -1,3 +1,3 @@ # frozen_string_literal: true -$second_literal_id = "abc".object_id +$second_literal = "abc" diff --git a/spec/ruby/language/fixtures/freeze_magic_comment_required_diff_enc.rb b/spec/ruby/language/fixtures/freeze_magic_comment_required_diff_enc.rb index f72a32e87911c0..739e96e99ac70f 100644 --- a/spec/ruby/language/fixtures/freeze_magic_comment_required_diff_enc.rb +++ b/spec/ruby/language/fixtures/freeze_magic_comment_required_diff_enc.rb @@ -1,4 +1,4 @@ # encoding: euc-jp # built-in for old regexp option # frozen_string_literal: true -$second_literal_id = "abc".object_id +$second_literal = "abc" diff --git a/spec/ruby/language/fixtures/freeze_magic_comment_required_no_comment.rb b/spec/ruby/language/fixtures/freeze_magic_comment_required_no_comment.rb index e09232a5f44ba4..6fbe175b42f4d8 100644 --- a/spec/ruby/language/fixtures/freeze_magic_comment_required_no_comment.rb +++ b/spec/ruby/language/fixtures/freeze_magic_comment_required_no_comment.rb @@ -1 +1 @@ -$second_literal_id = "abc".object_id +$second_literal = "abc" diff --git a/spec/ruby/language/for_spec.rb b/spec/ruby/language/for_spec.rb index 7fc6751d070eb1..dfaafa296fe469 100644 --- a/spec/ruby/language/for_spec.rb +++ b/spec/ruby/language/for_spec.rb @@ -215,7 +215,15 @@ def each j.should == 6 end - it "executes code in containing variable scope" do + it "declares iteration variables in the surrounding variable scope" do + for a, b in [[1,2]] + end + + a.should == 1 + b.should == 2 + end + + it "declares variables in the body in the surrounding variable scope" do for i in 1..2 a = 123 end @@ -223,7 +231,7 @@ def each a.should == 123 end - it "executes code in containing variable scope with 'do'" do + it "declares variables in the body in the surrounding variable scope with 'do'" do for i in 1..2 do a = 123 end @@ -231,6 +239,96 @@ def each a.should == 123 end + it "declares variables inside a block as normal" do + for i in 1..2 do + proc { + inside_proc = 42 + }.call + end + local_variables.should == [:i] + end + + it "declares variables inside a lambda as normal" do + for i in 1..2 do + -> { + inside_proc = 42 + }.call + end + local_variables.should == [:i] + end + + it "can be nested" do + for a in [6] + for b in [7] + c = a * b + end + end + local_variables.sort.should == [:a, :b, :c] + c.should == 42 + end + + it "can be nested with blocks in between" do + # This is an edge case spec for Ruby implementations which have + # their own runtime scope per for loop body (like YARV and TruffleRuby) + for a in [1] + a1 = a + a1.should == a + for b in [2] + b1 = b + a1.should == a + b1.should == b + proc { + inside_proc = 42 + + a1.should == a + b1.should == b + inside_proc.should == 42 + + for c in [3].map { |enum_var| + a1.should == a + b1.should == b + inside_proc.should == 42 + enum_var + } + c1 = c + + a1.should == a + b1.should == b + c1.should == c + inside_proc.should == 42 + + for d in [4] + d1 = d + + a1.should == a + b1.should == b + c1.should == c + d1.should == d + inside_proc.should == 42 + end + end + local_variables.sort.should == [:a, :a1, :b, :b1, :c, :c1, :d, :d1, :inside_proc] + }.call + end + end + local_variables.sort.should == [:a, :a1, :b, :b1] + end + + it "can be nested with forward arguments" do + def bar(*args) + args + end + + def foo(...) + for a in [1] + r = bar(...) + end + r + end + + foo(2, 3).should == [2, 3] + end + it "does not try to access variables outside the method" do ForSpecs::ForInClassMethod.foo.should == [:bar, :baz] ForSpecs::ForInClassMethod::READER.call.should == :same_variable_set_outside diff --git a/spec/ruby/language/match_spec.rb b/spec/ruby/language/match_spec.rb index ebf677cabc32bf..096ebee022fbb9 100644 --- a/spec/ruby/language/match_spec.rb +++ b/spec/ruby/language/match_spec.rb @@ -46,6 +46,14 @@ matched.should == "foo" unmatched.should == nil end + + it "sets existing local variables if declared in a higher scope" do + a = 42 + 1.times do + /(?foo)/ =~ @string + end + a.should == "foo" + end end describe "on syntax of 'string_literal' =~ /regexp/" do diff --git a/spec/ruby/library/stringscanner/getch_spec.rb b/spec/ruby/library/stringscanner/getch_spec.rb index d369391b140ce8..868422047dd1a1 100644 --- a/spec/ruby/library/stringscanner/getch_spec.rb +++ b/spec/ruby/library/stringscanner/getch_spec.rb @@ -11,6 +11,13 @@ s.getch.should == "c" end + it "scans newlines too" do + s = StringScanner.new("a\nc") + s.getch.should == "a" + s.getch.should == "\n" + s.getch.should == "c" + end + it "is multi-byte character sensitive" do # Japanese hiragana "A" in EUC-JP src = "\244\242".dup.force_encoding("euc-jp") diff --git a/spec/ruby/library/stringscanner/rest_size_spec.rb b/spec/ruby/library/stringscanner/rest_size_spec.rb index a5e971631a5303..65d3b50e089412 100644 --- a/spec/ruby/library/stringscanner/rest_size_spec.rb +++ b/spec/ruby/library/stringscanner/rest_size_spec.rb @@ -14,8 +14,17 @@ @s.rest_size.should == 0 end - it "is equivalent to rest.size" do + it "is equivalent to rest.bytesize" do @s.scan(/This/) - @s.rest_size.should == @s.rest.size + @s.rest_size.should == @s.rest.bytesize + + s = StringScanner.new('été') + s.rest_size.should == 5 + s.scan(/./) + s.rest_size.should == 3 + s.scan(/./) + s.rest_size.should == 2 + s.scan(/./) + s.rest_size.should == 0 end end diff --git a/spec/ruby/library/stringscanner/scan_integer_spec.rb b/spec/ruby/library/stringscanner/scan_integer_spec.rb index fe0d26f4049076..84a8fa744c0568 100644 --- a/spec/ruby/library/stringscanner/scan_integer_spec.rb +++ b/spec/ruby/library/stringscanner/scan_integer_spec.rb @@ -44,7 +44,7 @@ -> { s.scan_integer - }.should raise_error(Encoding::CompatibilityError, 'ASCII incompatible encoding: UTF-16BE') + }.should raise_error(Encoding::CompatibilityError, /ASCII incompatible encoding: UTF-16BE|incompatible encoding regexp match/) end context "given base" do