Skip to content

Commit

Permalink
Implement Hash pattern matching and the rest of pattern matching
Browse files Browse the repository at this point in the history
  • Loading branch information
eregon committed Jan 28, 2024
1 parent 4e37175 commit 586674a
Show file tree
Hide file tree
Showing 21 changed files with 636 additions and 159 deletions.
10 changes: 3 additions & 7 deletions spec/ruby/language/pattern_matching_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -770,11 +770,7 @@ def obj.deconstruct; "" end
it "accepts a subclass of Array from #deconstruct" do
obj = Object.new
def obj.deconstruct
subarray = Class.new(Array).new(2)
def subarray.[](n)
n
end
subarray
Class.new(Array).new([0, 1])
end

eval(<<~RUBY).should == true
Expand Down Expand Up @@ -1004,7 +1000,7 @@ def obj.deconstruct; [1] end
in {"a" => 1}
end
RUBY
}.should raise_error(SyntaxError, /unexpected/)
}.should raise_error(SyntaxError, /unexpected|expected a label as the key in the hash pattern/)
end

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

it "raise SyntaxError when keys duplicate in pattern" do
Expand Down
38 changes: 0 additions & 38 deletions spec/tags/language/pattern_matching_tags.txt
Original file line number Diff line number Diff line change
@@ -1,48 +1,10 @@
fails:Pattern matching variable pattern supports using any name with _ at the beginning in a pattern several times
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 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 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
fails:Pattern matching Hash pattern does not match object if #deconstruct_keys method returns Hash with non-symbol keys
fails:Pattern matching Hash pattern does not match object if elements of Hash returned by #deconstruct_keys method does not match values in pattern
fails:Pattern matching Hash pattern passes keys specified in pattern as arguments to #deconstruct_keys method
fails:Pattern matching Hash pattern passes keys specified in pattern to #deconstruct_keys method if pattern contains double splat operator **
fails:Pattern matching Hash pattern passes nil to #deconstruct_keys method if pattern contains double splat operator **rest
fails:Pattern matching Hash pattern binds variables
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 Array pattern accepts a subclass of Array from #deconstruct
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
fails:Pattern matching alternative pattern can be used as a nested pattern
fails:Pattern matching Array pattern can be used as a nested pattern
fails:Pattern matching Hash pattern can be used as a nested pattern
fails:Pattern matching find pattern can be nested
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 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
fails:Pattern matching variable pattern does not support using variable name (except _) several times
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 supports pinning expressions in hash pattern
60 changes: 60 additions & 0 deletions src/main/java/org/truffleruby/core/array/ArrayDeconstructNode.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* Copyright (c) 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.core.array;

import com.oracle.truffle.api.dsl.Cached;
import com.oracle.truffle.api.frame.VirtualFrame;
import com.oracle.truffle.api.profiles.InlinedBranchProfile;
import com.oracle.truffle.api.profiles.InlinedConditionProfile;
import org.truffleruby.core.cast.BooleanCastNode;
import org.truffleruby.language.RubyContextSourceNode;
import org.truffleruby.language.RubyNode;

import com.oracle.truffle.api.dsl.NodeChild;
import com.oracle.truffle.api.dsl.Specialization;
import org.truffleruby.language.control.RaiseException;
import org.truffleruby.language.dispatch.DispatchNode;

import static org.truffleruby.language.dispatch.DispatchConfiguration.PUBLIC;

