Skip to content

Commit 643aa8d

Browse files
committed
[GR-34365] Implement Pattern Matching in TruffleRuby (GSoC) (#2683)
PullRequest: truffleruby/3780
2 parents d42c333 + d902957 commit 643aa8d

33 files changed

+8356
-5854
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ Compatibility:
1111
* Fix `Hash#shift` when Hash is empty but has initial default value or initial default proc (@itarato).
1212
* Make `Array#shuffle` produce the same results as CRuby (@rwstauner).
1313
* Add `Process.argv0` method (@andrykonchin).
14+
* Add support for array pattern matching. This is opt-in via `--pattern-matching` since pattern matching is not fully supported yet. (#2683, @razetime).
1415

1516
Performance:
1617

spec/ruby/language/pattern_matching_spec.rb

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,7 @@
205205
in []
206206
end
207207
RUBY
208-
}.should raise_error(SyntaxError, /syntax error, unexpected `in'/)
208+
}.should raise_error(SyntaxError, /syntax error, unexpected `in'|\(eval\):3: syntax error, unexpected keyword_in/)
209209

210210
-> {
211211
eval <<~RUBY
@@ -214,7 +214,7 @@
214214
when 1 == 1
215215
end
216216
RUBY
217-
}.should raise_error(SyntaxError, /syntax error, unexpected `when'/)
217+
}.should raise_error(SyntaxError, /syntax error, unexpected `when'|\(eval\):3: syntax error, unexpected keyword_when/)
218218
end
219219

220220
it "checks patterns until the first matching" do
@@ -251,6 +251,18 @@
251251
}.should raise_error(NoMatchingPatternError, /\[0, 1\]/)
252252
end
253253

254+
it "raises NoMatchingPatternError if no pattern matches and evaluates the expression only once" do
255+
evals = 0
256+
-> {
257+
eval <<~RUBY
258+
case (evals += 1; [0, 1])
259+
in [0]
260+
end
261+
RUBY
262+
}.should raise_error(NoMatchingPatternError, /\[0, 1\]/)
263+
evals.should == 1
264+
end
265+
254266
it "does not allow calculation or method calls in a pattern" do
255267
-> {
256268
eval <<~RUBY
Lines changed: 9 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,4 @@
1-
fails:Pattern matching binds variables
2-
fails:Pattern matching cannot mix in and when operators
3-
fails:Pattern matching raises NoMatchingPatternError if no pattern matches and no else clause
4-
fails:Pattern matching guards supports if guard
5-
fails:Pattern matching guards supports unless guard
6-
fails:Pattern matching guards makes bound variables visible in guard
7-
fails:Pattern matching guards does not evaluate guard if pattern does not match
8-
fails:Pattern matching guards takes guards into account when there are several matching patterns
9-
fails:Pattern matching guards executes else clause if no guarded pattern matches
10-
fails:Pattern matching guards raises NoMatchingPatternError if no guarded pattern matches and no else clause
11-
fails:Pattern matching variable pattern matches a value and binds variable name to this value
12-
fails:Pattern matching variable pattern makes bounded variable visible outside a case statement scope
13-
fails:Pattern matching variable pattern create local variables even if a pattern doesn't match
14-
fails:Pattern matching variable pattern allow using _ name to drop values
15-
fails:Pattern matching variable pattern supports using _ in a pattern several times
161
fails:Pattern matching variable pattern supports using any name with _ at the beginning in a pattern several times
17-
fails:Pattern matching variable pattern does not support using variable name (except _) several times
182
fails:Pattern matching variable pattern supports existing variables in a pattern specified with ^ operator
193
fails:Pattern matching variable pattern allows applying ^ operator to bound variables
204
fails:Pattern matching variable pattern requires bound variable to be specified in a pattern before ^ operator when it relies on a bound variable
@@ -24,20 +8,9 @@ fails:Pattern matching alternative pattern support underscore prefixed variables
248
fails:Pattern matching AS pattern binds a variable to a value if pattern matches
259
fails:Pattern matching AS pattern can be used as a nested pattern
2610
fails:Pattern matching Array pattern supports form Constant(pat, pat, ...)
27-
fails:Pattern matching Array pattern supports form pat, pat, ...
28-
fails:Pattern matching Array pattern does not match object if Constant === object returns false
29-
fails:Pattern matching Array pattern does not match object without #deconstruct method
30-
fails:Pattern matching Array pattern raises TypeError if #deconstruct method does not return array
31-
fails:Pattern matching Array pattern does not match object if elements of array returned by #deconstruct method does not match elements in pattern
32-
fails:Pattern matching Array pattern binds variables
33-
fails:Pattern matching Array pattern supports splat operator *rest
34-
fails:Pattern matching Array pattern does match partially from the array beginning if list + , syntax used
35-
fails:Pattern matching Array pattern matches anything with *
3611
fails:Pattern matching Hash pattern supports form id: pat, id: pat, ...
3712
fails:Pattern matching Hash pattern supports a: which means a: a
3813
fails:Pattern matching Hash pattern can mix key (a:) and key-value (a: b) declarations
39-
fails:Pattern matching Hash pattern does not support string interpolation in keys
40-
fails:Pattern matching Hash pattern raise SyntaxError when keys duplicate in pattern
4114
fails:Pattern matching Hash pattern does not match object if Constant === object returns false
4215
fails:Pattern matching Hash pattern does not match object without #deconstruct_keys method
4316
fails:Pattern matching Hash pattern does not match object if #deconstruct_keys method does not return Hash
@@ -51,17 +24,10 @@ fails:Pattern matching Hash pattern supports double splat operator **rest
5124
fails:Pattern matching Hash pattern treats **nil like there should not be any other keys in a matched Hash
5225
fails:Pattern matching Hash pattern matches anything with **
5326
fails:Pattern matching refinements are used for #deconstruct_keys
54-
fails:Pattern matching does not allow calculation or method calls in a pattern
55-
fails:Pattern matching Hash pattern does not support non-symbol keys
5627
fails:Pattern matching Array pattern accepts a subclass of Array from #deconstruct
5728
fails:Pattern matching can be standalone assoc operator that deconstructs value
5829
fails:Pattern matching Array pattern calls #deconstruct once for multiple patterns, caching the result
59-
fails:Pattern matching find pattern captures preceding elements to the pattern
60-
fails:Pattern matching find pattern captures following elements to the pattern
6130
fails:Pattern matching find pattern captures both preceding and following elements to the pattern
62-
fails:Pattern matching find pattern can capture the entirety of the pattern
63-
fails:Pattern matching find pattern will match an empty Array-like structure
64-
fails:Pattern matching warning when regular form does not warn about pattern matching is experimental feature
6531
fails:Pattern matching warning when one-line form warns about pattern matching is experimental feature
6632
fails:Pattern matching alternative pattern can be used as a nested pattern
6733
fails:Pattern matching Array pattern can be used as a nested pattern
@@ -76,3 +42,12 @@ fails:Pattern matching supports pinning class variables
7642
fails:Pattern matching supports pinning global variables
7743
fails:Pattern matching supports pinning expressions
7844
fails:Pattern matching warning when one-line form does not warn about pattern matching is experimental feature
45+
fails:Pattern matching Hash pattern supports form Constant(id: pat, id: pat, ...)
46+
fails:Pattern matching Hash pattern supports form Constant[id: pat, id: pat, ...]
47+
fails:Pattern matching Hash pattern supports form {id: pat, id: pat, ...}
48+
fails:Pattern matching Hash pattern supports 'string': key literal
49+
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
50+
fails:Pattern matching Hash pattern calls #deconstruct_keys per pattern
51+
fails:Pattern matching Hash pattern can match partially
52+
fails:Pattern matching Hash pattern matches {} with {}
53+
fails:Pattern matching refinements are used for #deconstruct

src/main/java/org/truffleruby/core/CoreLibrary.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,7 @@ public class CoreLibrary {
233233

234234
public final GlobalVariables globalVariables;
235235
public final BindingLocalVariablesObject interactiveBindingLocalVariablesObject;
236+
public final RubyClass noMatchingPatternErrorClass;
236237

237238
@CompilationFinal private RubyClass eagainWaitReadable;
238239
@CompilationFinal private RubyClass eagainWaitWritable;
@@ -331,6 +332,7 @@ public CoreLibrary(RubyContext context, RubyLanguage language) {
331332
unsupportedMessageErrorClass = defineClass(polyglotModule, standardErrorClass, "UnsupportedMessageError");
332333
polyglotForeignObjectClass = defineClass(polyglotModule, objectClass, "ForeignObject");
333334
polyglotForeignClasses = new RubyClass[ForeignClassNode.Trait.COMBINATIONS];
335+
noMatchingPatternErrorClass = defineClass(standardErrorClass, "NoMatchingPatternError");
334336

335337
// StandardError > RuntimeError
336338
runtimeErrorClass = defineClass(standardErrorClass, "RuntimeError");
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
* Copyright (c) 2022, 2023 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.profiles.ConditionProfile;
13+
import org.truffleruby.language.RubyContextSourceNode;
14+
import org.truffleruby.language.RubyNode;
15+
16+
import com.oracle.truffle.api.frame.VirtualFrame;
17+
18+
public class ArrayPatternLengthCheckNode extends RubyContextSourceNode {
19+
20+
@Child RubyNode currentValueToMatch;
21+
final int patternLength;
22+
final boolean hasRest;
23+
24+
final ConditionProfile isArrayProfile = ConditionProfile.create();
25+
26+
public ArrayPatternLengthCheckNode(int patternLength, RubyNode currentValueToMatch, boolean hasRest) {
27+
this.currentValueToMatch = currentValueToMatch;
28+
this.patternLength = patternLength;
29+
this.hasRest = hasRest;
30+
}
31+
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+
}
42+
} else {
43+
return false;
44+
}
45+
}
46+
47+
@Override
48+
public RubyNode cloneUninitialized() {
49+
return new ArrayPatternLengthCheckNode(patternLength, currentValueToMatch.cloneUninitialized(), hasRest)
50+
.copyFlags(this);
51+
}
52+
}

src/main/java/org/truffleruby/core/exception/CoreExceptions.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,15 @@ public RubyException noMemoryError(Node currentNode, OutOfMemoryError javaThrowa
378378
javaThrowable);
379379
}
380380

381+
// NoMatchingPatternError
382+
383+
@TruffleBoundary
384+
public RubyException noMatchingPatternError(Object errorMessage, Node currentNode) {
385+
assert RubyStringLibrary.getUncached().isRubyString(errorMessage);
386+
RubyClass exceptionClass = context.getCoreLibrary().noMatchingPatternErrorClass;
387+
return ExceptionOperations.createRubyException(context, exceptionClass, errorMessage, currentNode, null);
388+
}
389+
381390
// Errno
382391

383392
@TruffleBoundary

src/main/java/org/truffleruby/core/string/TStringConstants.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ public class TStringConstants {
4343
}
4444
}
4545

