Skip to content

Commit 7ff628f

Browse files
eregonandrykonchin
authored andcommitted
Add YARPPatternMatchingTranslator based on PatternMatchingTranslator
* But with several cleanups and fixes. * Also allow Prism error messages in pattern matching specs.
1 parent a6f5a6f commit 7ff628f

File tree

6 files changed

+272
-80
lines changed

6 files changed

+272
-80
lines changed

spec/ruby/language/pattern_matching_spec.rb

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@
207207
in []
208208
end
209209
RUBY
210-
}.should raise_error(SyntaxError, /syntax error, unexpected `in'|\(eval\):3: syntax error, unexpected keyword_in/)
210+
}.should raise_error(SyntaxError, /syntax error, unexpected `in'|\(eval\):3: syntax error, unexpected keyword_in|cannot parse the expression/)
211211

212212
-> {
213213
eval <<~RUBY
@@ -216,7 +216,7 @@
216216
when 1 == 1
217217
end
218218
RUBY
219-
}.should raise_error(SyntaxError, /syntax error, unexpected `when'|\(eval\):3: syntax error, unexpected keyword_when/)
219+
}.should raise_error(SyntaxError, /syntax error, unexpected `when'|\(eval\):3: syntax error, unexpected keyword_when|cannot parse the expression/)
220220
end
221221

222222
it "checks patterns until the first matching" do
@@ -273,7 +273,7 @@
273273
true
274274
end
275275
RUBY
276-
}.should raise_error(SyntaxError, /unexpected/)
276+
}.should raise_error(SyntaxError, /unexpected|expected a delimiter after the predicates of a `when` clause/)
277277
end
278278

279279
it "evaluates the case expression once for multiple patterns, caching the result" do
@@ -1398,15 +1398,26 @@ def ===(obj)
13981398
RUBY
13991399

14001400
eval(<<~RUBY).should == true
1401-
case {name: '2.6', released_at: Time.new(2018, 12, 25)}
1402-
in {released_at: ^(Time.new(2010)..Time.new(2020))}
1401+
case 0
1402+
in ^(0+0)
14031403
true
14041404
end
14051405
RUBY
1406+
end
14061407

