Skip to content

Commit 11d5b57

Browse files
eregonandrykonchin
authored andcommitted
Implement Hash pattern matching and the rest of pattern matching
1 parent 7ff628f commit 11d5b57

21 files changed

+636
-159
lines changed

spec/ruby/language/pattern_matching_spec.rb

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -770,11 +770,7 @@ def obj.deconstruct; "" end
770770
it "accepts a subclass of Array from #deconstruct" do
771771
obj = Object.new
772772
def obj.deconstruct
773-
subarray = Class.new(Array).new(2)
774-
def subarray.[](n)
775-
n
776-
end
777-
subarray
773+
Class.new(Array).new([0, 1])
778774
end
779775

780776
eval(<<~RUBY).should == true
@@ -1004,7 +1000,7 @@ def obj.deconstruct; [1] end
10041000
in {"a" => 1}
10051001
end
10061002
RUBY
1007-
}.should raise_error(SyntaxError, /unexpected/)
1003+
}.should raise_error(SyntaxError, /unexpected|expected a label as the key in the hash pattern/)
10081004
end
10091005

10101006
it "does not support string interpolation in keys" do
@@ -1016,7 +1012,7 @@ def obj.deconstruct; [1] end
10161012
in {"#{x}": 1}
10171013
end
10181014
RUBY
1019-
}.should raise_error(SyntaxError, /symbol literal with interpolation is not allowed/)
1015+
}.should raise_error(SyntaxError, /symbol literal with interpolation is not allowed|expected a label as the key in the hash pattern/)
10201016
end
10211017