46+
47+
public static final TruffleString __FILE__ = ascii("__FILE__");
48+
public static final TruffleString __LINE__ = ascii("__LINE__");
49+
public static final TruffleString __ENCODING__ = ascii("__ENCODING__");
4650
public static final TruffleString AMPERSAND = ascii("&");
4751
public static final TruffleString AMPERSAND_AMPERSAND = ascii("&&");
4852
public static final TruffleString AMPERSAND_DOT = ascii("&.");
@@ -93,6 +97,7 @@ public class TStringConstants {
9397
public static final TruffleString RBRACKET = ascii("]");
9498
public static final TruffleString RCURLY = ascii("}");
9599
public static final TruffleString RPAREN = ascii(")");
100+
public static final TruffleString SELF = ascii("self");
96101
public static final TruffleString SEMICOLON = ascii(";");
97102
public static final TruffleString SLASH = ascii("/");
98103
public static final TruffleString STAR = ascii("*");
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*
2+
* Copyright (c) 2022, 2023 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.language.control;
11+
12+
import org.truffleruby.language.RubyContextSourceNode;
13+
import org.truffleruby.language.RubyNode;
14+
15+
import com.oracle.truffle.api.frame.VirtualFrame;
16+
17+
public final class ExecuteAndReturnTrueNode extends RubyContextSourceNode {
18+
19+
@Child RubyNode child;
20+
21+
public ExecuteAndReturnTrueNode(RubyNode child) {
22+
this.child = child;
23+
}
24+
25+
@Override
26+
public Object execute(VirtualFrame frame) {
27+
child.doExecuteVoid(frame);
28+
return true;
29+
}
30+
31+
@Override
32+
public RubyNode cloneUninitialized() {
33+
return new ExecuteAndReturnTrueNode(child.cloneUninitialized()).copyFlags(this);
34+
}
35+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
* Copyright (c) 2022, 2023 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.language.control;
11+
12+
import com.oracle.truffle.api.dsl.Cached;
13+
import com.oracle.truffle.api.dsl.NodeChild;
14+
import com.oracle.truffle.api.dsl.Specialization;
15+
import org.truffleruby.language.RubyContextSourceNode;
16+
17+
import org.truffleruby.language.RubyNode;
18+
import org.truffleruby.language.dispatch.DispatchNode;
19+
20+
@NodeChild(value = "expressionNode", type = RubyNode.class)
21+
public abstract class NoMatchingPatternNode extends RubyContextSourceNode {
22+
23+
protected abstract RubyNode getExpressionNode();
24+
25+
@Specialization
26+
protected Object noMatchingPattern(Object expression,
27+
@Cached DispatchNode inspectNode) {
28+
Object inspected = inspectNode.call(coreLibrary().truffleTypeModule, "rb_inspect", expression);
29+
throw new RaiseException(getContext(), coreExceptions().noMatchingPatternError(inspected, this));
30+
}
31+
32+
@Override
33+
public RubyNode cloneUninitialized() {
34+
return NoMatchingPatternNodeGen.create(getExpressionNode().cloneUninitialized()).copyFlags(this);
35+
}
36+
}

src/main/java/org/truffleruby/language/dispatch/RubyCallNodeParameters.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,8 @@ public RubyCallNodeParameters(
3131
RubyNode block,
3232
ArgumentsDescriptor descriptor,
3333
RubyNode[] arguments,
34-
boolean isSplatted,
3534
boolean ignoreVisibility) {
36-
this(receiver, methodName, block, descriptor, arguments, isSplatted, ignoreVisibility, false, false, false);
35+
this(receiver, methodName, block, descriptor, arguments, false, ignoreVisibility, false, false, false);
3736
}
3837

3938
public RubyCallNodeParameters(

0 commit comments

Comments
 (0)