1408+
it "supports pinning expressions in array pattern" do
14071409
eval(<<~RUBY).should == true
1408-
case 0
1409-
in ^(0+0)
1410+
case [3]
1411+
in [^(1+2)]
1412+
true
1413+
end
1414+
RUBY
1415+
end
1416+
1417+
it "supports pinning expressions in hash pattern" do
1418+
eval(<<~RUBY).should == true
1419+
case {name: '2.6', released_at: Time.new(2018, 12, 25)}
1420+
in {released_at: ^(Time.new(2010)..Time.new(2020))}
14101421
true
14111422
end
14121423
RUBY
Lines changed: 1 addition & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,9 @@
11
fails:Pattern matching variable pattern supports using any name with _ at the beginning in a pattern several times
2-
fails:Pattern matching variable pattern supports existing variables in a pattern specified with ^ operator
3-
fails:Pattern matching variable pattern allows applying ^ operator to bound variables
42
fails:Pattern matching alternative pattern matches if any of patterns matches
53
fails:Pattern matching alternative pattern does not support variable binding
64
fails:Pattern matching alternative pattern support underscore prefixed variables in alternation
75
fails:Pattern matching AS pattern binds a variable to a value if pattern matches
86
fails:Pattern matching AS pattern can be used as a nested pattern
9-
fails:Pattern matching Array pattern supports form Constant(pat, pat, ...)
107
fails:Pattern matching Hash pattern supports form id: pat, id: pat, ...
118
fails:Pattern matching Hash pattern supports a: which means a: a
129
fails:Pattern matching Hash pattern can mix key (a:) and key-value (a: b) declarations
@@ -24,7 +21,6 @@ fails:Pattern matching Hash pattern treats **nil like there should not be any ot
2421
fails:Pattern matching Hash pattern matches anything with **
2522
fails:Pattern matching refinements are used for #deconstruct_keys
2623
fails:Pattern matching Array pattern accepts a subclass of Array from #deconstruct
27-
fails:Pattern matching can be standalone assoc operator that deconstructs value
2824
fails:Pattern matching Array pattern calls #deconstruct once for multiple patterns, caching the result
2925
fails:Pattern matching find pattern captures both preceding and following elements to the pattern
3026
fails:Pattern matching warning when one-line form warns about pattern matching is experimental feature
@@ -36,11 +32,6 @@ fails:Pattern matching find pattern can be nested with an array pattern
3632
fails:Pattern matching find pattern can be nested within a hash pattern
3733
fails:Pattern matching find pattern can nest hash and array patterns
3834
fails:Pattern matching can omit parentheses in one line pattern matching
39-
fails:Pattern matching supports pinning instance variables
40-
fails:Pattern matching supports pinning class variables
41-
fails:Pattern matching supports pinning global variables
42-
fails:Pattern matching supports pinning expressions
43-
fails:Pattern matching warning when one-line form does not warn about pattern matching is experimental feature
4435
fails:Pattern matching Hash pattern supports form Constant(id: pat, id: pat, ...)
4536
fails:Pattern matching Hash pattern supports form Constant[id: pat, id: pat, ...]
4637
fails:Pattern matching Hash pattern supports form {id: pat, id: pat, ...}
@@ -50,53 +41,8 @@ fails:Pattern matching Hash pattern calls #deconstruct_keys per pattern
5041
fails:Pattern matching Hash pattern can match partially
5142
fails:Pattern matching Hash pattern matches {} with {}
5243
fails:Pattern matching refinements are used for #deconstruct
53-
fails:Pattern matching can be standalone assoc operator that deconstructs value and properly scopes variables
54-
fails:Pattern matching extends case expression with case/in construction
55-
fails:Pattern matching allows using then operator
56-
fails:Pattern matching binds variables
57-
fails:Pattern matching cannot mix in and when operators
58-
fails:Pattern matching checks patterns until the first matching
59-
fails:Pattern matching executes else clause if no pattern matches
60-
fails:Pattern matching raises NoMatchingPatternError if no pattern matches and no else clause
61-
fails:Pattern matching raises NoMatchingPatternError if no pattern matches and evaluates the expression only once
62-
fails:Pattern matching does not allow calculation or method calls in a pattern
63-
fails:Pattern matching evaluates the case expression once for multiple patterns, caching the result
64-
fails:Pattern matching find pattern captures preceding elements to the pattern
65-
fails:Pattern matching find pattern captures following elements to the pattern
66-
fails:Pattern matching find pattern can capture the entirety of the pattern
67-
fails:Pattern matching find pattern will match an empty Array-like structure
68-
fails:Pattern matching warning when regular form does not warn about pattern matching is experimental feature
69-
fails:Pattern matching guards supports if guard
70-
fails:Pattern matching guards supports unless guard
71-
fails:Pattern matching guards makes bound variables visible in guard
72-
fails:Pattern matching guards does not evaluate guard if pattern does not match
73-
fails:Pattern matching guards takes guards into account when there are several matching patterns
74-
fails:Pattern matching guards executes else clause if no guarded pattern matches
75-
fails:Pattern matching guards raises NoMatchingPatternError if no guarded pattern matches and no else clause
76-
fails:Pattern matching value pattern matches an object such that pattern === object
77-
fails:Pattern matching value pattern allows string literal with interpolation
78-
fails:Pattern matching variable pattern matches a value and binds variable name to this value
79-
fails:Pattern matching variable pattern makes bounded variable visible outside a case statement scope
80-
fails:Pattern matching variable pattern create local variables even if a pattern doesn't match
81-
fails:Pattern matching variable pattern allow using _ name to drop values
82-
fails:Pattern matching variable pattern supports using _ in a pattern several times
8344
fails:Pattern matching variable pattern does not support using variable name (except _) several times
84-
fails:Pattern matching Array pattern supports form Constant[pat, pat, ...]
85-
fails:Pattern matching Array pattern supports form [pat, pat, ...]
86-
fails:Pattern matching Array pattern supports form pat, pat, ...
87-
fails:Pattern matching Array pattern matches an object with #deconstruct method which returns an array and each element in array matches element in pattern
88-
fails:Pattern matching Array pattern calls #deconstruct even on objects that are already an array
89-
fails:Pattern matching Array pattern does not match object if Constant === object returns false
90-
fails:Pattern matching Array pattern does not match object without #deconstruct method
91-
fails:Pattern matching Array pattern raises TypeError if #deconstruct method does not return array
92-
fails:Pattern matching Array pattern does not match object if elements of array returned by #deconstruct method does not match elements in pattern
93-
fails:Pattern matching Array pattern binds variables
94-
fails:Pattern matching Array pattern supports splat operator *rest
95-
fails:Pattern matching Array pattern does not match partially by default
96-
fails:Pattern matching Array pattern does match partially from the array beginning if list + , syntax used
97-
fails:Pattern matching Array pattern matches [] with []
98-
fails:Pattern matching Array pattern matches anything with *
9945
fails:Pattern matching Hash pattern does not support non-symbol keys
10046
fails:Pattern matching Hash pattern does not support string interpolation in keys
10147
fails:Pattern matching Hash pattern raise SyntaxError when keys duplicate in pattern
102-
fails:Pattern matching refinements are used for #=== in constant pattern
48+
fails:Pattern matching supports pinning expressions in hash pattern

