Repository: ruby/yarp
Pull request #1442 (2023-09-11 16:33:37)

Introduce MatchLastLineNode and InterpolatedMatchLastLineNode
These are replacements for regular expressions when they are used alone as the predicate of a conditional. That's because they are significantly different from a regular expression because they are not evaluated for truthyness, but instead evaluated as a match against the last line read by an IO object.


Patch:
diff --git a/config.yml b/config.yml
index 1d96410f..ae63472b 100644
--- a/config.yml
+++ b/config.yml
@@ -1488,6 +1488,25 @@ nodes:
 
           1
           ^
+  - name: InterpolatedMatchLastLineNode
+    fields:
+      - name: opening_loc
+        type: location
+      - name: parts
+        type: node[]
+      - name: closing_loc
+        type: location
+      - name: flags
+        type: flags
+        kind: RegularExpressionFlags
+    newline: parts
+    comment: |
+      Represents a regular expression literal that contains interpolation that
+      is being used in the predicate of a conditional to implicitly match
+      against the last line read by an IO object.
+
+          if /foo #{bar} baz/ then end
+             ^^^^^^^^^^^^^^^^
   - name: InterpolatedRegularExpressionNode
     fields:
       - name: opening_loc
@@ -1702,6 +1721,26 @@ nodes:
 
           foo = 1
           ^^^^^^^
+  - name: MatchLastLineNode
+    fields:
+      - name: opening_loc
+        type: location
+      - name: content_loc
+        type: location
+      - name: closing_loc
+        type: location
+      - name: unescaped
+        type: string
+      - name: flags
+        type: flags
+        kind: RegularExpressionFlags
+    comment: |
+      Represents a regular expression literal used in the predicate of a
+      conditional to implicitly match against the last line read by an IO
+      object.
+
+          if /foo/i then end
+             ^^^^^^
   - name: MatchPredicateNode
     fields:
       - name: value
diff --git a/src/yarp.c b/src/yarp.c
index 820361e2..b2fecf9c 100644
--- a/src/yarp.c
+++ b/src/yarp.c
@@ -450,20 +450,27 @@ yp_parser_optional_constant_id_token(yp_parser_t *parser, const yp_token_t *toke
     return token->type == YP_TOKEN_NOT_PROVIDED ? 0 : yp_parser_constant_id_token(parser, token);
 }
 
-// Mark any range nodes in this subtree as flipflops.
+// The predicate of conditional nodes can change what would otherwise be regular
+// nodes into specialized nodes. For example:
+//
+// if foo .. bar         => RangeNode becomes FlipFlopNode
+// if foo and bar .. baz => RangeNode becomes FlipFlopNode
+// if /foo/              => RegularExpressionNode becomes MatchLastLineNode
+// if /foo #{bar}/       => InterpolatedRegularExpressionNode becomes InterpolatedMatchLastLineNode
+//
 static void
