Skip to content

Commit

Permalink
Add YARPPatternMatchingTranslator based on PatternMatchingTranslator
Browse files Browse the repository at this point in the history
* But with several cleanups and fixes.
* Also allow Prism error messages in pattern matching specs.
  • Loading branch information
eregon committed Jan 28, 2024
1 parent 4bbce69 commit 4e37175
Show file tree
Hide file tree
Showing 6 changed files with 272 additions and 80 deletions.
25 changes: 18 additions & 7 deletions spec/ruby/language/pattern_matching_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@
in []
end
RUBY
}.should raise_error(SyntaxError, /syntax error, unexpected `in'|\(eval\):3: syntax error, unexpected keyword_in/)
}.should raise_error(SyntaxError, /syntax error, unexpected `in'|\(eval\):3: syntax error, unexpected keyword_in|cannot parse the expression/)

-> {
eval <<~RUBY
Expand All @@ -216,7 +216,7 @@
when 1 == 1
end
RUBY
}.should raise_error(SyntaxError, /syntax error, unexpected `when'|\(eval\):3: syntax error, unexpected keyword_when/)
}.should raise_error(SyntaxError, /syntax error, unexpected `when'|\(eval\):3: syntax error, unexpected keyword_when|cannot parse the expression/)
end

it "checks patterns until the first matching" do
Expand Down Expand Up @@ -273,7 +273,7 @@
true
end
RUBY
}.should raise_error(SyntaxError, /unexpected/)
}.should raise_error(SyntaxError, /unexpected|expected a delimiter after the predicates of a `when` clause/)
end

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

eval(<<~RUBY).should == true
case {name: '2.6', released_at: Time.new(2018, 12, 25)}
in {released_at: ^(Time.new(2010)..Time.new(2020))}
case 0
in ^(0+0)
true
end
RUBY
end

it "supports pinning expressions in array pattern" do
eval(<<~RUBY).should == true
case 0
in ^(0+0)
case [3]
in [^(1+2)]
true
end
RUBY
end