10221018
it "raise SyntaxError when keys duplicate in pattern" do
Lines changed: 0 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,10 @@
1-
fails:Pattern matching variable pattern supports using any name with _ at the beginning in a pattern several times
2-
fails:Pattern matching alternative pattern matches if any of patterns matches
31
fails:Pattern matching alternative pattern does not support variable binding
4-
fails:Pattern matching alternative pattern support underscore prefixed variables in alternation
5-
fails:Pattern matching AS pattern binds a variable to a value if pattern matches
6-
fails:Pattern matching AS pattern can be used as a nested pattern
7-
fails:Pattern matching Hash pattern supports form id: pat, id: pat, ...
8-
fails:Pattern matching Hash pattern supports a: which means a: a
9-
fails:Pattern matching Hash pattern can mix key (a:) and key-value (a: b) declarations
10-
fails:Pattern matching Hash pattern does not match object if Constant === object returns false
11-
fails:Pattern matching Hash pattern does not match object without #deconstruct_keys method
12-
fails:Pattern matching Hash pattern does not match object if #deconstruct_keys method does not return Hash
13-
fails:Pattern matching Hash pattern does not match object if #deconstruct_keys method returns Hash with non-symbol keys
14-
fails:Pattern matching Hash pattern does not match object if elements of Hash returned by #deconstruct_keys method does not match values in pattern
15-
fails:Pattern matching Hash pattern passes keys specified in pattern as arguments to #deconstruct_keys method
16-
fails:Pattern matching Hash pattern passes keys specified in pattern to #deconstruct_keys method if pattern contains double splat operator **
17-
fails:Pattern matching Hash pattern passes nil to #deconstruct_keys method if pattern contains double splat operator **rest
18-
fails:Pattern matching Hash pattern binds variables
19-
fails:Pattern matching Hash pattern supports double splat operator **rest
20-
fails:Pattern matching Hash pattern treats **nil like there should not be any other keys in a matched Hash
21-
fails:Pattern matching Hash pattern matches anything with **
22-
fails:Pattern matching refinements are used for #deconstruct_keys
23-
fails:Pattern matching Array pattern accepts a subclass of Array from #deconstruct
242
fails:Pattern matching Array pattern calls #deconstruct once for multiple patterns, caching the result
253
fails:Pattern matching find pattern captures both preceding and following elements to the pattern
264
fails:Pattern matching warning when one-line form warns about pattern matching is experimental feature
27-
fails:Pattern matching alternative pattern can be used as a nested pattern
28-
fails:Pattern matching Array pattern can be used as a nested pattern
29-
fails:Pattern matching Hash pattern can be used as a nested pattern
305
fails:Pattern matching find pattern can be nested
316
fails:Pattern matching find pattern can be nested with an array pattern
327
fails:Pattern matching find pattern can be nested within a hash pattern
338
fails:Pattern matching find pattern can nest hash and array patterns
34-
fails:Pattern matching can omit parentheses in one line pattern matching
35-
fails:Pattern matching Hash pattern supports form Constant(id: pat, id: pat, ...)
36-
fails:Pattern matching Hash pattern supports form Constant[id: pat, id: pat, ...]
37-
fails:Pattern matching Hash pattern supports form {id: pat, id: pat, ...}
38-
fails:Pattern matching Hash pattern supports 'string': key literal
39-
fails:Pattern matching Hash pattern matches an object with #deconstruct_keys method which returns a Hash with equal keys and each value in Hash matches value in pattern
40-
fails:Pattern matching Hash pattern calls #deconstruct_keys per pattern
41-
fails:Pattern matching Hash pattern can match partially
42-
fails:Pattern matching Hash pattern matches {} with {}
43-
fails:Pattern matching refinements are used for #deconstruct
449
fails:Pattern matching variable pattern does not support using variable name (except _) several times
45-
fails:Pattern matching Hash pattern does not support non-symbol keys
46-
fails:Pattern matching Hash pattern does not support string interpolation in keys
4710
fails:Pattern matching Hash pattern raise SyntaxError when keys duplicate in pattern
48-
fails:Pattern matching supports pinning expressions in hash pattern
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/*
2+
* Copyright (c) 2024 Oracle and/or its affiliates. All rights reserved. This
3+
* code is released under a tri EPL/GPL/LGPL license. You can use it,
4+
* redistribute it and/or modify it under the terms of the:
5+
*
6+
* Eclipse Public License version 2.0, or
7+
* GNU General Public License version 2, or
8+
* GNU Lesser General Public License version 2.1.
9+
*/
10+
package org.truffleruby.core.array;
11+
12+
import com.oracle.truffle.api.dsl.Cached;
13+
import com.oracle.truffle.api.frame.VirtualFrame;
14+
import com.oracle.truffle.api.profiles.InlinedBranchProfile;
15+
import com.oracle.truffle.api.profiles.InlinedConditionProfile;
16+
import org.truffleruby.core.cast.BooleanCastNode;
17+
import org.truffleruby.language.RubyContextSourceNode;
18+
import org.truffleruby.language.RubyNode;
19+
20+
import com.oracle.truffle.api.dsl.NodeChild;
21+
import com.oracle.truffle.api.dsl.Specialization;
22+
import org.truffleruby.language.control.RaiseException;
23+
import org.truffleruby.language.dispatch.DispatchNode;
24+
25+
import static org.truffleruby.language.dispatch.DispatchConfiguration.PUBLIC;
26+
27+
// Implemented in Java because the call to #deconstruct needs to honor refinements
28+
@NodeChild(value = "valueNode", type = RubyNode.class)
29+
public abstract class ArrayDeconstructNode extends RubyContextSourceNode {
30+
31+
abstract RubyNode getValueNode();
32+
33+
@Specialization
34+
Object deconstruct(VirtualFrame frame, Object toMatch,
35+
@Cached DispatchNode respondToNode,
36+
@Cached BooleanCastNode booleanCastNode,
37+
@Cached DispatchNode deconstructNode,
38+
@Cached InlinedConditionProfile hasDeconstructProfile,
39+
@Cached InlinedBranchProfile errorProfile) {
40+
if (hasDeconstructProfile.profile(this, booleanCastNode.execute(this,
41+
respondToNode.callWithFrame(PUBLIC, frame, toMatch, "respond_to?", coreSymbols().DECONSTRUCT)))) {
42+
Object deconstructed = deconstructNode.callWithFrame(PUBLIC, frame, toMatch, "deconstruct");
43+
if (deconstructed instanceof RubyArray) {
44+
return deconstructed;
45+
} else {
46+
errorProfile.enter(this);
47+
throw new RaiseException(getContext(),
48+
coreExceptions().typeError("deconstruct must return Array", this));
49+
}
50+
} else {
51+
return nil;
52+
}
53+
}
54+
55+
@Override
56+
public RubyNode cloneUninitialized() {
57+
return ArrayDeconstructNodeGen.create(getValueNode().cloneUninitialized()).copyFlags(this);
58+
}
59+
60+
}

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

Lines changed: 20 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -9,44 +9,43 @@
99
*/
1010
package org.truffleruby.core.array;
1111

12-
import com.oracle.truffle.api.profiles.ConditionProfile;
12+
import com.oracle.truffle.api.dsl.Fallback;
13+
import com.oracle.truffle.api.dsl.NodeChild;
14+
import com.oracle.truffle.api.dsl.Specialization;
1315
import org.truffleruby.language.RubyContextSourceNode;
1416
import org.truffleruby.language.RubyNode;
1517

