From 597fe2cc2f3b6950da41cfc4f3e4825e83213eef Mon Sep 17 00:00:00 2001 From: Andrew Konchin Date: Tue, 22 Aug 2023 15:15:55 +0300 Subject: [PATCH 1/2] Fix Range#bsearch and raise TypeError when range boundaries are non-numeric and block not passed --- CHANGELOG.md | 1 + spec/ruby/core/range/bsearch_spec.rb | 30 +++++++++++++++++++++++-- src/main/ruby/truffleruby/core/range.rb | 10 +++++++-- 3 files changed, 37 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d3b9779dd0ae..42eeec79b13c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ Bug fixes: * Run context cleanup such as showing the output of tools when `SignalException` and `Interrupt` escape (@eregon). * Handle a new variable inside the `case` target expression correctly (#3377, @eregon). * The arguments of `Thread.new(*args, &block)` need to be marked as shared between multiple threads (#3179, @eregon). +* Fix `Range#bsearch` and raise `TypeError` when range boundaries are non-numeric and block not passed (@andrykonchin). Compatibility: diff --git a/spec/ruby/core/range/bsearch_spec.rb b/spec/ruby/core/range/bsearch_spec.rb index 9c93671d85d1..cb30885b70d1 100644 --- a/spec/ruby/core/range/bsearch_spec.rb +++ b/spec/ruby/core/range/bsearch_spec.rb @@ -27,6 +27,10 @@ -> { ("a".."e").bsearch { true } }.should raise_error(TypeError) end + it "raises TypeError when non-Numeric begin/end and block not passed" do + -> { ("a".."e").bsearch }.should raise_error(TypeError) + end + context "with Integer values" do context "with a block returning true or false" do it "returns nil if the block returns false for every element" do @@ -94,6 +98,10 @@ (4..2).bsearch { 0 }.should == nil (4..2).bsearch { -1 }.should == nil end + + it "returns enumerator when block not passed" do + (0...3).bsearch.kind_of?(Enumerator).should == true + end end context "with Float values" do @@ -156,7 +164,6 @@ it "returns nil if the block returns greater than zero for every element" do (0.3..3.0).bsearch { |x| x <=> -1 }.should be_nil - end it "returns nil if the block never returns zero" do @@ -213,6 +220,10 @@ (0...inf).bsearch { |x| x >= Float::MAX ? 0 : 1 }.should == Float::MAX end end + + it "returns enumerator when block not passed" do + (0.1...2.3).bsearch.kind_of?(Enumerator).should == true + end end context "with endless ranges and Integer values" do @@ -250,6 +261,10 @@ [1, 2, 3].should include(result) end end + + it "returns enumerator when block not passed" do + eval("(-2..)").bsearch.kind_of?(Enumerator).should == true + end end context "with endless ranges and Float values" do @@ -327,8 +342,11 @@ eval("(0.0...)").bsearch { 0 }.should != inf end end - end + it "returns enumerator when block not passed" do + eval("(0.1..)").bsearch.kind_of?(Enumerator).should == true + end + end context "with beginless ranges and Integer values" do context "with a block returning true or false" do @@ -361,6 +379,10 @@ [1, 2, 3].should include(result) end end + + it "returns enumerator when block not passed" do + (..10).bsearch.kind_of?(Enumerator).should == true + end end context "with beginless ranges and Float values" do @@ -432,5 +454,9 @@ (...inf).bsearch { |x| 3 - x }.should == 3 end end + + it "returns enumerator when block not passed" do + (..-0.1).bsearch.kind_of?(Enumerator).should == true + end end end diff --git a/src/main/ruby/truffleruby/core/range.rb b/src/main/ruby/truffleruby/core/range.rb index b2b13ed2b818..602af53f1190 100644 --- a/src/main/ruby/truffleruby/core/range.rb +++ b/src/main/ruby/truffleruby/core/range.rb @@ -66,8 +66,6 @@ def eql?(other) end def bsearch(&block) - return to_enum :bsearch unless block_given? - start = self.begin stop = self.end @@ -99,6 +97,8 @@ def bsearch(&block) end private def bsearch_float(&block) + return to_enum :bsearch_float unless block_given? + normalized_begin = Primitive.nil?(self.begin) ? -Float::INFINITY : self.begin.to_f normalized_end = Primitive.nil?(self.end) ? Float::INFINITY : self.end.to_f normalized_end = normalized_end.prev_float if self.exclude_end? @@ -187,6 +187,8 @@ def bsearch(&block) end private def bsearch_endless(&block) + return to_enum :bsearch_endless unless block_given? + min = self.begin cur = min diff = 1 @@ -207,6 +209,8 @@ def bsearch(&block) end private def bsearch_beginless(&block) + return to_enum :bsearch_beginless unless block_given? + max = self.end cur = max diff = 1 @@ -227,6 +231,8 @@ def bsearch(&block) end private def bsearch_integer(&block) + return to_enum :bsearch_integer unless block_given? + min = self.begin max = self.end max -= 1 if Primitive.is_a?(max, Integer) and exclude_end? From 3d43e58cb866532c4474a813fa11925a8fdef582 Mon Sep 17 00:00:00 2001 From: Andrew Konchin Date: Tue, 22 Aug 2023 15:25:50 +0300 Subject: [PATCH 2/2] Adjust TypeError messages to match CRuby --- spec/ruby/core/range/bsearch_spec.rb | 16 ++++++++++------ src/main/ruby/truffleruby/core/range.rb | 8 ++++---- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/spec/ruby/core/range/bsearch_spec.rb b/spec/ruby/core/range/bsearch_spec.rb index cb30885b70d1..5254ab756cb1 100644 --- a/spec/ruby/core/range/bsearch_spec.rb +++ b/spec/ruby/core/range/bsearch_spec.rb @@ -9,26 +9,30 @@ it_behaves_like :enumeratorized_with_unknown_size, :bsearch, (1..3) it "raises a TypeError if the block returns an Object" do - -> { (0..1).bsearch { Object.new } }.should raise_error(TypeError) + -> { (0..1).bsearch { Object.new } }.should raise_error(TypeError, "wrong argument type Object (must be numeric, true, false or nil)") end - it "raises a TypeError if the block returns a String" do - -> { (0..1).bsearch { "1" } }.should raise_error(TypeError) + it "raises a TypeError if the block returns a String and boundaries are Integer values" do + -> { (0..1).bsearch { "1" } }.should raise_error(TypeError, "wrong argument type String (must be numeric, true, false or nil)") + end + + it "raises a TypeError if the block returns a String and boundaries are Float values" do + -> { (0.0..1.0).bsearch { "1" } }.should raise_error(TypeError, "wrong argument type String (must be numeric, true, false or nil)") end it "raises a TypeError if the Range has Object values" do value = mock("range bsearch") r = Range.new value, value - -> { r.bsearch { true } }.should raise_error(TypeError) + -> { r.bsearch { true } }.should raise_error(TypeError, "can't do binary search for MockObject") end it "raises a TypeError if the Range has String values" do - -> { ("a".."e").bsearch { true } }.should raise_error(TypeError) + -> { ("a".."e").bsearch { true } }.should raise_error(TypeError, "can't do binary search for String") end it "raises TypeError when non-Numeric begin/end and block not passed" do - -> { ("a".."e").bsearch }.should raise_error(TypeError) + -> { ("a".."e").bsearch }.should raise_error(TypeError, "can't do binary search for String") end context "with Integer values" do diff --git a/src/main/ruby/truffleruby/core/range.rb b/src/main/ruby/truffleruby/core/range.rb index 602af53f1190..2b19408ea97b 100644 --- a/src/main/ruby/truffleruby/core/range.rb +++ b/src/main/ruby/truffleruby/core/range.rb @@ -77,12 +77,12 @@ def bsearch(&block) elsif Primitive.is_a?(stop, Integer) bsearch_integer(&block) else - raise TypeError, "bsearch is not available for #{Primitive.class(stop)}" + raise TypeError, "can't do binary search for #{Primitive.class(stop)}" end elsif Primitive.nil?(start) && Primitive.is_a?(stop, Integer) bsearch_beginless(&block) else - raise TypeError, "bsearch is not available for #{Primitive.class(start)}" + raise TypeError, "can't do binary search for #{Primitive.class(start)}" end end @@ -165,7 +165,7 @@ def bsearch(&block) when false, nil min = mid else - raise TypeError, 'Range#bsearch block must return Numeric or boolean' + raise TypeError, "wrong argument type #{Primitive.class(result)} (must be numeric, true, false or nil)" end end @@ -261,7 +261,7 @@ def bsearch(&block) when false, nil min = mid + 1 else - raise TypeError, 'Range#bsearch block must return Numeric or boolean' + raise TypeError, "wrong argument type #{Primitive.class(result)} (must be numeric, true, false or nil)" end end