it "supports pinning expressions in hash pattern" do
eval(<<~RUBY).should == true
case {name: '2.6', released_at: Time.new(2018, 12, 25)}
in {released_at: ^(Time.new(2010)..Time.new(2020))}
true
end
RUBY
Expand Down
56 changes: 1 addition & 55 deletions spec/tags/language/pattern_matching_tags.txt
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
fails:Pattern matching variable pattern supports using any name with _ at the beginning in a pattern several times
fails:Pattern matching variable pattern supports existing variables in a pattern specified with ^ operator
fails:Pattern matching variable pattern allows applying ^ operator to bound variables
fails:Pattern matching alternative pattern matches if any of patterns matches
fails:Pattern matching alternative pattern does not support variable binding
fails:Pattern matching alternative pattern support underscore prefixed variables in alternation
fails:Pattern matching AS pattern binds a variable to a value if pattern matches
fails:Pattern matching AS pattern can be used as a nested pattern
fails:Pattern matching Array pattern supports form Constant(pat, pat, ...)
fails:Pattern matching Hash pattern supports form id: pat, id: pat, ...
fails:Pattern matching Hash pattern supports a: which means a: a
fails:Pattern matching Hash pattern can mix key (a:) and key-value (a: b) declarations
Expand All @@ -24,7 +21,6 @@ fails:Pattern matching Hash pattern treats **nil like there should not be any ot
fails:Pattern matching Hash pattern matches anything with **
fails:Pattern matching refinements are used for #deconstruct_keys
fails:Pattern matching Array pattern accepts a subclass of Array from #deconstruct
fails:Pattern matching can be standalone assoc operator that deconstructs value
fails:Pattern matching Array pattern calls #deconstruct once for multiple patterns, caching the result
fails:Pattern matching find pattern captures both preceding and following elements to the pattern
fails:Pattern matching warning when one-line form warns about pattern matching is experimental feature
Expand All @@ -36,11 +32,6 @@ fails:Pattern matching find pattern can be nested with an array pattern
fails:Pattern matching find pattern can be nested within a hash pattern
fails:Pattern matching find pattern can nest hash and array patterns
fails:Pattern matching can omit parentheses in one line pattern matching
fails:Pattern matching supports pinning instance variables
fails:Pattern matching supports pinning class variables
fails:Pattern matching supports pinning global variables
fails:Pattern matching supports pinning expressions
fails:Pattern matching warning when one-line form does not warn about pattern matching is experimental feature
fails:Pattern matching Hash pattern supports form Constant(id: pat, id: pat, ...)
fails:Pattern matching Hash pattern supports form Constant[id: pat, id: pat, ...]
fails:Pattern matching Hash pattern supports form {id: pat, id: pat, ...}
Expand All @@ -50,53 +41,8 @@ fails:Pattern matching Hash pattern calls #deconstruct_keys per pattern
fails:Pattern matching Hash pattern can match partially
fails:Pattern matching Hash pattern matches {} with {}
fails:Pattern matching refinements are used for #deconstruct
fails:Pattern matching can be standalone assoc operator that deconstructs value and properly scopes variables
fails:Pattern matching extends case expression with case/in construction
fails:Pattern matching allows using then operator
fails:Pattern matching binds variables
fails:Pattern matching cannot mix in and when operators
fails:Pattern matching checks patterns until the first matching
fails:Pattern matching executes else clause if no pattern matches
fails:Pattern matching raises NoMatchingPatternError if no pattern matches and no else clause
fails:Pattern matching raises NoMatchingPatternError if no pattern matches and evaluates the expression only once
fails:Pattern matching does not allow calculation or method calls in a pattern
fails:Pattern matching evaluates the case expression once for multiple patterns, caching the result
fails:Pattern matching find pattern captures preceding elements to the pattern
fails:Pattern matching find pattern captures following elements to the pattern
fails:Pattern matching find pattern can capture the entirety of the pattern
fails:Pattern matching find pattern will match an empty Array-like structure
fails:Pattern matching warning when regular form does not warn about pattern matching is experimental feature
fails:Pattern matching guards supports if guard
fails:Pattern matching guards supports unless guard
fails:Pattern matching guards makes bound variables visible in guard
fails:Pattern matching guards does not evaluate guard if pattern does not match
fails:Pattern matching guards takes guards into account when there are several matching patterns
fails:Pattern matching guards executes else clause if no guarded pattern matches
fails:Pattern matching guards raises NoMatchingPatternError if no guarded pattern matches and no else clause
fails:Pattern matching value pattern matches an object such that pattern === object
fails:Pattern matching value pattern allows string literal with interpolation
fails:Pattern matching variable pattern matches a value and binds variable name to this value
fails:Pattern matching variable pattern makes bounded variable visible outside a case statement scope
fails:Pattern matching variable pattern create local variables even if a pattern doesn't match
fails:Pattern matching variable pattern allow using _ name to drop values
fails:Pattern matching variable pattern supports using _ in a pattern several times
fails:Pattern matching variable pattern does not support using variable name (except _) several times
fails:Pattern matching Array pattern supports form Constant[pat, pat, ...]
fails:Pattern matching Array pattern supports form [pat, pat, ...]
fails:Pattern matching Array pattern supports form pat, pat, ...
fails:Pattern matching Array pattern matches an object with #deconstruct method which returns an array and each element in array matches element in pattern
fails:Pattern matching Array pattern calls #deconstruct even on objects that are already an array
fails:Pattern matching Array pattern does not match object if Constant === object returns false
fails:Pattern matching Array pattern does not match object without #deconstruct method
fails:Pattern matching Array pattern raises TypeError if #deconstruct method does not return array
fails:Pattern matching Array pattern does not match object if elements of array returned by #deconstruct method does not match elements in pattern
fails:Pattern matching Array pattern binds variables
fails:Pattern matching Array pattern supports splat operator *rest
fails:Pattern matching Array pattern does not match partially by default
fails:Pattern matching Array pattern does match partially from the array beginning if list + , syntax used
fails:Pattern matching Array pattern matches [] with []
fails:Pattern matching Array pattern matches anything with *
fails:Pattern matching Hash pattern does not support non-symbol keys
fails:Pattern matching Hash pattern does not support string interpolation in keys
fails:Pattern matching Hash pattern raise SyntaxError when keys duplicate in pattern
fails:Pattern matching refinements are used for #=== in constant pattern
fails:Pattern matching supports pinning expressions in hash pattern
9 changes: 2 additions & 7 deletions src/main/java/org/truffleruby/parser/DeadNode.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@
*/
package org.truffleruby.parser;

import com.oracle.truffle.api.CompilerDirectives;
import org.truffleruby.language.RubyContextSourceNode;

