Skip to content

Commit

Permalink
[GR-59866] Add Range#reverse_each method
Browse files Browse the repository at this point in the history
PullRequest: truffleruby/4413
  • Loading branch information
andrykonchin committed Dec 4, 2024
2 parents 9f8855d + a501576 commit b8b6544
Show file tree
Hide file tree
Showing 5 changed files with 197 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ Compatibility:
* `String#unpack` now raises `ArgumentError` for unknown directives (#3681, @Th3-M4jor).
* `Thread::Queue#freeze` now raises `TypeError` when called (#3681, @Th3-M4jor).
* `Thread::SizedQueue#freeze` now raises `TypeError` when called (#3681, @Th3-M4jor).
* Add `Range#reverse_each` (#3681, @andrykonchin).

Performance:

Expand Down
101 changes: 101 additions & 0 deletions spec/ruby/core/range/reverse_each_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
require_relative '../../spec_helper'

ruby_version_is "3.3" do
describe "Range#reverse_each" do
it "traverses the Range in reverse order and pass each element to block" do
a = []
(1..3).reverse_each { |i| a << i }
a.should == [3, 2, 1]

a = []
(1...3).reverse_each { |i| a << i }
a.should == [2, 1]
end

it "returns self" do
r = (1..3)
r.reverse_each { |x| }.should equal(r)
end

it "returns an Enumerator if no block given" do
enum = (1..3).reverse_each
enum.should be_an_instance_of(Enumerator)
enum.to_a.should == [3, 2, 1]
end

it "raises a TypeError for endless Ranges of Integers" do
-> {
(1..).reverse_each.take(3)
}.should raise_error(TypeError, "can't iterate from NilClass")
end

it "raises a TypeError for endless Ranges of non-Integers" do
-> {
("a"..).reverse_each.take(3)
}.should raise_error(TypeError, "can't iterate from NilClass")
end

context "Integer boundaries" do
it "supports beginningless Ranges" do
(..5).reverse_each.take(3).should == [5, 4, 3]
end
end

context "non-Integer boundaries" do
it "uses #succ to iterate a Range of non-Integer elements" do
y = mock('y')
x = mock('x')

x.should_receive(:succ).any_number_of_times.and_return(y)
x.should_receive(:<=>).with(y).any_number_of_times.and_return(-1)
x.should_receive(:<=>).with(x).any_number_of_times.and_return(0)
y.should_receive(:<=>).with(x).any_number_of_times.and_return(1)
y.should_receive(:<=>).with(y).any_number_of_times.and_return(0)

a = []
(x..y).each { |i| a << i }
a.should == [x, y]
end

it "uses #succ to iterate a Range of Strings" do
a = []
('A'..'D').reverse_each { |i| a << i }
a.should == ['D','C','B','A']
end

it "uses #succ to iterate a Range of Symbols" do
a = []
(:A..:D).reverse_each { |i| a << i }
a.should == [:D, :C, :B, :A]
end

it "raises a TypeError when `begin` value does not respond to #succ" do
-> { (Time.now..Time.now).reverse_each { |x| x } }.should raise_error(TypeError, /can't iterate from Time/)
-> { (//..//).reverse_each { |x| x } }.should raise_error(TypeError, /can't iterate from Regexp/)
-> { ([]..[]).reverse_each { |x| x } }.should raise_error(TypeError, /can't iterate from Array/)
end

it "does not support beginningless Ranges" do
-> {
(..'a').reverse_each { |x| x }
}.should raise_error(TypeError, /can't iterate from NilClass/)
end
end

context "when no block is given" do
describe "returned Enumerator size" do
it "returns the Range size when Range size is finite" do
(1..3).reverse_each.size.should == 3
end

it "returns Infinity when Range size is infinite" do
(..3).reverse_each.size.should == Float::INFINITY
end

it "returns nil when Range size is unknown" do
('a'..'z').reverse_each.size.should == nil
end
end
end
end
end
64 changes: 64 additions & 0 deletions src/main/java/org/truffleruby/core/range/RangeNodes.java
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,70 @@ public RubyNode cloneUninitialized() {
}
}

@CoreMethod(names = "reverse_each", needsBlock = true, enumeratorSize = "size")
public abstract static class ReverseEachNode extends CoreMethodArrayArgumentsNode {

@Child private DispatchNode reverseEachInternalCall;

@Specialization
RubyIntRange reverseEachInt(RubyIntRange range, RubyProc block,
@Cached @Shared InlinedConditionProfile excludedEndProfile,
@Cached @Exclusive InlinedLoopConditionProfile loopProfile,
@Cached @Shared CallBlockNode yieldNode) {
final int end;
if (excludedEndProfile.profile(this, range.excludedEnd)) {
end = range.end - 1;
} else {
end = range.end;
}

int n = end;
try {
for (; loopProfile.inject(this, n >= range.begin); n--) {
yieldNode.yield(this, block, n);
}
} finally {
profileAndReportLoopCount(this, loopProfile, end - range.begin + 1);
}

return range;
}

@Specialization
RubyLongRange reverseEachLong(RubyLongRange range, RubyProc block,
@Cached @Shared InlinedConditionProfile excludedEndProfile,
@Cached @Exclusive InlinedLoopConditionProfile loopProfile,
@Cached @Shared CallBlockNode yieldNode) {
final long end;
if (excludedEndProfile.profile(this, range.excludedEnd)) {
end = range.end - 1;
} else {
end = range.end;
}

long n = end;
try {
for (; loopProfile.inject(this, n >= range.begin); n--) {
yieldNode.yield(this, block, n);
}
} finally {
profileAndReportLoopCount(this, loopProfile, end - range.begin + 1);
}

return range;
}

@Specialization
Object reverseEachObject(RubyObjectRange range, RubyProc block) {
if (reverseEachInternalCall == null) {
CompilerDirectives.transferToInterpreterAndInvalidate();
reverseEachInternalCall = insert(DispatchNode.create());
}

return reverseEachInternalCall.callWithBlock(range, "reverse_each_internal", block);
}
}

@GenerateCached(false)
@GenerateInline
public abstract static class NewRangeNode extends RubyBaseNode {
Expand Down
30 changes: 30 additions & 0 deletions src/main/ruby/truffleruby/core/range.rb
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,36 @@ def %(n)
Truffle::RangeOperations.step_no_block(self, n)
end

private def reverse_each_internal(&block)
return to_enum { size } unless block_given?

if Primitive.nil?(self.end)
raise TypeError, "can't iterate from NilClass"
end

if Primitive.is_a?(self.begin, Integer) && Primitive.is_a?(self.end, Integer)
last = exclude_end? ? self.end - 1 : self.end

i = last
while i >= first
yield i
i -= 1
end
elsif Primitive.nil?(self.begin) && Primitive.is_a?(self.end, Integer)
last = exclude_end? ? self.end - 1 : self.end

i = last
while true
yield i
i -= 1
end
else
method(:reverse_each).super_method.call(&block)
end

self
end

private def step_internal(step_size = 1, &block) # :yields: object
return Truffle::RangeOperations.step_no_block(self, step_size) unless block

Expand Down
1 change: 1 addition & 0 deletions test/mri/excludes/TestRange.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
exclude :test_reverse_each_for_beginless_range, "TypeError: can't iterate from NilClass"
exclude :test_reverse_each_for_empty_range, "RangeError: long too big to convert into `int'"
exclude :test_reverse_each_for_endless_range, "[TypeError] exception expected, not #<RangeError: cannot convert endless range to an array>."
exclude :test_reverse_each_for_single_point_range, "<no message> (java.lang.NegativeArraySizeException)"
exclude :test_size, "<3> expected but was <(31/10)>."
exclude :test_step, "ArgumentError expected but nothing was raised."
exclude :test_step_with_succ, "NoMethodError: undefined method `i' for nil:NilClass"

0 comments on commit b8b6544

Please sign in to comment.