Skip to content

Commit ede6725

Browse files
committed
[GR-18163] Fix Enumerable#reduce to handle non-Symbol method name parameter
PullRequest: truffleruby/4296
2 parents 693abb3 + 30e3a1d commit ede6725

File tree

5 files changed

+70
-22
lines changed

5 files changed

+70
-22
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ Compatibility:
2929
* Support `:buffer` keyword argument to `Array#pack` (#3559, @andrykonchyn).
3030
* Set `RbConfig::CONFIG['host_cpu']` to `arm64` on darwin platform (#3571, @andrykonchin).
3131
* Fix `RegexpError` messages to match CRuby better (#3398, @andrykonchin).
32+
* Fix `Enumerable#reduce` to handle non-Symbol method name parameter (#2931, @andrykonchin).
3233

3334
Performance:
3435

spec/ruby/core/enumerable/shared/inject.rb

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,23 @@
1616

1717
it "can take two argument" do
1818
EnumerableSpecs::Numerous.new(1, 2, 3).send(@method, 10, :-).should == 4
19+
EnumerableSpecs::Numerous.new(1, 2, 3).send(@method, 10, "-").should == 4
20+
21+
[1, 2, 3].send(@method, 10, :-).should == 4
22+
[1, 2, 3].send(@method, 10, "-").should == 4
23+
end
24+
25+
it "converts non-Symbol method name argument to String with #to_str if two arguments" do
26+
name = Object.new
27+
def name.to_str; "-"; end
28+
29+
EnumerableSpecs::Numerous.new(1, 2, 3).send(@method, 10, name).should == 4
30+
[1, 2, 3].send(@method, 10, name).should == 4
31+
end
32+
33+
it "raises TypeError when the second argument is not Symbol or String and it cannot be converted to String if two arguments" do
34+
-> { EnumerableSpecs::Numerous.new(1, 2, 3).send(@method, 10, Object.new) }.should raise_error(TypeError, /is not a symbol nor a string/)
35+
-> { [1, 2, 3].send(@method, 10, Object.new) }.should raise_error(TypeError, /is not a symbol nor a string/)
1936
end
2037

2138
it "ignores the block if two arguments" do
@@ -39,6 +56,25 @@
3956

4057
it "can take a symbol argument" do
4158
EnumerableSpecs::Numerous.new(10, 1, 2, 3).send(@method, :-).should == 4
59+
[10, 1, 2, 3].send(@method, :-).should == 4
60+
end
61+
62+
it "can take a String argument" do
63+
EnumerableSpecs::Numerous.new(10, 1, 2, 3).send(@method, "-").should == 4
64+
[10, 1, 2, 3].send(@method, "-").should == 4
65+
end
66+
67+
it "converts non-Symbol method name argument to String with #to_str" do
68+
name = Object.new
69+
def name.to_str; "-"; end
70+
71+
EnumerableSpecs::Numerous.new(10, 1, 2, 3).send(@method, name).should == 4
72+
[10, 1, 2, 3].send(@method, name).should == 4
73+
end
74+
75+
it "raises TypeError when passed not Symbol or String method name argument and it cannot be converted to String" do
76+
-> { EnumerableSpecs::Numerous.new(10, 1, 2, 3).send(@method, Object.new) }.should raise_error(TypeError, /is not a symbol nor a string/)
77+
-> { [10, 1, 2, 3].send(@method, Object.new) }.should raise_error(TypeError, /is not a symbol nor a string/)
4278
end
4379

4480
it "without argument takes a block with an accumulator (with first element as initial value) and the current element. Value of block becomes new accumulator" do
@@ -77,7 +113,6 @@
77113
EnumerableSpecs::EachDefiner.new('a','b','c').send(@method) {|result, i| i+result}.should == "cba"
78114
EnumerableSpecs::EachDefiner.new(3, 4, 5).send(@method) {|result, i| result*i}.should == 60
79115
EnumerableSpecs::EachDefiner.new([1], 2, 'a','b').send(@method){|r,i| r<<i}.should == [1, 2, 'a', 'b']
80-
81116
end
82117
83118
it "returns nil when fails(legacy rubycon)" do

src/main/java/org/truffleruby/core/array/ArrayNodes.java

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
import org.truffleruby.core.array.library.SharedArrayStorage;
5151
import org.truffleruby.core.cast.BooleanCastNode;
5252
import org.truffleruby.core.cast.CmpIntNode;
53+
import org.truffleruby.core.cast.NameToJavaStringNode;
5354
import org.truffleruby.core.cast.ToAryNode;
5455
import org.truffleruby.core.cast.ToIntNode;
5556
import org.truffleruby.core.cast.ToLongNode;
@@ -71,7 +72,6 @@
7172
import org.truffleruby.core.string.RubyString;
7273
import org.truffleruby.core.string.StringHelperNodes;
7374
import org.truffleruby.core.support.TypeNodes.CheckFrozenNode;
74-
import org.truffleruby.core.symbol.RubySymbol;
7575
import org.truffleruby.extra.ffi.Pointer;
7676
import org.truffleruby.interop.ToJavaStringNode;
7777
import org.truffleruby.language.Nil;
@@ -1352,34 +1352,39 @@ public void accept(Node node, CallBlockNode yieldNode, RubyArray array, Object s
13521352

13531353
// Uses Symbol and no block
13541354

1355-
@Specialization(guards = { "isEmptyArray(array)" })
1356-
Object injectSymbolEmptyArrayNoInitial(
1357-
RubyArray array, RubySymbol initialOrSymbol, NotProvided symbol, Nil block) {
1355+
@Specialization(guards = { "isEmptyArray(array)", "wasProvided(initialOrSymbol)" })
1356+
Object injectSymbolEmptyArrayNoInitial(RubyArray array, Object initialOrSymbol, NotProvided symbol, Nil block,
1357+
@Cached @Shared NameToJavaStringNode nameToJavaStringNode) {
1358+
nameToJavaStringNode.execute(this, initialOrSymbol); // ensure a method name is either a Symbol or could be converted to String
13581359
return nil;
13591360
}
13601361

13611362
@Specialization(
13621363
guards = {
13631364
"isEmptyArray(array)",
1364-
"wasProvided(initialOrSymbol)" })
1365-
Object injectSymbolEmptyArray(RubyArray array, Object initialOrSymbol, RubySymbol symbol, Nil block) {
1365+
"wasProvided(initialOrSymbol)",
1366+
"wasProvided(symbol)" })
1367+
Object injectSymbolEmptyArray(RubyArray array, Object initialOrSymbol, Object symbol, Nil block,
1368+
@Cached @Shared NameToJavaStringNode nameToJavaStringNode) {
1369+
nameToJavaStringNode.execute(this, symbol); // ensure a method name is either a Symbol or could be converted to String
13661370
return initialOrSymbol;
13671371
}
13681372

13691373
@Specialization(
1370-
guards = { "!isEmptyArray(array)" },
1374+
guards = { "!isEmptyArray(array)", "wasProvided(initialOrSymbol)" },
13711375
limit = "storageStrategyLimit()")
13721376
Object injectSymbolNoInitial(
1373-
VirtualFrame frame, RubyArray array, RubySymbol initialOrSymbol, NotProvided symbol, Nil block,
1377+
VirtualFrame frame, RubyArray array, Object initialOrSymbol, NotProvided symbol, Nil block,
13741378
@Bind("array.getStore()") Object store,
13751379
@CachedLibrary("store") ArrayStoreLibrary stores,
13761380
@Cached @Shared IntValueProfile arraySizeProfile,
13771381
@Cached @Exclusive LoopConditionProfile loopProfile,
1378-
@Cached @Shared ToJavaStringNode toJavaString) {
1382+
@Cached @Shared NameToJavaStringNode nameToJavaStringNode) {
1383+
String methodName = nameToJavaStringNode.execute(this, initialOrSymbol); // ensure a method name is either a Symbol or could be converted to String
13791384
return injectSymbolHelper(
13801385
frame,
13811386
array,
1382-
toJavaString.execute(this, initialOrSymbol),
1387+
methodName,
13831388
stores,
13841389
store,
13851390
stores.read(store, 0),
@@ -1391,19 +1396,21 @@ Object injectSymbolNoInitial(
13911396
@Specialization(
13921397
guards = {
13931398
"!isEmptyArray(array)",
1394-
"wasProvided(initialOrSymbol)" },
1399+
"wasProvided(initialOrSymbol)",
1400+
"wasProvided(symbol)" },
13951401
limit = "storageStrategyLimit()")
13961402
Object injectSymbolWithInitial(
1397-
VirtualFrame frame, RubyArray array, Object initialOrSymbol, RubySymbol symbol, Nil block,
1403+
VirtualFrame frame, RubyArray array, Object initialOrSymbol, Object symbol, Nil block,
13981404
@Bind("array.getStore()") Object store,
13991405
@CachedLibrary("store") ArrayStoreLibrary stores,
14001406
@Cached @Shared IntValueProfile arraySizeProfile,
14011407
@Cached @Exclusive LoopConditionProfile loopProfile,
1402-
@Cached @Shared ToJavaStringNode toJavaString) {
1408+
@Cached @Shared NameToJavaStringNode nameToJavaStringNode) {
1409+
String methodName = nameToJavaStringNode.execute(this, symbol); // ensure a method name is either a Symbol or could be converted to String
14031410
return injectSymbolHelper(
14041411
frame,
14051412
array,
1406-
toJavaString.execute(this, symbol),
1413+
methodName,
14071414
stores,
14081415
store,
14091416
initialOrSymbol,

src/main/ruby/truffleruby/core/enumerable.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -555,7 +555,7 @@ def inject(initial = undefined, sym = undefined, &block)
555555
# inject(initial_operand, symbol) -> object
556556

557557
sym, initial = initial, undefined if Primitive.undefined?(sym)
558-
sym = sym.to_sym
558+
sym = Truffle::Type.coerce_to_symbol(sym)
559559

560560
each do
561561
o = Primitive.single_block_arg

src/main/ruby/truffleruby/core/type.rb

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -441,13 +441,18 @@ def self.coerce_to_path(obj, check_null = true)
441441
path
442442
end
443443

444-
def self.coerce_to_symbol(obj)
445-
if Primitive.is_a? obj, Symbol
446-
obj
447-
else
448-
obj = obj.to_str if obj.respond_to?(:to_str)
449-
coerce_to(obj, Symbol, :to_sym)
444+
# Convert an object to Symbol (e.g. a method name).
445+
# Non-Symbol values are converted to String with #to_str. If it couldn't be converted - TypeError is raised.
446+
# Uses the logic of rb_check_id/rb_check_string_type/rb_check_convert_type_with_id
447+
def self.coerce_to_symbol(object)
448+
return object if Primitive.is_a?(object, Symbol)
449+
450+
string = Truffle::Type.rb_check_convert_type(object, String, :to_str)
451+
if Primitive.nil?(string)
452+
raise TypeError, "#{object} is not a symbol nor a string"
450453
end
454+
455+
string.to_sym
451456
end
452457

453458
def self.symbol_or_string_to_symbol(obj)

0 commit comments

Comments
 (0)