src/main/java/org/truffleruby/parser/DeadNode.java

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@
99
*/
1010
package org.truffleruby.parser;
1111

12+
import com.oracle.truffle.api.CompilerDirectives;
1213
import org.truffleruby.language.RubyContextSourceNode;
1314

14-
import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
1515
import com.oracle.truffle.api.frame.VirtualFrame;
1616
import org.truffleruby.language.RubyNode;
1717

@@ -27,12 +27,7 @@ public DeadNode(String reason) {
2727

2828
@Override
2929
public Object execute(VirtualFrame frame) {
30-
throw exception();
31-
}
32-
33-
@TruffleBoundary
34-
private RuntimeException exception() {
35-
return new UnsupportedOperationException(reason);
30+
throw CompilerDirectives.shouldNotReachHere(reason);
3631
}
3732

3833
@Override

src/main/java/org/truffleruby/parser/YARPBaseTranslator.java

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import org.truffleruby.language.RubyContextSourceNode;
2121
import org.truffleruby.language.RubyNode;
2222
import org.truffleruby.language.arguments.NoKeywordArgumentsDescriptor;
23+
import org.truffleruby.language.control.RaiseException;
2324
import org.truffleruby.language.control.SequenceNode;
2425
import org.truffleruby.language.dispatch.RubyCallNodeParameters;
2526
import org.truffleruby.language.literal.NilLiteralNode;
@@ -65,11 +66,14 @@ public final TranslatorEnvironment getEnvironment() {
6566

6667
@Override
6768
protected RubyNode defaultVisit(Nodes.Node node) {
69+
var context = RubyLanguage.getCurrentContext();
6870
String code = toString(node);
69-
throw new Error(
70-
this.getClass().getSimpleName() + " does not know how to translate " + node.getClass().getSimpleName() +
71-
" at " + RubyLanguage.getCurrentContext().fileLine(getSourceSection(node)) +
72-
"\nCode snippet:\n" + code + "\nPrism AST:\n" + node);
71+
var message = this.getClass().getSimpleName() + " does not know how to translate " +
72+
node.getClass().getSimpleName() + " at " + context.fileLine(getSourceSection(node)) +
73+
"\nCode snippet:\n" + code + "\nPrism AST:\n" + node;
74+
throw new RaiseException(context,
75+
context.getCoreExceptions().syntaxError(message, null, getSourceSection(node)));
76+
// throw new Error(message);
7377
}
7478

7579
protected static RubyNode[] createArray(int size) {
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
/*
2+
* Copyright (c) 2013, 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.parser;
11+
12+
import java.util.Arrays;
13+
14+
import com.oracle.truffle.api.CompilerDirectives;
15+
import org.prism.Nodes;
16+
import org.truffleruby.RubyLanguage;
17+
import org.truffleruby.core.array.ArrayIndexNodes;
18+
import org.truffleruby.core.array.ArrayPatternLengthCheckNode;
19+
import org.truffleruby.core.array.ArraySliceNodeGen;
20+
import org.truffleruby.language.RubyNode;
21+
import org.truffleruby.language.control.AndNodeGen;
22+
import org.truffleruby.language.control.ExecuteAndReturnTrueNode;
23+
import org.truffleruby.language.control.NotNodeGen;
24+
import org.truffleruby.language.literal.TruffleInternalModuleLiteralNode;
25+
import org.truffleruby.language.locals.ReadLocalNode;
26+
import org.truffleruby.language.locals.WriteLocalNode;
27+
28+
29+
/** Translate the pattern of pattern matching. Executing the translated node must result in true or false. Every visit
30+
* method here should return a node which is the condition of whether it matched. Visit methods can change & restore
31+
* {@code currentValueToMatch} to change which expression is being matched against. */
32+
public final class YARPPatternMatchingTranslator extends YARPBaseTranslator {
33+
34+
private final YARPTranslator yarpTranslator;
35+
36+
private RubyNode currentValueToMatch;
37+
38+
public YARPPatternMatchingTranslator(
39+
RubyLanguage language,
40+
TranslatorEnvironment environment,
41+
RubySource rubySource,
42+
YARPTranslator yarpTranslator) {
43+
super(language, environment, rubySource);
44+
this.yarpTranslator = yarpTranslator;
45+
}
46+
47+
public RubyNode translatePatternNode(Nodes.Node patternNode, RubyNode expressionValue) {
48+
currentValueToMatch = expressionValue;
49+
return patternNode.accept(this);
50+
}
51+
52+
@Override
53+
public RubyNode visitIfNode(Nodes.IfNode node) { // a guard like `in [a] if a.even?`
54+
assert node.statements.body.length == 1;
55+
var pattern = node.statements.body[0].accept(this);
56+
var condition = node.predicate.accept(yarpTranslator); // translate after the pattern which might introduce new variables
57+
return AndNodeGen.create(pattern, condition);
58+
}
59+
60+
@Override
61+
public RubyNode visitUnlessNode(Nodes.UnlessNode node) { // a guard like `in [a] unless a.even?`
62+
assert node.statements.body.length == 1;
63+
var pattern = node.statements.body[0].accept(this);
64+
var condition = NotNodeGen.create(node.predicate.accept(yarpTranslator)); // translate after the pattern which might introduce new variables
65+
return AndNodeGen.create(pattern, condition);
66+
}
67+
68+
@Override
69+
public RubyNode visitArrayPatternNode(Nodes.ArrayPatternNode node) {
70+
var preNodes = node.requireds;
71+
var restNode = node.rest;
72+
var postNodes = node.posts;
73+
74+
int preSize = preNodes.length;
75+
int postSize = postNodes.length;
76+
77+
var deconstructed = createCallNode(new TruffleInternalModuleLiteralNode(), "deconstruct_checked",
78+
currentValueToMatch);
79+
80+
final int deconstructedSlot = environment.declareLocalTemp("pattern_deconstruct_array");
81+
final ReadLocalNode readTemp = environment.readNode(deconstructedSlot, node);
82+
final RubyNode assignTemp = readTemp.makeWriteNode(deconstructed);
83+
currentValueToMatch = readTemp;
84+
85+
RubyNode condition = new ArrayPatternLengthCheckNode(preSize + postSize,
86+
currentValueToMatch, restNode != null);
87+
88+
if (node.constant != null) { // Constant[a]
89+
condition = AndNodeGen.create(matchValue(node.constant), condition);
90+
}
91+
92+
for (int i = 0; i < preNodes.length; i++) {
93+
var preNode = preNodes[i];
94+
95+
RubyNode prev = currentValueToMatch;
96+
currentValueToMatch = ArrayIndexNodes.ReadConstantIndexNode.create(currentValueToMatch, i);
97+
try {
98+
condition = AndNodeGen.create(condition, preNode.accept(this));
99+
} finally {
100+
currentValueToMatch = prev;
101+
}
102+
}
103+
104+
if (restNode != null) {
105+
if (restNode instanceof Nodes.SplatNode splatNode) {
106+
if (splatNode.expression != null) {
107+
RubyNode prev = currentValueToMatch;
108+
currentValueToMatch = ArraySliceNodeGen.create(preSize, -postSize, currentValueToMatch);
109+
try {
110+
condition = AndNodeGen.create(condition, splatNode.expression.accept(this));
111+
} finally {
112+
currentValueToMatch = prev;
113+
}
114+
} else { // in [1, *, 2]
115+
// Nothing
116+
}
117+
} else if (restNode instanceof Nodes.ImplicitRestNode) { // in [0, 1,]
118+
// Nothing
119+
} else {
120+
throw CompilerDirectives.shouldNotReachHere(node.getClass().getName());
121+
}
122+
}
123+
124+
for (int i = 0; i < postNodes.length; i++) {
125+
var postNode = postNodes[i];
126+
int index = -postNodes.length + i;
127+
RubyNode prev = currentValueToMatch;
128+
currentValueToMatch = ArrayIndexNodes.ReadConstantIndexNode.create(currentValueToMatch, index);
129+
try {
130+
condition = AndNodeGen.create(condition, postNode.accept(this));
131+
} finally {
132+
currentValueToMatch = prev;
133+
}
134+
}
135+
136+
return YARPTranslator.sequence(node, Arrays.asList(assignTemp, condition));
137+
}
138+
139+
@Override
140+
public RubyNode visitHashPatternNode(Nodes.HashPatternNode node) {
141+
// var deconstructed = createCallNode(currentValueToMatch, "deconstruct_keys", new NilLiteralNode());
142+
143+
// Not correct, the pattern cannot always be represented as a runtime value and this overflows the stack:
144+
// return createCallNode(
145+
// new TruffleInternalModuleLiteralNode(),
146+
// "hash_pattern_matches?",
147+
// node.accept(this),
148+
// NodeUtil.cloneNode(deconstructed));
149+
150+
return defaultVisit(node);
151+
}
152+
153+
@Override
154+
public RubyNode visitLocalVariableTargetNode(Nodes.LocalVariableTargetNode node) {
155+
WriteLocalNode writeLocalNode = yarpTranslator.visitLocalVariableTargetNode(node);
156+
writeLocalNode.setValueNode(currentValueToMatch);
157+
return new ExecuteAndReturnTrueNode(writeLocalNode);
158+
}
159+
160+
@Override
161+
public RubyNode visitPinnedVariableNode(Nodes.PinnedVariableNode node) {
162+
return matchValue(node.variable);
163+
}
164+
165+
@Override
166+
public RubyNode visitPinnedExpressionNode(Nodes.PinnedExpressionNode node) {
167+
return matchValue(node.expression);
168+
}
169+
170+
@Override
171+
protected RubyNode defaultVisit(Nodes.Node node) {
172+
return matchValue(node);
173+
}
174+
175+
private RubyNode matchValue(Nodes.Node value) {
176+
RubyNode translatedValue = value.accept(yarpTranslator);
177+
return createCallNode(translatedValue, "===", currentValueToMatch);
178+
}
179+
180+
}

0 commit comments

Comments
 (0)