Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement partial support for Ruby 2.7 pattern matching #2186

Merged
merged 28 commits into from
Dec 18, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
73f5cbb
Super-basic support for parsing case-in
chrisseaton Dec 2, 2020
eda877e
Add Array#deconstruct
wildmaples Dec 2, 2020
da1b8f4
Implement Struct#deconstruct
Dec 2, 2020
5256f4f
Add implementation for Hash#deconstruct_keys
wildmaples Dec 2, 2020
397b2d1
Implement Struct#deconstruct_keys
Dec 2, 2020
e88844c
Add and clarify Struct#deconstruct_keys specs
Dec 2, 2020
9d4a4e3
Specify call to respond_to? when checking if objects respond to #deco…
wildmaples Dec 2, 2020
2b1ceb8
Specify when deconstruct and deconstruct_keys is called
chrisseaton Dec 2, 2020
3c1a82d
Specify case expression caching behaviour
tomstuart Dec 8, 2020
3870050
Rename expressionNode and rubyExpression to make it more obvious what…
tomstuart Dec 3, 2020
5e1d3f2
Call #deconstruct on the case expression when the pattern is an array
tomstuart Dec 3, 2020
434618d
Match elements of array pattern against elements of case expression
tomstuart Dec 3, 2020
49e3594
Use #deconstruct_keys and match hash pattern with case expression
wildmaples Dec 3, 2020
76e0962
Support binding a single variable in a pattern match
Dec 3, 2020
4cef9ae
Extract pattern matching helper method
Dec 3, 2020
71d106c
Inline local variables in helper method to avoid name clashes
Dec 3, 2020
2729080
Combine local variables for call parameters in helper method
Dec 3, 2020
96e02ad
Convert patternNode into a RubyNode inline
Dec 3, 2020
a7b267c
Combine similar switch statements in helper method
Dec 3, 2020
677bcd0
Recursively match array literals against array patterns
Dec 4, 2020
6a69150
Update changelog
tomstuart Dec 11, 2020
311eee3
Fix old linting errors
tomstuart Dec 11, 2020
51a64e3
Rename deconstruct spec
chrisseaton Dec 11, 2020
3db440d
NodeType.INNODE
chrisseaton Dec 11, 2020
72edeba
Struct#deconstruct_keys
chrisseaton Dec 11, 2020
b41a3b1
Comment on `in 1, 2` parsing bug
chrisseaton Dec 11, 2020
1314c85
Introduce expert experimental --pattern-matching option
tomstuart Dec 14, 2020
4ca026c
Raise SyntaxError on pattern match if the feature isn’t enabled
tomstuart Dec 15, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Compatibility:
* Updated `Regexp.last_match` to support `Symbol` and `String` parameter (#2179).
* Added support for numbered block parameters (`_1` etc).
* Fixed `String#upto` issue with non-ascii strings (#2183).
* Implemented partial support for pattern matching (#2186).

Performance:

Expand Down
12 changes: 10 additions & 2 deletions spec/ruby/core/struct/deconstruct_keys_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,19 @@
s.deconstruct_keys([0] ).should == {0 => 10}
end

it "ignores not existing attribute names" do
it "returns an empty hash when there are more keys than attributes" do
struct = Struct.new(:x, :y)
s = struct.new(1, 2)

s.deconstruct_keys([:a, :b, :c]).should == {}
s.deconstruct_keys([:x, :y, :a]).should == {}
end

it "returns at first not existing attribute name" do
struct = Struct.new(:x, :y)
s = struct.new(1, 2)

s.deconstruct_keys([:a, :x]).should == {}
s.deconstruct_keys([:x, :a]).should == {x: 1}
eregon marked this conversation as resolved.
Show resolved Hide resolved
end

it "accepts nil argument and return all the attributes" do
Expand Down
96 changes: 96 additions & 0 deletions spec/ruby/language/pattern_matching_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,19 @@
}.should raise_error(SyntaxError, /unexpected/)
end

it "evaluates the case expression once for multiple patterns, caching the result" do
eval(<<~RUBY).should == true
case (ScratchPad << :foo; 1)
in 0
false
in 1
true
end
RUBY

ScratchPad.recorded.should == [:foo]
end

describe "guards" do
it "supports if guard" do
eval(<<~RUBY).should == false
Expand Down Expand Up @@ -508,6 +521,47 @@ def obj.deconstruct; [0, 1] end
RUBY
end

ruby_version_is "3.0" do
it "calls #deconstruct once for multiple patterns, caching the result" do
obj = Object.new

def obj.deconstruct
ScratchPad << :deconstruct
[0, 1]
end

eval(<<~RUBY).should == true
case obj
in [1, 2]
false
in [0, 1]
true
end
RUBY

ScratchPad.recorded.should == [:deconstruct]
end
end

it "calls #deconstruct even on objects that are already an array" do
obj = [1, 2]
def obj.deconstruct
ScratchPad << :deconstruct
[3, 4]
end

eval(<<~RUBY).should == true
case obj
in [3, 4]
true
else
false
end
RUBY

ScratchPad.recorded.should == [:deconstruct]
end

it "does not match object if Constant === object returns false" do
eval(<<~RUBY).should == false
case [0, 1, 2]
Expand All @@ -521,6 +575,7 @@ def obj.deconstruct; [0, 1] end

it "does not match object without #deconstruct method" do
obj = Object.new
obj.should_receive(:respond_to?).with(:deconstruct)

eval(<<~RUBY).should == false
case obj
Expand All @@ -546,6 +601,26 @@ def obj.deconstruct; "" end
}.should raise_error(TypeError, /deconstruct must return Array/)
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
end

eval(<<~RUBY).should == true
case obj
in [1, 2]
false
in [0, 1]
true
end
RUBY
end

it "does not match object if elements of array returned by #deconstruct method does not match elements in pattern" do
obj = Object.new
def obj.deconstruct; [1] end
Expand Down Expand Up @@ -778,6 +853,26 @@ def obj.deconstruct_keys(*); {a: 1} end
RUBY
end

it "calls #deconstruct_keys per pattern" do
obj = Object.new

def obj.deconstruct_keys(*)
ScratchPad << :deconstruct_keys
{a: 1}
end

eval(<<~RUBY).should == true
case obj
in {b: 1}
false
in {a: 1}
true
end
RUBY

ScratchPad.recorded.should == [:deconstruct_keys, :deconstruct_keys]
end

it "does not match object if Constant === object returns false" do
eval(<<~RUBY).should == false
case {a: 1}
Expand All @@ -791,6 +886,7 @@ def obj.deconstruct_keys(*); {a: 1} end

it "does not match object without #deconstruct_keys method" do
obj = Object.new
obj.should_receive(:respond_to?).with(:deconstruct_keys)

eval(<<~RUBY).should == false
case obj
Expand Down
1 change: 0 additions & 1 deletion spec/tags/core/array/deconstruct_tags.txt

This file was deleted.

3 changes: 0 additions & 3 deletions spec/tags/core/hash/deconstruct_keys_tags.txt

This file was deleted.

8 changes: 0 additions & 8 deletions spec/tags/core/struct/deconstruct_keys_tags.txt

This file was deleted.

1 change: 0 additions & 1 deletion spec/tags/core/struct/deconstruct_tags.txt

This file was deleted.

23 changes: 3 additions & 20 deletions spec/tags/language/pattern_matching_tags.txt
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
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 guards supports if guard
fails:Pattern matching guards supports unless guard
Expand All @@ -12,8 +8,6 @@ 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
Expand All @@ -30,30 +24,20 @@ 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 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 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 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 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 supports 'string': key literal
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 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 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 @@ -65,10 +49,9 @@ fails:Pattern matching Hash pattern passes nil to #deconstruct_keys method if pa
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 can match partially
fails:Pattern matching Hash pattern matches {} with {}
fails:Pattern matching Hash pattern matches anything with **
fails:Pattern matching refinements are used for #deconstruct
fails:Pattern matching refinements are used for #deconstruct_keys
fails:Pattern matching refinements are used for #=== in constant pattern
fails:Pattern matching warning warns about pattern matching is experimental feature
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
13 changes: 13 additions & 0 deletions src/main/java/org/truffleruby/options/LanguageOptions.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ public class LanguageOptions {
public final boolean FROZEN_STRING_LITERALS;
/** --lazy-default=true */
public final boolean DEFAULT_LAZY;
/** --pattern-matching=false */
public final boolean PATTERN_MATCHING;
/** --lazy-translation-user=DEFAULT_LAZY */
public final boolean LAZY_TRANSLATION_USER;
/** --backtraces-omit-unused=true */
Expand Down Expand Up @@ -54,6 +56,7 @@ public class LanguageOptions {
public LanguageOptions(Env env, OptionValues options) {
FROZEN_STRING_LITERALS = options.get(OptionsCatalog.FROZEN_STRING_LITERALS_KEY);
DEFAULT_LAZY = options.get(OptionsCatalog.DEFAULT_LAZY_KEY);
PATTERN_MATCHING = options.get(OptionsCatalog.PATTERN_MATCHING_KEY);
LAZY_TRANSLATION_USER = options.hasBeenSet(OptionsCatalog.LAZY_TRANSLATION_USER_KEY) ? options.get(OptionsCatalog.LAZY_TRANSLATION_USER_KEY) : DEFAULT_LAZY;
BACKTRACES_OMIT_UNUSED = options.get(OptionsCatalog.BACKTRACES_OMIT_UNUSED_KEY);
LAZY_TRANSLATION_LOG = options.get(OptionsCatalog.LAZY_TRANSLATION_LOG_KEY);
Expand All @@ -74,6 +77,8 @@ public Object fromDescriptor(OptionDescriptor descriptor) {
return FROZEN_STRING_LITERALS;
case "ruby.lazy-default":
return DEFAULT_LAZY;
case "ruby.pattern-matching":
return PATTERN_MATCHING;
case "ruby.lazy-translation-user":
return LAZY_TRANSLATION_USER;
case "ruby.backtraces-omit-unused":
Expand Down Expand Up @@ -106,6 +111,7 @@ public Object fromDescriptor(OptionDescriptor descriptor) {
public static boolean areOptionsCompatible(OptionValues one, OptionValues two) {
return one.get(OptionsCatalog.FROZEN_STRING_LITERALS_KEY).equals(two.get(OptionsCatalog.FROZEN_STRING_LITERALS_KEY)) &&
one.get(OptionsCatalog.DEFAULT_LAZY_KEY).equals(two.get(OptionsCatalog.DEFAULT_LAZY_KEY)) &&
one.get(OptionsCatalog.PATTERN_MATCHING_KEY).equals(two.get(OptionsCatalog.PATTERN_MATCHING_KEY)) &&
one.get(OptionsCatalog.LAZY_TRANSLATION_USER_KEY).equals(two.get(OptionsCatalog.LAZY_TRANSLATION_USER_KEY)) &&
one.get(OptionsCatalog.BACKTRACES_OMIT_UNUSED_KEY).equals(two.get(OptionsCatalog.BACKTRACES_OMIT_UNUSED_KEY)) &&
one.get(OptionsCatalog.LAZY_TRANSLATION_LOG_KEY).equals(two.get(OptionsCatalog.LAZY_TRANSLATION_LOG_KEY)) &&
Expand Down Expand Up @@ -138,6 +144,13 @@ public static boolean areOptionsCompatibleOrLog(TruffleLogger logger, LanguageOp
return false;
}

oldValue = oldOptions.PATTERN_MATCHING;
newValue = newOptions.PATTERN_MATCHING;
if (!newValue.equals(oldValue)) {
logger.fine("not reusing pre-initialized context: --pattern-matching differs, was: " + oldValue + " and is now: " + newValue);
return false;
}

oldValue = oldOptions.LAZY_TRANSLATION_USER;
newValue = newOptions.LAZY_TRANSLATION_USER;
if (!newValue.equals(oldValue)) {
Expand Down
Loading