16-
import com.oracle.truffle.api.frame.VirtualFrame;
18+
@NodeChild(value = "valueNode", type = RubyNode.class)
19+
public abstract class ArrayPatternLengthCheckNode extends RubyContextSourceNode {
1720

18-
public final class ArrayPatternLengthCheckNode extends RubyContextSourceNode {
19-
20-
@Child RubyNode currentValueToMatch;
2121
final int patternLength;
2222
final boolean hasRest;
2323

24-
final ConditionProfile isArrayProfile = ConditionProfile.create();
25-
26-
public ArrayPatternLengthCheckNode(int patternLength, RubyNode currentValueToMatch, boolean hasRest) {
27-
this.currentValueToMatch = currentValueToMatch;
24+
public ArrayPatternLengthCheckNode(int patternLength, boolean hasRest) {
2825
this.patternLength = patternLength;
2926
this.hasRest = hasRest;
3027
}
3128

32-
@Override
33-
public Object execute(VirtualFrame frame) {
34-
Object matchArray = currentValueToMatch.execute(frame);
35-
if (isArrayProfile.profile(matchArray instanceof RubyArray)) {
36-
long size = ((RubyArray) matchArray).getArraySize();
37-
if (hasRest) {
38-
return patternLength <= size;
39-
} else {
40-
return patternLength == size;
41-
}
29+
abstract RubyNode getValueNode();
30+
31+
@Specialization
32+
boolean arrayLengthCheck(RubyArray matchArray) {
33+
int size = matchArray.size;
34+
if (hasRest) {
35+
return patternLength <= size;
4236
} else {
43-
return false;
37+
return patternLength == size;
4438
}
4539
}
4640

41+
@Fallback
42+
boolean notArray(Object value) {
43+
return false;
44+
}
45+
4746
@Override
4847
public RubyNode cloneUninitialized() {
49-
return new ArrayPatternLengthCheckNode(patternLength, currentValueToMatch.cloneUninitialized(), hasRest)
48+
return ArrayPatternLengthCheckNodeGen.create(patternLength, hasRest, getValueNode().cloneUninitialized())
5049
.copyFlags(this);
5150
}
5251
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/*
2+
* Copyright (c) 2024 Oracle and/or its affiliates. All rights reserved. This
3+
* code is released under a tri EPL/GPL/LGPL license. You can use it,
4+
* redistribute it and/or modify it under the terms of the:
5+
*
6+
* Eclipse Public License version 2.0, or
7+
* GNU General Public License version 2, or
8+
* GNU Lesser General Public License version 2.1.
9+
*/
10+
package org.truffleruby.core.array;
11+
12+
import com.oracle.truffle.api.CompilerDirectives.CompilationFinal;
13+
import com.oracle.truffle.api.frame.VirtualFrame;
14+
import org.truffleruby.language.RubyContextSourceNode;
15+
import org.truffleruby.language.RubyGuards;
16+
import org.truffleruby.language.RubyNode;
17+
18+
public final class ArrayStaticLiteralNode extends RubyContextSourceNode {
19+
20+
@CompilationFinal(dimensions = 1) private final Object[] values;
21+
22+
public ArrayStaticLiteralNode(Object[] values) {
23+
assert allValuesArePrimitiveOrImmutable(values);
24+
this.values = values;
25+
}
26+
27+
private static boolean allValuesArePrimitiveOrImmutable(Object[] values) {
28+
for (Object value : values) {
29+
assert RubyGuards.isPrimitiveOrImmutable(value);
30+
}
31+
return true;
32+
}
33+
34+
@Override
35+
public RubyArray execute(VirtualFrame frame) {
36+
// Copying here is the simplest since we need to return a mutable Array.
37+
// An alternative would be to use COW via DelegatedArrayStorage.
38+
return createArray(ArrayUtils.copy(values));
39+
}
40+
41+
@Override
42+
public RubyNode cloneUninitialized() {
43+
return new ArrayStaticLiteralNode(values);
44+
}
45+
46+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/*
2+
* Copyright (c) 2024 Oracle and/or its affiliates. All rights reserved. This
3+
* code is released under a tri EPL/GPL/LGPL license. You can use it,
4+
* redistribute it and/or modify it under the terms of the:
5+
*
6+
* Eclipse Public License version 2.0, or
7+
* GNU General Public License version 2, or
8+
* GNU Lesser General Public License version 2.1.
9+
*/
10+
package org.truffleruby.core.hash;
11+
12+
import static org.truffleruby.language.dispatch.DispatchConfiguration.PUBLIC;
13+
14+
import org.truffleruby.core.cast.BooleanCastNode;
15+
import org.truffleruby.language.RubyContextSourceNode;
16+
import org.truffleruby.language.RubyNode;
17+
import org.truffleruby.language.control.RaiseException;
18+
import org.truffleruby.language.dispatch.DispatchNode;
19+
20+
import com.oracle.truffle.api.dsl.Cached;
21+
import com.oracle.truffle.api.dsl.NodeChild;
22+
import com.oracle.truffle.api.dsl.Specialization;
23+
import com.oracle.truffle.api.frame.VirtualFrame;
24+
import com.oracle.truffle.api.profiles.InlinedBranchProfile;
25+
import com.oracle.truffle.api.profiles.InlinedConditionProfile;
26+
27+
// Implemented in Java because the call to #deconstruct_keys needs to honor refinements
28+
@NodeChild(value = "valueNode", type = RubyNode.class)
29+
@NodeChild(value = "keysNode", type = RubyNode.class)
30+
public abstract class HashDeconstructKeysNode extends RubyContextSourceNode {
31+
32+
abstract RubyNode getValueNode();
33+
34+
abstract RubyNode getKeysNode();
35+
36+
@Specialization
37+
Object deconstructKeys(VirtualFrame frame, Object toMatch, Object keys,
38+
@Cached DispatchNode respondToNode,
39+
@Cached BooleanCastNode booleanCastNode,
40+
@Cached DispatchNode deconstructKeysNode,
41+
@Cached InlinedConditionProfile hasDeconstructKeysProfile,
42+
@Cached InlinedBranchProfile errorProfile) {
43+
if (hasDeconstructKeysProfile.profile(this, booleanCastNode.execute(this,
44+
respondToNode.callWithFrame(PUBLIC, frame, toMatch, "respond_to?", coreSymbols().DECONSTRUCT_KEYS)))) {
45+
Object deconstructed = deconstructKeysNode.callWithFrame(PUBLIC, frame, toMatch, "deconstruct_keys", keys);
46+
if (deconstructed instanceof RubyHash) {
47+
return deconstructed;
48+
} else {
49+
errorProfile.enter(this);
50+
throw new RaiseException(getContext(),
51+
coreExceptions().typeError("deconstruct_keys must return Hash", this));
52+
}
53+
} else {
54+
return nil;
55+
}
56+
}
57+
58+
@Override
59+
public RubyNode cloneUninitialized() {
60+
return HashDeconstructKeysNodeGen
61+
.create(getValueNode().cloneUninitialized(), getKeysNode().cloneUninitialized()).copyFlags(this);
62+
}
63+
64+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*
2+
* Copyright (c) 2024 Oracle and/or its affiliates. All rights reserved. This
3+
* code is released under a tri EPL/GPL/LGPL license. You can use it,
4+
* redistribute it and/or modify it under the terms of the:
5+
*
6+
* Eclipse Public License version 2.0, or
7+
* GNU General Public License version 2, or
8+
* GNU Lesser General Public License version 2.1.
9+
*/
10+
package org.truffleruby.core.hash;
11+
12+
import com.oracle.truffle.api.dsl.ImportStatic;
13+
import com.oracle.truffle.api.dsl.NodeChild;
14+
import com.oracle.truffle.api.dsl.Specialization;
15+
import com.oracle.truffle.api.frame.Frame;
16+
import com.oracle.truffle.api.library.CachedLibrary;
17+
import org.truffleruby.collections.PEBiFunction;
18+
import org.truffleruby.core.hash.library.HashStoreLibrary;
19+
import org.truffleruby.core.symbol.RubySymbol;
20+
import org.truffleruby.language.NotProvided;
21+
import org.truffleruby.language.RubyContextSourceNode;
22+
import org.truffleruby.language.RubyNode;
23+
24+
/** The same as {@link HashNodes.GetOrUndefinedNode} but with a static key. */
25+
@ImportStatic(HashGuards.class)
26+
@NodeChild(value = "hashNode", type = RubyNode.class)
27+
public abstract class HashGetOrUndefinedNode extends RubyContextSourceNode implements PEBiFunction {
28+
29+
private final RubySymbol key;
30+
31+
public HashGetOrUndefinedNode(RubySymbol key) {
32+
this.key = key;
33+
}
34+
35+
abstract RubyNode getHashNode();
36+
37+
@Specialization(limit = "hashStrategyLimit()")
38+
Object get(RubyHash hash,
39+
@CachedLibrary("hash.store") HashStoreLibrary hashes) {
40+
return hashes.lookupOrDefault(hash.store, null, hash, key, this);
41+
}
42+
43+
@Override
44+
public Object accept(Frame frame, Object hash, Object key) {
45+
return NotProvided.INSTANCE;
46+
}
47+
48+
@Override
49+
public RubyNode cloneUninitialized() {
50+
var copy = HashGetOrUndefinedNodeGen.create(key, getHashNode().cloneUninitialized());
51+
return copy.copyFlags(this);
52+
}
53+
54+
}

0 commit comments

Comments
 (0)