From b60153241121297c94de976419d421683da4d51b Mon Sep 17 00:00:00 2001 From: Kazuki Tsujimoto Date: Sun, 1 Nov 2020 13:28:24 +0900 Subject: [PATCH] Pattern matching is no longer experimental --- NEWS.md | 18 +++++++- doc/syntax/pattern_matching.rdoc | 66 ++++++++---------------------- parse.y | 22 ++++------ test/ruby/test_pattern_matching.rb | 4 +- 4 files changed, 46 insertions(+), 64 deletions(-) diff --git a/NEWS.md b/NEWS.md index c7a4036a0a..2fa1b3db2e 100644 --- a/NEWS.md +++ b/NEWS.md @@ -48,7 +48,23 @@ sufficient information, see the ChangeLog file or Redmine instead of a warning. yield in a class definition outside of a method is now a SyntaxError instead of a LocalJumpError. [[Feature #15575]] -* Find pattern is added. [[Feature #16828]] +* Pattern matching is no longer experimental. [[Feature #17260]] + +* One-line pattern matching now uses `=>` instead of `in`. [EXPERIMENTAL] + [[Feature #17260]] + + ```ruby + # version 3.0 + {a: 0, b: 1} => {a:} + p a # => 0 + + # version 2.7 + {a: 0, b: 1} in {a:} + p a # => 0 + ``` + +* Find pattern is added. [EXPERIMENTAL] + [[Feature #16828]] ```ruby case ["a", 1, "b", "c", 2, "d", "e", "f", 3] diff --git a/doc/syntax/pattern_matching.rdoc b/doc/syntax/pattern_matching.rdoc index 9d3101c86b..8419af58bb 100644 --- a/doc/syntax/pattern_matching.rdoc +++ b/doc/syntax/pattern_matching.rdoc @@ -1,12 +1,8 @@ = Pattern matching -Pattern matching is an experimental feature allowing deep matching of structured values: checking the structure and binding the matched parts to local variables. +Pattern matching is a feature allowing deep matching of structured values: checking the structure and binding the matched parts to local variables. -Pattern matching in Ruby is implemented with the +in+ operator, which can be used in a standalone expression: - - in - -or within the +case+ statement: +Pattern matching in Ruby is implemented with the +case+/+in+ expression: case in @@ -19,11 +15,15 @@ or within the +case+ statement: ... end -(Note that +in+ and +when+ branches can *not* be mixed in one +case+ statement.) +or with the +=>+ operator, which can be used in a standalone expression: -Pattern matching is _exhaustive_: if variable doesn't match pattern (in a separate +in+ statement), or doesn't matches any branch of +case+ statement (and +else+ branch is absent), +NoMatchingPatternError+ is raised. + => -Therefore, +case+ statement might be used for conditional matching and unpacking: +(Note that +in+ and +when+ branches can *not* be mixed in one +case+ expression.) + +Pattern matching is _exhaustive_: if variable doesn't match pattern (in a separate +in+ clause), or doesn't matches any branch of +case+ expression (and +else+ branch is absent), +NoMatchingPatternError+ is raised. + +Therefore, +case+ expression might be used for conditional matching and unpacking: config = {db: {user: 'admin', password: 'abc123'}} @@ -37,11 +37,11 @@ Therefore, +case+ statement might be used for conditional matching and unpacking end # Prints: "Connect with user 'admin'" -whilst standalone +in+ statement is most useful when expected data structure is known beforehand, to just unpack parts of it: +whilst the +=>+ operator is most useful when expected data structure is known beforehand, to just unpack parts of it: config = {db: {user: 'admin', password: 'abc123'}} - config in {db: {user:}} # will raise if the config's structure is unexpected + config => {db: {user:}} # will raise if the config's structure is unexpected puts "Connect with user '#{user}'" # Prints: "Connect with user 'admin'" @@ -113,7 +113,7 @@ Both array and hash patterns support "rest" specification: end #=> "matched" -In +case+ (but not in standalone +in+) statement, parentheses around both kinds of patterns could be omitted +In +case+ (but not in +=>+) expression, parentheses around both kinds of patterns could be omitted case [1, 2] in Integer, Integer @@ -378,53 +378,23 @@ Additionally, when matching custom classes, expected class could be specified as == Current feature status -As of Ruby 2.7, feature is considered _experimental_: its syntax can change in the future, and the performance is not optimized yet. Every time you use pattern matching in code, the warning will be printed: +As of Ruby 3.0, one-line pattern matching and find pattern are considered _experimental_: its syntax can change in the future. Every time you use these features in code, the warning will be printed: - {a: 1, b: 2} in {a:} - # warning: Pattern matching is experimental, and the behavior may change in future versions of Ruby! + [0] => [*, 0, *] + # warning: Find pattern is experimental, and the behavior may change in future versions of Ruby! + # warning: One-line pattern matching is experimental, and the behavior may change in future versions of Ruby! To suppress this warning, one may use newly introduced Warning::[]= method: Warning[:experimental] = false - eval('{a: 1, b: 2} in {a:}') + eval('[0] => [*, 0, *]') # ...no warning printed... Note that pattern-matching warning is raised at a compile time, so this will not suppress warning: Warning[:experimental] = false # At the time this line is evaluated, the parsing happened and warning emitted - {a: 1, b: 2} in {a:} + [0] => [*, 0, *] So, only subsequently loaded files or `eval`-ed code is affected by switching the flag. Alternatively, command-line key -W:no-experimental can be used to turn off "experimental" feature warnings. - -One of the things developer should be aware of, which probably to be fixed in the upcoming versions, is that pattern matching statement rewrites mentioned local variables on partial match, even if the whole pattern is not matched. - - a = 5 - case [1, 2] - in String => a, String - "matched" - else - "not matched" - end - #=> "not matched" - a - #=> 5 -- even partial match not happened, a is not rewritten - - case [1, 2] - in a, String - "matched" - else - "not matched" - end - #=> "not matched" - a - #=> 1 -- the whole pattern not matched, but partial match happened, a is rewritten - -Currently, the only core class implementing +deconstruct+ and +deconstruct_keys+ is Struct. - - Point = Struct.new(:x, :y) - Point[1, 2] in [a, b] - # successful match - Point[1, 2] in {x:, y:} - # successful match diff --git a/parse.y b/parse.y index 256e9915ad..98fd4ae921 100644 --- a/parse.y +++ b/parse.y @@ -502,7 +502,6 @@ static NODE *new_find_pattern(struct parser_params *p, NODE *constant, NODE *fnd static NODE *new_find_pattern_tail(struct parser_params *p, ID pre_rest_arg, NODE *args, ID post_rest_arg, const YYLTYPE *loc); static NODE *new_hash_pattern(struct parser_params *p, NODE *constant, NODE *hshptn, const YYLTYPE *loc); static NODE *new_hash_pattern_tail(struct parser_params *p, NODE *kw_args, ID kw_rest_arg, const YYLTYPE *loc); -static NODE *new_case3(struct parser_params *p, NODE *val, NODE *pat, const YYLTYPE *loc); static NODE *new_kw_arg(struct parser_params *p, NODE *k, const YYLTYPE *loc); static NODE *args_with_numbered(struct parser_params*,NODE*,int); @@ -1661,7 +1660,11 @@ expr : command_call { p->ctxt.in_kwarg = $3.in_kwarg; /*%%%*/ - $$ = new_case3(p, $1, NEW_IN($5, 0, 0, &@5), &@$); + $$ = NEW_CASE3($1, NEW_IN($5, 0, 0, &@5), &@$); + + if (rb_warning_category_enabled_p(RB_WARN_CATEGORY_EXPERIMENTAL)) + rb_warn0L(nd_line($$), "One-line pattern matching is experimental, and the behavior may change in future versions of Ruby!"); + /*% %*/ /*% ripper: case!($1, in!($5, Qnil, Qnil)) %*/ } @@ -2998,7 +3001,7 @@ primary : literal k_end { /*%%%*/ - $$ = new_case3(p, $2, $4, &@$); + $$ = NEW_CASE3($2, $4, &@$); /*% %*/ /*% ripper: case!($2, $4) %*/ } @@ -4176,6 +4179,9 @@ p_args_tail : p_rest p_find : p_rest ',' p_args_post ',' p_rest { $$ = new_find_pattern_tail(p, $1, $3, $5, &@$); + + if (rb_warning_category_enabled_p(RB_WARN_CATEGORY_EXPERIMENTAL)) + rb_warn0L(nd_line($$), "Find pattern is experimental, and the behavior may change in future versions of Ruby!"); } ; @@ -11679,16 +11685,6 @@ new_hash_pattern_tail(struct parser_params *p, NODE *kw_args, ID kw_rest_arg, co return node; } -static NODE * -new_case3(struct parser_params *p, NODE *val, NODE *pat, const YYLTYPE *loc) -{ - NODE *node = NEW_CASE3(val, pat, loc); - - if (rb_warning_category_enabled_p(RB_WARN_CATEGORY_EXPERIMENTAL)) - rb_warn0L(nd_line(node), "Pattern matching is experimental, and the behavior may change in future versions of Ruby!"); - return node; -} - static NODE* dsym_node(struct parser_params *p, NODE *node, const YYLTYPE *loc) { diff --git a/test/ruby/test_pattern_matching.rb b/test/ruby/test_pattern_matching.rb index d4de685495..b155cb8579 100644 --- a/test/ruby/test_pattern_matching.rb +++ b/test/ruby/test_pattern_matching.rb @@ -1473,13 +1473,13 @@ END assert_warn('') {eval(code)} Warning[:experimental] = true - assert_warn(/Pattern matching is experimental/) {eval(code)} + assert_warn(/is experimental/) {eval(code)} ensure Warning[:experimental] = w end def test_experimental_warning - assert_experimental_warning("case 0; in 0; end") + assert_experimental_warning("case [0]; in [*, 0, *]; end") assert_experimental_warning("0 => 0") end end