-yp_flip_flop(yp_node_t *node) {
+yp_conditional_predicate(yp_node_t *node) {
     switch (YP_NODE_TYPE(node)) {
         case YP_AND_NODE: {
             yp_and_node_t *cast = (yp_and_node_t *) node;
-            yp_flip_flop(cast->left);
-            yp_flip_flop(cast->right);
+            yp_conditional_predicate(cast->left);
+            yp_conditional_predicate(cast->right);
             break;
         }
         case YP_OR_NODE: {
             yp_or_node_t *cast = (yp_or_node_t *) node;
-            yp_flip_flop(cast->left);
-            yp_flip_flop(cast->right);
+            yp_conditional_predicate(cast->left);
+            yp_conditional_predicate(cast->right);
             break;
         }
         case YP_PARENTHESES_NODE: {
@@ -471,7 +478,7 @@ yp_flip_flop(yp_node_t *node) {
 
             if ((cast->body != NULL) && YP_NODE_TYPE_P(cast->body, YP_STATEMENTS_NODE)) {
                 yp_statements_node_t *statements = (yp_statements_node_t *) cast->body;
-                if (statements->body.size == 1) yp_flip_flop(statements->body.nodes[0]);
+                if (statements->body.size == 1) yp_conditional_predicate(statements->body.nodes[0]);
             }
 
             break;
@@ -479,19 +486,36 @@ yp_flip_flop(yp_node_t *node) {
         case YP_RANGE_NODE: {
             yp_range_node_t *cast = (yp_range_node_t *) node;
             if (cast->left) {
-                yp_flip_flop(cast->left);
+                yp_conditional_predicate(cast->left);
             }
             if (cast->right) {
-                yp_flip_flop(cast->right);
+                yp_conditional_predicate(cast->right);
             }
 
             // Here we change the range node into a flip flop node. We can do
             // this since the nodes are exactly the same except for the type.
+            // We're only asserting against the size when we should probably
+            // assert against the entire layout, but we'll assume tests will
+            // catch this.
             assert(sizeof(yp_range_node_t) == sizeof(yp_flip_flop_node_t));
             node->type = YP_FLIP_FLOP_NODE;
 
             break;
         }
+        case YP_REGULAR_EXPRESSION_NODE:
+            // Here we change the regular expression node into a match last line
+            // node. We can do this since the nodes are exactly the same except
+            // for the type.
+            assert(sizeof(yp_regular_expression_node_t) == sizeof(yp_match_last_line_node_t));
+            node->type = YP_MATCH_LAST_LINE_NODE;
+            break;
+        case YP_INTERPOLATED_REGULAR_EXPRESSION_NODE:
+            // Here we change the interpolated regular expression node into an
+            // interpolated match last line node. We can do this since the nodes
+            // are exactly the same except for the type.
+            assert(sizeof(yp_interpolated_regular_expression_node_t) == sizeof(yp_interpolated_match_last_line_node_t));
+            node->type = YP_INTERPOLATED_MATCH_LAST_LINE_NODE;
+            break;
         default:
             break;
     }
@@ -2562,7 +2586,7 @@ yp_if_node_create(yp_parser_t *parser,
     yp_node_t *consequent,
     const yp_token_t *end_keyword
 ) {
-    yp_flip_flop(predicate);
+    yp_conditional_predicate(predicate);
     yp_if_node_t *node = YP_ALLOC_NODE(parser, yp_if_node_t);
 
     const uint8_t *end;
@@ -2598,7 +2622,7 @@ yp_if_node_create(yp_parser_t *parser,
 // Allocate and initialize new IfNode node in the modifier form.
 static yp_if_node_t *
 yp_if_node_modifier_create(yp_parser_t *parser, yp_node_t *statement, const yp_token_t *if_keyword, yp_node_t *predicate) {
-    yp_flip_flop(predicate);
+    yp_conditional_predicate(predicate);
     yp_if_node_t *node = YP_ALLOC_NODE(parser, yp_if_node_t);
 
     yp_statements_node_t *statements = yp_statements_node_create(parser);
@@ -2626,7 +2650,7 @@ yp_if_node_modifier_create(yp_parser_t *parser, yp_node_t *statement, const yp_t
 // Allocate and initialize an if node from a ternary expression.
 static yp_if_node_t *
 yp_if_node_ternary_create(yp_parser_t *parser, yp_node_t *predicate, yp_node_t *true_expression, const yp_token_t *colon, yp_node_t *false_expression) {
-    yp_flip_flop(predicate);
+    yp_conditional_predicate(predicate);
 
     yp_statements_node_t *if_statements = yp_statements_node_create(parser);
     yp_statements_node_body_append(if_statements, true_expression);
@@ -4326,7 +4350,7 @@ yp_undef_node_append(yp_undef_node_t *node, yp_node_t *name) {
 // Allocate a new UnlessNode node.
 static yp_unless_node_t *
 yp_unless_node_create(yp_parser_t *parser, const yp_token_t *keyword, yp_node_t *predicate, yp_statements_node_t *statements) {
-    yp_flip_flop(predicate);
+    yp_conditional_predicate(predicate);
     yp_unless_node_t *node = YP_ALLOC_NODE(parser, yp_unless_node_t);
 
     const uint8_t *end;
@@ -4358,7 +4382,7 @@ yp_unless_node_create(yp_parser_t *parser, const yp_token_t *keyword, yp_node_t
 // Allocate and initialize new UnlessNode node in the modifier form.
 static yp_unless_node_t *
 yp_unless_node_modifier_create(yp_parser_t *parser, yp_node_t *statement, const yp_token_t *unless_keyword, yp_node_t *predicate) {
-    yp_flip_flop(predicate);
+    yp_conditional_predicate(predicate);
     yp_unless_node_t *node = YP_ALLOC_NODE(parser, yp_unless_node_t);
 
     yp_statements_node_t *statements = yp_statements_node_create(parser);
@@ -12027,7 +12051,7 @@ parse_expression_prefix(yp_parser_t *parser, yp_binding_power_t binding_power) {
                     arguments.closing_loc = YP_LOCATION_TOKEN_VALUE(&parser->previous);
                 } else {
                     receiver = parse_expression(parser, YP_BINDING_POWER_COMPOSITION, YP_ERR_NOT_EXPRESSION);
-                    yp_flip_flop(receiver);
+                    yp_conditional_predicate(receiver);
 
                     if (!parser->recovering) {
                         accept(parser, YP_TOKEN_NEWLINE);
@@ -12037,7 +12061,7 @@ parse_expression_prefix(yp_parser_t *parser, yp_binding_power_t binding_power) {
                 }
             } else {
                 receiver = parse_expression(parser, YP_BINDING_POWER_DEFINED, YP_ERR_NOT_EXPRESSION);
-                yp_flip_flop(receiver);
+                yp_conditional_predicate(receiver);
             }
 
             return (yp_node_t *) yp_call_node_not_create(parser, receiver, &message, &arguments);
@@ -12633,7 +12657,7 @@ parse_expression_prefix(yp_parser_t *parser, yp_binding_power_t binding_power) {
             yp_node_t *receiver = parse_expression(parser, yp_binding_powers[parser->previous.type].right, YP_ERR_UNARY_RECEIVER_BANG);
             yp_call_node_t *node = yp_call_node_unary_create(parser, &operator, receiver, "!");
 
-            yp_flip_flop(receiver);
+            yp_conditional_predicate(receiver);
             return (yp_node_t *) node;
         }
         case YP_TOKEN_TILDE: {


Test patch:
diff --git a/test/yarp/location_test.rb b/test/yarp/location_test.rb
index 6b219a0b..46cae55c 100644
--- a/test/yarp/location_test.rb
+++ b/test/yarp/location_test.rb
@@ -450,6 +450,10 @@ module YARP
       assert_location(IntegerNode, "0o1_000")
     end
 
+    def test_InterpolatedMatchLastLineNode
+      assert_location(InterpolatedMatchLastLineNode, "if /foo \#{bar}/ then end", 3...15, &:predicate)
+    end
+
     def test_InterpolatedRegularExpressionNode
       assert_location(InterpolatedRegularExpressionNode, "/\#{foo}/")
     end
@@ -525,6 +529,10 @@ module YARP
       assert_location(LocalVariableWriteNode, "foo = bar")
     end
 
+    def test_MatchLastLineNode
+      assert_location(MatchLastLineNode, "if /foo/ then end", 3...8, &:predicate)
+    end
+
     def test_MatchPredicateNode
       assert_location(MatchPredicateNode, "foo in bar")
     end
diff --git a/test/yarp/snapshots/unparser/corpus/literal/if.txt b/test/yarp/snapshots/unparser/corpus/literal/if.txt
index 88cc0119..4a439644 100644
--- a/test/yarp/snapshots/unparser/corpus/literal/if.txt
+++ b/test/yarp/snapshots/unparser/corpus/literal/if.txt
@@ -6,7 +6,7 @@
         ├── @ IfNode (location: (0...18))
         │   ├── if_keyword_loc: (0...2) = "if"
         │   ├── predicate:
-        │   │   @ RegularExpressionNode (location: (3...8))
+        │   │   @ MatchLastLineNode (location: (3...8))
         │   │   ├── opening_loc: (3...4) = "/"
         │   │   ├── content_loc: (4...7) = "foo"
         │   │   ├── closing_loc: (7...8) = "/"
diff --git a/test/yarp/snapshots/whitequark/cond_match_current_line.txt b/test/yarp/snapshots/whitequark/cond_match_current_line.txt
index e5902bce..88e35d7c 100644
--- a/test/yarp/snapshots/whitequark/cond_match_current_line.txt
+++ b/test/yarp/snapshots/whitequark/cond_match_current_line.txt
@@ -5,7 +5,7 @@
     └── body: (length: 2)
         ├── @ CallNode (location: (0...6))
         │   ├── receiver:
-        │   │   @ RegularExpressionNode (location: (1...6))
+        │   │   @ MatchLastLineNode (location: (1...6))
         │   │   ├── opening_loc: (1...2) = "/"
         │   │   ├── content_loc: (2...5) = "wat"
         │   │   ├── closing_loc: (5...6) = "/"
@@ -22,7 +22,7 @@
         └── @ IfNode (location: (8...21))
             ├── if_keyword_loc: (8...10) = "if"
             ├── predicate:
-            │   @ RegularExpressionNode (location: (11...16))
+            │   @ MatchLastLineNode (location: (11...16))
             │   ├── opening_loc: (11...12) = "/"
             │   ├── content_loc: (12...15) = "wat"
             │   ├── closing_loc: (15...16) = "/"