// Implemented in Java because the call to #deconstruct needs to honor refinements
@NodeChild(value = "valueNode", type = RubyNode.class)
public abstract class ArrayDeconstructNode extends RubyContextSourceNode {

abstract RubyNode getValueNode();

@Specialization
Object deconstruct(VirtualFrame frame, Object toMatch,
@Cached DispatchNode respondToNode,
@Cached BooleanCastNode booleanCastNode,
@Cached DispatchNode deconstructNode,
@Cached InlinedConditionProfile hasDeconstructProfile,
@Cached InlinedBranchProfile errorProfile) {
if (hasDeconstructProfile.profile(this, booleanCastNode.execute(this,
respondToNode.callWithFrame(PUBLIC, frame, toMatch, "respond_to?", coreSymbols().DECONSTRUCT)))) {
Object deconstructed = deconstructNode.callWithFrame(PUBLIC, frame, toMatch, "deconstruct");
if (deconstructed instanceof RubyArray) {
return deconstructed;
} else {
errorProfile.enter(this);
throw new RaiseException(getContext(),
coreExceptions().typeError("deconstruct must return Array", this));
}
} else {
return nil;
}
}

@Override
public RubyNode cloneUninitialized() {
return ArrayDeconstructNodeGen.create(getValueNode().cloneUninitialized()).copyFlags(this);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -9,44 +9,43 @@
*/
package org.truffleruby.core.array;

import com.oracle.truffle.api.profiles.ConditionProfile;
import com.oracle.truffle.api.dsl.Fallback;
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 com.oracle.truffle.api.frame.VirtualFrame;
@NodeChild(value = "valueNode", type = RubyNode.class)
public abstract class ArrayPatternLengthCheckNode extends RubyContextSourceNode {

public final 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;
public ArrayPatternLengthCheckNode(int patternLength, boolean hasRest) {
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;
}
abstract RubyNode getValueNode();

@Specialization
boolean arrayLengthCheck(RubyArray matchArray) {
int size = matchArray.size;
if (hasRest) {
return patternLength <= size;
} else {
return false;
return patternLength == size;
}
}

@Fallback
boolean notArray(Object value) {
return false;
}

@Override
public RubyNode cloneUninitialized() {
return new ArrayPatternLengthCheckNode(patternLength, currentValueToMatch.cloneUninitialized(), hasRest)
return ArrayPatternLengthCheckNodeGen.create(patternLength, hasRest, getValueNode().cloneUninitialized())
.copyFlags(this);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* Copyright (c) 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.core.array;

import com.oracle.truffle.api.CompilerDirectives.CompilationFinal;
import com.oracle.truffle.api.frame.VirtualFrame;
import org.truffleruby.language.RubyContextSourceNode;
import org.truffleruby.language.RubyGuards;
import org.truffleruby.language.RubyNode;

public final class ArrayStaticLiteralNode extends RubyContextSourceNode {

@CompilationFinal(dimensions = 1) private final Object[] values;

public ArrayStaticLiteralNode(Object[] values) {
assert allValuesArePrimitiveOrImmutable(values);
this.values = values;
}

private static boolean allValuesArePrimitiveOrImmutable(Object[] values) {
for (Object value : values) {
assert RubyGuards.isPrimitiveOrImmutable(value);
}
return true;
}

@Override
public RubyArray execute(VirtualFrame frame) {
// Copying here is the simplest since we need to return a mutable Array.
// An alternative would be to use COW via DelegatedArrayStorage.
return createArray(ArrayUtils.copy(values));
}

@Override
public RubyNode cloneUninitialized() {
return new ArrayStaticLiteralNode(values);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* Copyright (c) 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.core.hash;

import static org.truffleruby.language.dispatch.DispatchConfiguration.PUBLIC;

import org.truffleruby.core.cast.BooleanCastNode;
import org.truffleruby.language.RubyContextSourceNode;
import org.truffleruby.language.RubyNode;
import org.truffleruby.language.control.RaiseException;
import org.truffleruby.language.dispatch.DispatchNode;

import com.oracle.truffle.api.dsl.Cached;
import com.oracle.truffle.api.dsl.NodeChild;
import com.oracle.truffle.api.dsl.Specialization;
import com.oracle.truffle.api.frame.VirtualFrame;
import com.oracle.truffle.api.profiles.InlinedBranchProfile;
import com.oracle.truffle.api.profiles.InlinedConditionProfile;

// Implemented in Java because the call to #deconstruct_keys needs to honor refinements
@NodeChild(value = "valueNode", type = RubyNode.class)
@NodeChild(value = "keysNode", type = RubyNode.class)
public abstract class HashDeconstructKeysNode extends RubyContextSourceNode {

abstract RubyNode getValueNode();

abstract RubyNode getKeysNode();

@Specialization
Object deconstructKeys(VirtualFrame frame, Object toMatch, Object keys,
@Cached DispatchNode respondToNode,
@Cached BooleanCastNode booleanCastNode,
@Cached DispatchNode deconstructKeysNode,
@Cached InlinedConditionProfile hasDeconstructKeysProfile,
@Cached InlinedBranchProfile errorProfile) {
if (hasDeconstructKeysProfile.profile(this, booleanCastNode.execute(this,
respondToNode.callWithFrame(PUBLIC, frame, toMatch, "respond_to?", coreSymbols().DECONSTRUCT_KEYS)))) {
Object deconstructed = deconstructKeysNode.callWithFrame(PUBLIC, frame, toMatch, "deconstruct_keys", keys);
if (deconstructed instanceof RubyHash) {
return deconstructed;
} else {
errorProfile.enter(this);
throw new RaiseException(getContext(),
coreExceptions().typeError("deconstruct_keys must return Hash", this));
}
} else {
return nil;
}
}

@Override
public RubyNode cloneUninitialized() {
return HashDeconstructKeysNodeGen
.create(getValueNode().cloneUninitialized(), getKeysNode().cloneUninitialized()).copyFlags(this);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* Copyright (c) 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.core.hash;

import com.oracle.truffle.api.dsl.ImportStatic;
import com.oracle.truffle.api.dsl.NodeChild;
import com.oracle.truffle.api.dsl.Specialization;
import com.oracle.truffle.api.frame.Frame;
import com.oracle.truffle.api.library.CachedLibrary;
import org.truffleruby.collections.PEBiFunction;
import org.truffleruby.core.hash.library.HashStoreLibrary;
import org.truffleruby.core.symbol.RubySymbol;
import org.truffleruby.language.NotProvided;
import org.truffleruby.language.RubyContextSourceNode;
import org.truffleruby.language.RubyNode;

/** The same as {@link HashNodes.GetOrUndefinedNode} but with a static key. */
@ImportStatic(HashGuards.class)
@NodeChild(value = "hashNode", type = RubyNode.class)
public abstract class HashGetOrUndefinedNode extends RubyContextSourceNode implements PEBiFunction {

private final RubySymbol key;

public HashGetOrUndefinedNode(RubySymbol key) {
this.key = key;
}

abstract RubyNode getHashNode();

@Specialization(limit = "hashStrategyLimit()")
Object get(RubyHash hash,
@CachedLibrary("hash.store") HashStoreLibrary hashes) {
return hashes.lookupOrDefault(hash.store, null, hash, key, this);
}

@Override
public Object accept(Frame frame, Object hash, Object key) {
return NotProvided.INSTANCE;
}

@Override
public RubyNode cloneUninitialized() {
var copy = HashGetOrUndefinedNodeGen.create(key, getHashNode().cloneUninitialized());
return copy.copyFlags(this);
}

}
Loading

0 comments on commit 586674a

Please sign in to comment.