Skip to content

Commit

Permalink
[GR-34365] Implement Pattern Matching in TruffleRuby (GSoC) (#2683)
Browse files Browse the repository at this point in the history
PullRequest: truffleruby/3780
  • Loading branch information
eregon committed Apr 18, 2023
2 parents d42c333 + d902957 commit 643aa8d
Show file tree
Hide file tree
Showing 33 changed files with 8,356 additions and 5,854 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Compatibility:
* Fix `Hash#shift` when Hash is empty but has initial default value or initial default proc (@itarato).
* Make `Array#shuffle` produce the same results as CRuby (@rwstauner).
* Add `Process.argv0` method (@andrykonchin).
* Add support for array pattern matching. This is opt-in via `--pattern-matching` since pattern matching is not fully supported yet. (#2683, @razetime).

Performance:

Expand Down
16 changes: 14 additions & 2 deletions spec/ruby/language/pattern_matching_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@
in []
end
RUBY
}.should raise_error(SyntaxError, /syntax error, unexpected `in'/)
}.should raise_error(SyntaxError, /syntax error, unexpected `in'|\(eval\):3: syntax error, unexpected keyword_in/)

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

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

it "raises NoMatchingPatternError if no pattern matches and evaluates the expression only once" do
evals = 0
-> {
eval <<~RUBY
case (evals += 1; [0, 1])
in [0]
end
RUBY
}.should raise_error(NoMatchingPatternError, /\[0, 1\]/)
evals.should == 1
end

it "does not allow calculation or method calls in a pattern" do
-> {
eval <<~RUBY
Expand Down
43 changes: 9 additions & 34 deletions spec/tags/language/pattern_matching_tags.txt
Original file line number Diff line number Diff line change
@@ -1,20 +1,4 @@
fails:Pattern matching binds variables
fails:Pattern matching cannot mix in and when operators
fails:Pattern matching raises NoMatchingPatternError if no pattern matches and no else clause
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 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 supports using any name with _ at the beginning in a pattern several times
fails:Pattern matching variable pattern does not support using variable name (except _) 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 variable pattern requires bound variable to be specified in a pattern before ^ operator when it relies on a bound variable
Expand All @@ -24,20 +8,9 @@ fails:Pattern matching alternative pattern support underscore prefixed variables
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 Array pattern supports form pat, pat, ...
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 match partially from the array beginning if list + , syntax used
fails:Pattern matching Array pattern matches anything with *
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
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 Hash pattern does not match object if Constant === object returns false
fails:Pattern matching Hash pattern does not match object without #deconstruct_keys method
fails:Pattern matching Hash pattern does not match object if #deconstruct_keys method does not return Hash
Expand All @@ -51,17 +24,10 @@ fails:Pattern matching Hash pattern supports double splat operator **rest
fails:Pattern matching Hash pattern treats **nil like there should not be any other keys in a matched Hash
fails:Pattern matching Hash pattern matches anything with **
fails:Pattern matching refinements are used for #deconstruct_keys
fails:Pattern matching does not allow calculation or method calls in a pattern
fails:Pattern matching Hash pattern does not support non-symbol 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 preceding elements to the pattern
fails:Pattern matching find pattern captures following elements to the pattern
fails:Pattern matching find pattern captures both preceding and 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 warning when one-line form warns about pattern matching is experimental feature
fails:Pattern matching alternative pattern can be used as a nested pattern
fails:Pattern matching Array pattern can be used as a nested pattern
Expand All @@ -76,3 +42,12 @@ 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, ...}
fails:Pattern matching Hash pattern supports 'string': key literal
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
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
2 changes: 2 additions & 0 deletions src/main/java/org/truffleruby/core/CoreLibrary.java
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,7 @@ public class CoreLibrary {

public final GlobalVariables globalVariables;
public final BindingLocalVariablesObject interactiveBindingLocalVariablesObject;
public final RubyClass noMatchingPatternErrorClass;

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

// StandardError > RuntimeError
runtimeErrorClass = defineClass(standardErrorClass, "RuntimeError");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* Copyright (c) 2022, 2023 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.core.array;

import com.oracle.truffle.api.profiles.ConditionProfile;
import org.truffleruby.language.RubyContextSourceNode;
import org.truffleruby.language.RubyNode;

import com.oracle.truffle.api.frame.VirtualFrame;

public class ArrayPatternLengthCheckNode extends RubyContextSourceNode {

@Child RubyNode currentValueToMatch;
final int patternLength;
final boolean hasRest;

final ConditionProfile isArrayProfile = ConditionProfile.create();

public ArrayPatternLengthCheckNode(int patternLength, RubyNode currentValueToMatch, boolean hasRest) {
this.currentValueToMatch = currentValueToMatch;
this.patternLength = patternLength;
this.hasRest = hasRest;
}

@Override
public Object execute(VirtualFrame frame) {
Object matchArray = currentValueToMatch.execute(frame);
if (isArrayProfile.profile(matchArray instanceof RubyArray)) {
long size = ((RubyArray) matchArray).getArraySize();
if (hasRest) {
return patternLength <= size;
} else {
return patternLength == size;
}
} else {
return false;
}
}

@Override
public RubyNode cloneUninitialized() {
return new ArrayPatternLengthCheckNode(patternLength, currentValueToMatch.cloneUninitialized(), hasRest)
.copyFlags(this);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,15 @@ public RubyException noMemoryError(Node currentNode, OutOfMemoryError javaThrowa
javaThrowable);
}

// NoMatchingPatternError

@TruffleBoundary
public RubyException noMatchingPatternError(Object errorMessage, Node currentNode) {
assert RubyStringLibrary.getUncached().isRubyString(errorMessage);
RubyClass exceptionClass = context.getCoreLibrary().noMatchingPatternErrorClass;
return ExceptionOperations.createRubyException(context, exceptionClass, errorMessage, currentNode, null);
}

// Errno

@TruffleBoundary
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ public class TStringConstants {
}
}


public static final TruffleString __FILE__ = ascii("__FILE__");
public static final TruffleString __LINE__ = ascii("__LINE__");
public static final TruffleString __ENCODING__ = ascii("__ENCODING__");
public static final TruffleString AMPERSAND = ascii("&");
public static final TruffleString AMPERSAND_AMPERSAND = ascii("&&");
public static final TruffleString AMPERSAND_DOT = ascii("&.");
Expand Down Expand Up @@ -93,6 +97,7 @@ public class TStringConstants {
public static final TruffleString RBRACKET = ascii("]");
public static final TruffleString RCURLY = ascii("}");
public static final TruffleString RPAREN = ascii(")");
public static final TruffleString SELF = ascii("self");
public static final TruffleString SEMICOLON = ascii(";");
public static final TruffleString SLASH = ascii("/");
public static final TruffleString STAR = ascii("*");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Copyright (c) 2022, 2023 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.language.control;

import org.truffleruby.language.RubyContextSourceNode;
import org.truffleruby.language.RubyNode;

import com.oracle.truffle.api.frame.VirtualFrame;

public final class ExecuteAndReturnTrueNode extends RubyContextSourceNode {

@Child RubyNode child;

public ExecuteAndReturnTrueNode(RubyNode child) {
this.child = child;
}

@Override
public Object execute(VirtualFrame frame) {
child.doExecuteVoid(frame);
return true;
}

@Override
public RubyNode cloneUninitialized() {
return new ExecuteAndReturnTrueNode(child.cloneUninitialized()).copyFlags(this);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* Copyright (c) 2022, 2023 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.language.control;

import com.oracle.truffle.api.dsl.Cached;
import com.oracle.truffle.api.dsl.NodeChild;
import com.oracle.truffle.api.dsl.Specialization;
import org.truffleruby.language.RubyContextSourceNode;

import org.truffleruby.language.RubyNode;
import org.truffleruby.language.dispatch.DispatchNode;

@NodeChild(value = "expressionNode", type = RubyNode.class)
public abstract class NoMatchingPatternNode extends RubyContextSourceNode {

protected abstract RubyNode getExpressionNode();

@Specialization
protected Object noMatchingPattern(Object expression,
@Cached DispatchNode inspectNode) {
Object inspected = inspectNode.call(coreLibrary().truffleTypeModule, "rb_inspect", expression);
throw new RaiseException(getContext(), coreExceptions().noMatchingPatternError(inspected, this));
}

@Override
public RubyNode cloneUninitialized() {
return NoMatchingPatternNodeGen.create(getExpressionNode().cloneUninitialized()).copyFlags(this);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,8 @@ public RubyCallNodeParameters(
RubyNode block,
ArgumentsDescriptor descriptor,
RubyNode[] arguments,
boolean isSplatted,
boolean ignoreVisibility) {
this(receiver, methodName, block, descriptor, arguments, isSplatted, ignoreVisibility, false, false, false);
this(receiver, methodName, block, descriptor, arguments, false, ignoreVisibility, false, false, false);
}

public RubyCallNodeParameters(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ public Object isDefined(VirtualFrame frame, RubyLanguage language, RubyContext c
return FrozenStrings.ASSIGNMENT;
}

public void setValueNode(RubyNode valueNode) {
this.valueNode = valueNode;
}

@Override
public boolean hasTag(Class<? extends Tag> tag) {
return tag == WriteVariableTag.class || super.hasTag(tag);
Expand All @@ -49,5 +53,4 @@ public Object getNodeObject() {
String name = getVariableName();
return new SingleMemberDescriptor(WriteVariableTag.NAME, name);
}

}
55 changes: 55 additions & 0 deletions src/main/java/org/truffleruby/parser/BaseTranslator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* Copyright (c) 2013, 2021 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 com.oracle.truffle.api.TruffleSafepoint;
import com.oracle.truffle.api.nodes.Node;
import com.oracle.truffle.api.source.Source;
import org.truffleruby.RubyLanguage;
import org.truffleruby.core.DummyNode;
import org.truffleruby.language.RubyNode;
import org.truffleruby.language.SourceIndexLength;
import org.truffleruby.parser.ast.ParseNode;

public abstract class BaseTranslator extends Translator {

protected final TranslatorEnvironment environment;

public BaseTranslator(
RubyLanguage language,
Source source,
ParserContext parserContext,
Node currentNode,
TranslatorEnvironment environment) {
super(language, source, parserContext, currentNode);
this.environment = environment;
}

protected RubyNode addNewlineIfNeeded(ParseNode jrubyNode, RubyNode node) {
if (jrubyNode.isNewline()) {
TruffleSafepoint.poll(DummyNode.INSTANCE);

final SourceIndexLength current = node.getEncapsulatingSourceIndexLength();

if (current == null) {
return node;
}

if (environment.getParseEnvironment().isCoverageEnabled()) {
node.unsafeSetIsCoverageLine();
language.coverageManager.setLineHasCode(source, current.toSourceSection(source).getStartLine());
}
node.unsafeSetIsNewLine();
}

return node;
}

}
Loading

0 comments on commit 643aa8d

Please sign in to comment.