Skip to content

Commit 501c9f8

Browse files
committed
[GR-26781] Implement Hash.ruby2_keywords_hash{,?} and **kwargs accept non-Symbols keys in 2.7
PullRequest: truffleruby/2215
2 parents d4a7fd2 + b5d72fa commit 501c9f8

19 files changed

+158
-117
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ Compatibility:
3131
* Implemented `BigDecimal` as C extension to improve compatibility.
3232
* Comment lines can be placed between fluent dot now (#2004, @ssnickolay).
3333
* Implemented `rb_make_exception`.
34+
* `**kwargs` now accept non-Symbol keys like Ruby 2.7.
3435

3536
Performance:
3637

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
require_relative '../../spec_helper'
2+
require_relative 'fixtures/classes'
3+
4+
ruby_version_is "2.7" do
5+
describe "Hash.ruby2_keywords_hash?" do
6+
it "returns false if the Hash is not a keywords Hash" do
7+
Hash.ruby2_keywords_hash?({}).should == false
8+
end
9+
10+
it "returns true if the Hash is a keywords Hash marked by Module#ruby2_keywords" do
11+
obj = Class.new {
12+
ruby2_keywords def m(*args)
13+
args.last
14+
end
15+
}.new
16+
Hash.ruby2_keywords_hash?(obj.m(a: 1)).should == true
17+
end
18+
19+
it "raises TypeError for non-Hash" do
20+
-> { Hash.ruby2_keywords_hash?(nil) }.should raise_error(TypeError)
21+
end
22+
end
23+
24+
describe "Hash.ruby2_keywords_hash" do
25+
it "returns a copy of a Hash and marks the copy as a keywords Hash" do
26+
h = {a: 1}.freeze
27+
kw = Hash.ruby2_keywords_hash(h)
28+
Hash.ruby2_keywords_hash?(h).should == false
29+
Hash.ruby2_keywords_hash?(kw).should == true
30+
kw.should == h
31+
end
32+
33+
it "returns an instance of the subclass if called on an instance of a subclass of Hash" do
34+
h = HashSpecs::MyHash.new
35+
h[:a] = 1
36+
kw = Hash.ruby2_keywords_hash(h)
37+
kw.class.should == HashSpecs::MyHash
38+
Hash.ruby2_keywords_hash?(h).should == false
39+
Hash.ruby2_keywords_hash?(kw).should == true
40+
kw.should == h
41+
end
42+
43+
it "raises TypeError for non-Hash" do
44+
-> { Hash.ruby2_keywords_hash(nil) }.should raise_error(TypeError)
45+
end
46+
end
47+
end

spec/ruby/language/block_spec.rb

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -873,14 +873,20 @@ def m(a) yield a end
873873
end.call(1, 2, 3).should == [[], 1, 2, 3]
874874
end
875875

876-
it "are required" do
876+
it "are required for a lambda" do
877877
-> {
878878
-> *a, b do
879879
[a, b]
880880
end.call
881881
}.should raise_error(ArgumentError)
882882
end
883883

884+
it "are assigned to nil when not enough arguments are given to a proc" do
885+
proc do |a, *b, c|
886+
[a, b, c]
887+
end.call.should == [nil, [], nil]
888+
end
889+
884890
describe "with required args" do
885891

886892
it "gathers remaining args in the splat" do
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
fails:Hash.ruby2_keywords_hash? returns true if the Hash is a keywords Hash marked by Module#ruby2_keywords

spec/tags/language/hash_tags.txt

Lines changed: 0 additions & 1 deletion
This file was deleted.

spec/tags/language/method_tags.txt

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,7 @@
11
fails:A method assigns local variables from method parameters for definition 'def m() end'
22
fails:A method assigns local variables from method parameters for definition 'def m(*a) a end'
3-
fails:A method assigns local variables from method parameters for definition 'def m(a=1, **) a end'
4-
fails:A method assigns local variables from method parameters for definition 'def m(*a, **) a end'
5-
fails:A method assigns local variables from method parameters for definition 'def m(*, **k) k end'
63
fails:A method assigns local variables from method parameters for definition 'def m(a = nil, **k) [a, k] end'
74
fails:A method assigns local variables from method parameters for definition 'def m(*a, **k) [a, k] end'
8-
fails:A method assigns local variables from method parameters for definition 'def m(**k); k end;'
9-
fails:A method assigns local variables from method parameters for definition 'def m(a:, **) a end'
10-
fails:A method assigns local variables from method parameters for definition 'def m(a:, **k) [a, k] end'
115
fails:A method assigns local variables from method parameters for definition 'def m(a, **nil); a end;'
126
fails:A method when passing an empty keyword splat to a method that does not accept keywords for definition 'def m(*a); a; end'
137
fails:A method when passing an empty keyword splat to a method that does not accept keywords for definition 'def m(a); a; end'

src/main/java/org/truffleruby/core/hash/EnsureSymbolKeysNode.java

Lines changed: 0 additions & 48 deletions
This file was deleted.

src/main/java/org/truffleruby/core/hash/HashNodes.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,23 @@ public boolean isSmallArrayOfPairs(Object[] args, RubyLanguage language) {
173173

174174
}
175175

176+
@CoreMethod(names = "ruby2_keywords_hash?", onSingleton = true, required = 1)
177+
public abstract static class IsRuby2KeywordsHashNode extends CoreMethodArrayArgumentsNode {
178+
@Specialization
179+
protected boolean isRuby2KeywordsHash(RubyHash hash) {
180+
return hash.ruby2_keywords;
181+
}
182+
}
183+
184+
@Primitive(name = "hash_mark_ruby2_keywords")
185+
public abstract static class HashMarkRuby2KeywordsNode extends CoreMethodArrayArgumentsNode {
186+
@Specialization
187+
protected RubyHash markRuby2Keywords(RubyHash hash) {
188+
hash.ruby2_keywords = true;
189+
return hash;
190+
}
191+
}
192+
176193
@ImportStatic(HashGuards.class)
177194
public abstract static class HashLookupOrExecuteDefaultNode extends RubyContextNode {
178195

src/main/java/org/truffleruby/core/hash/RubyHash.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ public class RubyHash extends RubyDynamicObject implements ObjectGraphNode {
2929
public Entry firstInSequence;
3030
public Entry lastInSequence;
3131
public boolean compareByIdentity;
32+
public boolean ruby2_keywords = false;
3233

3334
public RubyHash(
3435
RubyClass rubyClass,

src/main/java/org/truffleruby/language/arguments/CheckKeywordArityNode.java

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
*/
1010
package org.truffleruby.language.arguments;
1111

12+
import com.oracle.truffle.api.CompilerDirectives;
1213
import org.truffleruby.RubyLanguage;
1314
import org.truffleruby.collections.BiConsumerNode;
1415
import org.truffleruby.core.hash.RubyHash;
@@ -42,9 +43,6 @@ public CheckKeywordArityNode(RubyLanguage language, Arity arity, RubyNode body)
4243
this.arity = arity;
4344
this.body = body;
4445
this.readUserKeywordsHashNode = new ReadUserKeywordsHashNode(arity.getRequired());
45-
this.checkKeywordArgumentsNode = new CheckKeywordArgumentsNode(language, arity);
46-
this.eachKeyNode = EachKeyValueNode.create();
47-
4846
}
4947

5048
@Override
@@ -74,47 +72,59 @@ private void checkArity(VirtualFrame frame) {
7472
throw new RaiseException(getContext(), coreExceptions().argumentError(given, arity.getRequired(), this));
7573
}
7674

77-
if (keywordArguments != null) {
78-
receivedKeywordsProfile.enter();
79-
eachKeyNode.executeEachKeyValue(frame, keywordArguments, checkKeywordArgumentsNode, null);
75+
if (!arity.hasKeywordsRest() && keywordArguments != null) {
76+
checkKeywordArguments(frame, keywordArguments);
77+
}
78+
}
79+
80+
void checkKeywordArguments(VirtualFrame frame, RubyHash keywordArguments) {
81+
if (eachKeyNode == null) {
82+
CompilerDirectives.transferToInterpreterAndInvalidate();
83+
eachKeyNode = insert(EachKeyValueNode.create());
8084
}
85+
if (checkKeywordArgumentsNode == null) {
86+
CompilerDirectives.transferToInterpreterAndInvalidate();
87+
checkKeywordArgumentsNode = insert(new CheckKeywordArgumentsNode(getLanguage(), arity));
88+
}
89+
eachKeyNode.executeEachKeyValue(frame, keywordArguments, checkKeywordArgumentsNode, null);
8190
}
8291

8392
private static class CheckKeywordArgumentsNode extends RubyContextNode implements BiConsumerNode {
8493

85-
private final boolean checkAllowedKeywords;
8694
private final boolean doesNotAcceptExtraArguments;
8795
private final int required;
8896
@CompilationFinal(dimensions = 1) private final RubySymbol[] allowedKeywords;
8997

9098
private final ConditionProfile isSymbolProfile = ConditionProfile.create();
9199
private final BranchProfile tooManyKeywordsProfile = BranchProfile.create();
92-
private final BranchProfile unknownKeywordProfile;
100+
private final BranchProfile unknownKeywordProfile = BranchProfile.create();
93101

94102
public CheckKeywordArgumentsNode(RubyLanguage language, Arity arity) {
95-
checkAllowedKeywords = !arity.hasKeywordsRest();
103+
assert !arity.hasKeywordsRest();
96104
doesNotAcceptExtraArguments = !arity.hasRest() && arity.getOptional() == 0;
97105
required = arity.getRequired();
98-
allowedKeywords = checkAllowedKeywords ? keywordsAsSymbols(language, arity) : null;
99-
unknownKeywordProfile = checkAllowedKeywords ? BranchProfile.create() : null;
106+
allowedKeywords = keywordsAsSymbols(language, arity);
100107
}
101108

102109
@Override
103110
public void accept(VirtualFrame frame, Object key, Object value, Object state) {
104111
if (isSymbolProfile.profile(key instanceof RubySymbol)) {
105-
if (checkAllowedKeywords && !keywordAllowed(key)) {
112+
if (!keywordAllowed(key)) {
106113
unknownKeywordProfile.enter();
107114
throw new RaiseException(
108115
getContext(),
109116
coreExceptions().argumentErrorUnknownKeyword((RubySymbol) key, this));
110117
}
111118
} else {
112-
final int given = RubyArguments.getArgumentsCount(frame); // -1 for keyword hash, +1 for reject Hash with non-Symbol key
119+
// the Hash would be split and a reject Hash be created to hold non-Symbols when there is no **kwrest parameter,
120+
// so we need to check if an extra argument is allowed
121+
final int given = RubyArguments.getArgumentsCount(frame); // -1 for keyword hash, +1 for reject Hash with non-Symbol keys
113122
if (doesNotAcceptExtraArguments && given > required) {
114123
tooManyKeywordsProfile.enter();
115124
throw new RaiseException(getContext(), coreExceptions().argumentError(given, required, this));
116125
}
117126
}
127+
118128
}
119129

120130
@ExplodeLoop

0 commit comments

Comments
 (0)