import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
import com.oracle.truffle.api.frame.VirtualFrame;
import org.truffleruby.language.RubyNode;

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

@Override
public Object execute(VirtualFrame frame) {
throw exception();
}

@TruffleBoundary
private RuntimeException exception() {
return new UnsupportedOperationException(reason);
throw CompilerDirectives.shouldNotReachHere(reason);
}

@Override
Expand Down
12 changes: 8 additions & 4 deletions src/main/java/org/truffleruby/parser/YARPBaseTranslator.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import org.truffleruby.language.RubyContextSourceNode;
import org.truffleruby.language.RubyNode;
import org.truffleruby.language.arguments.NoKeywordArgumentsDescriptor;
import org.truffleruby.language.control.RaiseException;
import org.truffleruby.language.control.SequenceNode;
import org.truffleruby.language.dispatch.RubyCallNodeParameters;
import org.truffleruby.language.literal.NilLiteralNode;
Expand Down Expand Up @@ -65,11 +66,14 @@ public final TranslatorEnvironment getEnvironment() {

@Override
protected RubyNode defaultVisit(Nodes.Node node) {
var context = RubyLanguage.getCurrentContext();
String code = toString(node);
throw new Error(
this.getClass().getSimpleName() + " does not know how to translate " + node.getClass().getSimpleName() +
" at " + RubyLanguage.getCurrentContext().fileLine(getSourceSection(node)) +
"\nCode snippet:\n" + code + "\nPrism AST:\n" + node);
var message = this.getClass().getSimpleName() + " does not know how to translate " +
node.getClass().getSimpleName() + " at " + context.fileLine(getSourceSection(node)) +
"\nCode snippet:\n" + code + "\nPrism AST:\n" + node;
throw new RaiseException(context,
context.getCoreExceptions().syntaxError(message, null, getSourceSection(node)));
// throw new Error(message);
}

protected static RubyNode[] createArray(int size) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
/*
* Copyright (c) 2013, 2024 Oracle and/or its affiliates. All rights reserved. This
* code is released under a tri EPL/GPL/LGPL license. You can use it,
* redistribute it and/or modify it under the terms of the:
*
* Eclipse Public License version 2.0, or
* GNU General Public License version 2, or
* GNU Lesser General Public License version 2.1.
*/
package org.truffleruby.parser;

import java.util.Arrays;

import com.oracle.truffle.api.CompilerDirectives;
import org.prism.Nodes;
import org.truffleruby.RubyLanguage;
import org.truffleruby.core.array.ArrayIndexNodes;
import org.truffleruby.core.array.ArrayPatternLengthCheckNode;
import org.truffleruby.core.array.ArraySliceNodeGen;
import org.truffleruby.language.RubyNode;
import org.truffleruby.language.control.AndNodeGen;
import org.truffleruby.language.control.ExecuteAndReturnTrueNode;
import org.truffleruby.language.control.NotNodeGen;
import org.truffleruby.language.literal.TruffleInternalModuleLiteralNode;
import org.truffleruby.language.locals.ReadLocalNode;
import org.truffleruby.language.locals.WriteLocalNode;


/** Translate the pattern of pattern matching. Executing the translated node must result in true or false. Every visit
* method here should return a node which is the condition of whether it matched. Visit methods can change & restore
* {@code currentValueToMatch} to change which expression is being matched against. */
public final class YARPPatternMatchingTranslator extends YARPBaseTranslator {

private final YARPTranslator yarpTranslator;

private RubyNode currentValueToMatch;

public YARPPatternMatchingTranslator(
RubyLanguage language,
TranslatorEnvironment environment,
RubySource rubySource,
YARPTranslator yarpTranslator) {
super(language, environment, rubySource);
this.yarpTranslator = yarpTranslator;
}

public RubyNode translatePatternNode(Nodes.Node patternNode, RubyNode expressionValue) {
currentValueToMatch = expressionValue;
return patternNode.accept(this);
}

@Override
public RubyNode visitIfNode(Nodes.IfNode node) { // a guard like `in [a] if a.even?`
assert node.statements.body.length == 1;
var pattern = node.statements.body[0].accept(this);
var condition = node.predicate.accept(yarpTranslator); // translate after the pattern which might introduce new variables
return AndNodeGen.create(pattern, condition);
}

@Override
public RubyNode visitUnlessNode(Nodes.UnlessNode node) { // a guard like `in [a] unless a.even?`
assert node.statements.body.length == 1;
var pattern = node.statements.body[0].accept(this);
var condition = NotNodeGen.create(node.predicate.accept(yarpTranslator)); // translate after the pattern which might introduce new variables
return AndNodeGen.create(pattern, condition);
}

@Override
public RubyNode visitArrayPatternNode(Nodes.ArrayPatternNode node) {
var preNodes = node.requireds;
var restNode = node.rest;
var postNodes = node.posts;

int preSize = preNodes.length;
int postSize = postNodes.length;

var deconstructed = createCallNode(new TruffleInternalModuleLiteralNode(), "deconstruct_checked",
currentValueToMatch);

final int deconstructedSlot = environment.declareLocalTemp("pattern_deconstruct_array");
final ReadLocalNode readTemp = environment.readNode(deconstructedSlot, node);
final RubyNode assignTemp = readTemp.makeWriteNode(deconstructed);
currentValueToMatch = readTemp;

RubyNode condition = new ArrayPatternLengthCheckNode(preSize + postSize,
currentValueToMatch, restNode != null);

if (node.constant != null) { // Constant[a]
condition = AndNodeGen.create(matchValue(node.constant), condition);
}

for (int i = 0; i < preNodes.length; i++) {
var preNode = preNodes[i];

RubyNode prev = currentValueToMatch;
currentValueToMatch = ArrayIndexNodes.ReadConstantIndexNode.create(currentValueToMatch, i);
try {
condition = AndNodeGen.create(condition, preNode.accept(this));
} finally {
currentValueToMatch = prev;
}
}

if (restNode != null) {
if (restNode instanceof Nodes.SplatNode splatNode) {
if (splatNode.expression != null) {
RubyNode prev = currentValueToMatch;
currentValueToMatch = ArraySliceNodeGen.create(preSize, -postSize, currentValueToMatch);
try {
condition = AndNodeGen.create(condition, splatNode.expression.accept(this));
} finally {
currentValueToMatch = prev;
}
} else { // in [1, *, 2]
// Nothing
}
} else if (restNode instanceof Nodes.ImplicitRestNode) { // in [0, 1,]
// Nothing
} else {
throw CompilerDirectives.shouldNotReachHere(node.getClass().getName());
}
}

for (int i = 0; i < postNodes.length; i++) {
var postNode = postNodes[i];
int index = -postNodes.length + i;
RubyNode prev = currentValueToMatch;
currentValueToMatch = ArrayIndexNodes.ReadConstantIndexNode.create(currentValueToMatch, index);
try {
condition = AndNodeGen.create(condition, postNode.accept(this));
} finally {
currentValueToMatch = prev;
}
}

return YARPTranslator.sequence(node, Arrays.asList(assignTemp, condition));
}

@Override
public RubyNode visitHashPatternNode(Nodes.HashPatternNode node) {
// var deconstructed = createCallNode(currentValueToMatch, "deconstruct_keys", new NilLiteralNode());

// Not correct, the pattern cannot always be represented as a runtime value and this overflows the stack:
// return createCallNode(
// new TruffleInternalModuleLiteralNode(),
// "hash_pattern_matches?",
// node.accept(this),
// NodeUtil.cloneNode(deconstructed));

return defaultVisit(node);
}

@Override
public RubyNode visitLocalVariableTargetNode(Nodes.LocalVariableTargetNode node) {
WriteLocalNode writeLocalNode = yarpTranslator.visitLocalVariableTargetNode(node);
writeLocalNode.setValueNode(currentValueToMatch);
return new ExecuteAndReturnTrueNode(writeLocalNode);
}

@Override
public RubyNode visitPinnedVariableNode(Nodes.PinnedVariableNode node) {
return matchValue(node.variable);
}

@Override
public RubyNode visitPinnedExpressionNode(Nodes.PinnedExpressionNode node) {
return matchValue(node.expression);
}

@Override
protected RubyNode defaultVisit(Nodes.Node node) {
return matchValue(node);
}

private RubyNode matchValue(Nodes.Node value) {
RubyNode translatedValue = value.accept(yarpTranslator);
return createCallNode(translatedValue, "===", currentValueToMatch);
}

}
Loading

0 comments on commit 4e37175

Please sign in to comment.