Repository: ruby/prism
Pull request #2753 (2024-04-29 19:32:45)

Match CRuby interpolation semantics
If a single string that is a static literal is interpolated, it does not impact whether or not the parent is a static literal. In this way, if you have something like a regular expression that interpolates a string literal, it's possible that you will end up pushing just a single regexp onto the stack as opposed to calling out to toregexp.


Patch:
diff --git a/gemfiles/2.7/Gemfile.lock b/gemfiles/2.7/Gemfile.lock
index 66268cf4..8c510b93 100644
--- a/gemfiles/2.7/Gemfile.lock
+++ b/gemfiles/2.7/Gemfile.lock
@@ -8,7 +8,7 @@ GEM
   specs:
     ast (2.4.2)
     ffi (1.16.3)
-    parser (3.3.1.0)
+    parser (3.3.0.5)
       ast (~> 2.4.1)
       racc
     power_assert (2.0.3)
diff --git a/gemfiles/3.0/Gemfile.lock b/gemfiles/3.0/Gemfile.lock
index 6cb63c2b..f0ffb223 100644
--- a/gemfiles/3.0/Gemfile.lock
+++ b/gemfiles/3.0/Gemfile.lock
@@ -9,11 +9,11 @@ GEM
     abbrev (0.1.2)
     ast (2.4.2)
     ffi (1.16.3)
-    mini_portile2 (2.8.6)
-    nokogiri (1.16.4)
+    mini_portile2 (2.8.5)
+    nokogiri (1.16.2)
       mini_portile2 (~> 2.8.2)
       racc (~> 1.4)
-    parser (3.3.1.0)
+    parser (3.3.0.5)
       ast (~> 2.4.1)
       racc
     power_assert (2.0.3)
diff --git a/gemfiles/3.1/Gemfile.lock b/gemfiles/3.1/Gemfile.lock
index 967b7ed3..859e940c 100644
--- a/gemfiles/3.1/Gemfile.lock
+++ b/gemfiles/3.1/Gemfile.lock
@@ -9,11 +9,11 @@ GEM
     abbrev (0.1.2)
     ast (2.4.2)
     ffi (1.16.3)
-    mini_portile2 (2.8.6)
-    nokogiri (1.16.4)
+    mini_portile2 (2.8.5)
+    nokogiri (1.16.2)
       mini_portile2 (~> 2.8.2)
       racc (~> 1.4)
-    parser (3.3.1.0)
+    parser (3.3.0.5)
       ast (~> 2.4.1)
       racc
     power_assert (2.0.3)
diff --git a/gemfiles/3.2/Gemfile.lock b/gemfiles/3.2/Gemfile.lock
index c47e6498..65de9ae5 100644
--- a/gemfiles/3.2/Gemfile.lock
+++ b/gemfiles/3.2/Gemfile.lock
@@ -9,11 +9,11 @@ GEM
     abbrev (0.1.2)
     ast (2.4.2)
     ffi (1.16.3)
-    mini_portile2 (2.8.6)
-    nokogiri (1.16.4)
+    mini_portile2 (2.8.5)
+    nokogiri (1.16.2)
       mini_portile2 (~> 2.8.2)
       racc (~> 1.4)
-    parser (3.3.1.0)
+    parser (3.3.0.5)
       ast (~> 2.4.1)
       racc
     power_assert (2.0.3)
diff --git a/gemfiles/3.3/Gemfile.lock b/gemfiles/3.3/Gemfile.lock
index c93095a3..fb1bb192 100644
--- a/gemfiles/3.3/Gemfile.lock
+++ b/gemfiles/3.3/Gemfile.lock
@@ -9,11 +9,11 @@ GEM
     abbrev (0.1.2)
     ast (2.4.2)
     ffi (1.16.3)
-    mini_portile2 (2.8.6)
-    nokogiri (1.16.4)
+    mini_portile2 (2.8.5)
+    nokogiri (1.16.2)
       mini_portile2 (~> 2.8.2)
       racc (~> 1.4)
-    parser (3.3.1.0)
+    parser (3.3.0.5)
       ast (~> 2.4.1)
       racc
     power_assert (2.0.3)
diff --git a/gemfiles/3.4/Gemfile.lock b/gemfiles/3.4/Gemfile.lock
index 22144921..71c89a28 100644
--- a/gemfiles/3.4/Gemfile.lock
+++ b/gemfiles/3.4/Gemfile.lock
@@ -9,11 +9,11 @@ GEM
     abbrev (0.1.2)
     ast (2.4.2)
     ffi (1.16.3)
-    mini_portile2 (2.8.6)
-    nokogiri (1.16.4)
+    mini_portile2 (2.8.5)
+    nokogiri (1.16.2)
       mini_portile2 (~> 2.8.2)
       racc (~> 1.4)
-    parser (3.3.1.0)
+    parser (3.3.0.5)
       ast (~> 2.4.1)
       racc
     power_assert (2.0.3)
diff --git a/gemfiles/jruby/Gemfile.lock b/gemfiles/jruby/Gemfile.lock
index 7017f4db..8ce32517 100644
--- a/gemfiles/jruby/Gemfile.lock
+++ b/gemfiles/jruby/Gemfile.lock
@@ -7,7 +7,7 @@ GEM
   remote: https://rubygems.org/
   specs:
     ast (2.4.2)
-    parser (3.3.1.0)
+    parser (3.3.0.5)
       ast (~> 2.4.1)
       racc
     power_assert (2.0.3)
diff --git a/gemfiles/truffleruby/Gemfile.lock b/gemfiles/truffleruby/Gemfile.lock
index dd849a94..9fa18a62 100644
--- a/gemfiles/truffleruby/Gemfile.lock
+++ b/gemfiles/truffleruby/Gemfile.lock
@@ -7,7 +7,7 @@ GEM
   remote: https://rubygems.org/
   specs:
     ast (2.4.2)
-    parser (3.3.1.0)
+    parser (3.3.0.5)
       ast (~> 2.4.1)
       racc
     power_assert (2.0.3)
diff --git a/gemfiles/typecheck/Gemfile.lock b/gemfiles/typecheck/Gemfile.lock
index b2247e2d..d52d75ed 100644
--- a/gemfiles/typecheck/Gemfile.lock
+++ b/gemfiles/typecheck/Gemfile.lock
@@ -39,7 +39,7 @@ GEM
     mutex_m (0.2.0)
     netrc (0.11.0)
     parallel (1.24.0)
-    parser (3.3.1.0)
+    parser (3.3.0.5)
       ast (~> 2.4.1)
       racc
     power_assert (2.0.3)
@@ -61,15 +61,15 @@ GEM
       sexp_processor (~> 4.16)
     securerandom (0.3.1)
     sexp_processor (4.17.1)
-    sorbet (0.5.11361)
-      sorbet-static (= 0.5.11361)
-    sorbet-runtime (0.5.11361)
-    sorbet-static (0.5.11361-aarch64-linux)
-    sorbet-static (0.5.11361-universal-darwin)
-    sorbet-static (0.5.11361-x86_64-linux)
-    sorbet-static-and-runtime (0.5.11361)
-      sorbet (= 0.5.11361)
-      sorbet-runtime (= 0.5.11361)
+    sorbet (0.5.11353)
+      sorbet-static (= 0.5.11353)
+    sorbet-runtime (0.5.11353)
+    sorbet-static (0.5.11353-aarch64-linux)
+    sorbet-static (0.5.11353-universal-darwin)
+    sorbet-static (0.5.11353-x86_64-linux)
+    sorbet-static-and-runtime (0.5.11353)
+      sorbet (= 0.5.11353)
+      sorbet-runtime (= 0.5.11353)
     spoom (1.3.0)
       erubi (>= 1.10.0)
       prism (>= 0.19.0)
diff --git a/src/prism.c b/src/prism.c
index 2815723e..24812ca4 100644
--- a/src/prism.c
+++ b/src/prism.c
@@ -5033,6 +5033,50 @@ pm_instance_variable_write_node_create(pm_parser_t *parser, pm_instance_variable
     return node;
 }
 
+/**
+ * Append a part into a list of string parts. Importantly this handles nested
+ * interpolated strings by not necessarily removing the marker for static
+ * literals.
+ */
+static void
+pm_interpolated_node_append(pm_node_t *node, pm_node_list_t *parts, pm_node_t *part) {
+    switch (PM_NODE_TYPE(part)) {
+        case PM_STRING_NODE:
+            pm_node_flag_set(part, PM_NODE_FLAG_STATIC_LITERAL | PM_STRING_FLAGS_FROZEN);
+            break;
+        case PM_EMBEDDED_STATEMENTS_NODE: {
+            pm_embedded_statements_node_t *cast = (pm_embedded_statements_node_t *) part;
+            pm_node_t *embedded = (cast->statements != NULL && cast->statements->body.size == 1) ? cast->statements->body.nodes[0] : NULL;
+
+            if (embedded == NULL) {
+                // If there are no statements or more than one statement, then
+                // we lose the static literal flag.
+                pm_node_flag_unset(node, PM_NODE_FLAG_STATIC_LITERAL);
+            } else if (PM_NODE_TYPE_P(embedded, PM_STRING_NODE)) {
+                // If the embedded statement is a string, then we can keep the
+                // static literal flag and mark the string as frozen.
+                pm_node_flag_set(embedded, PM_NODE_FLAG_STATIC_LITERAL | PM_STRING_FLAGS_FROZEN);
+            } else if (PM_NODE_TYPE_P(embedded, PM_INTERPOLATED_STRING_NODE) && PM_NODE_FLAG_P(embedded, PM_NODE_FLAG_STATIC_LITERAL)) {
+                // If the embedded statement is an interpolated string and it's
+                // a static literal, then we can keep the static literal flag.
+            } else {
+                // Otherwise we lose the static literal flag.
+                pm_node_flag_unset(node, PM_NODE_FLAG_STATIC_LITERAL);
+            }
+
+            break;
+        }
+        case PM_EMBEDDED_VARIABLE_NODE:
+            pm_node_flag_unset((pm_node_t *) node, PM_NODE_FLAG_STATIC_LITERAL);
+            break;
+        default:
+            assert(false && "unexpected node type");
+            break;
+    }
+
+    pm_node_list_append(parts, part);
+}
+
 /**
  * Allocate a new InterpolatedRegularExpressionNode node.
  */
@@ -5066,54 +5110,113 @@ pm_interpolated_regular_expression_node_append(pm_interpolated_regular_expressio
         node->base.location.end = part->location.end;
     }
 
-    if (PM_NODE_TYPE_P(part, PM_STRING_NODE)) {
-        pm_node_flag_set(part, PM_NODE_FLAG_STATIC_LITERAL | PM_STRING_FLAGS_FROZEN);
-    }
-
-    if (!PM_NODE_FLAG_P(part, PM_NODE_FLAG_STATIC_LITERAL)) {
-        pm_node_flag_unset((pm_node_t *) node, PM_NODE_FLAG_STATIC_LITERAL);
-    }
-
-    pm_node_list_append(&node->parts, part);
+    pm_interpolated_node_append((pm_node_t *) node, &node->parts, part);
 }
 
 static inline void
 pm_interpolated_regular_expression_node_closing_set(pm_parser_t *parser, pm_interpolated_regular_expression_node_t *node, const pm_token_t *closing) {
     node->closing_loc = PM_LOCATION_TOKEN_VALUE(closing);
     node->base.location.end = closing->end;
-    pm_node_flag_set((pm_node_t *)node, pm_regular_expression_flags_create(parser, closing));
+    pm_node_flag_set((pm_node_t *) node, pm_regular_expression_flags_create(parser, closing));
 }
 
 /**
  * Append a part to an InterpolatedStringNode node.
+ *
+ * This has some somewhat complicated semantics, because we need to update
+ * multiple flags that have somewhat confusing interactions.
+ *
+ * PM_NODE_FLAG_STATIC_LITERAL indicates that the node should be treated as a
+ * single static literal string that can be pushed onto the stack on its own.
+ * Note that this doesn't necessarily mean that the string will be frozen or
+ * not; the instructions in CRuby will be either putobject or putstring,
+ * depending on the combination of `--enable-frozen-string-literal`,
+ * `# frozen_string_literal: true`, and whether or not there is interpolation.
+ *
+ * PM_INTERPOLATED_STRING_NODE_FLAGS_FROZEN indicates that the string should be
+ * explicitly frozen. This will only happen if the string is comprised entirely
+ * of string parts that are themselves static literals and frozen.
+ *
+ * PM_INTERPOLATED_STRING_NODE_FLAGS_MUTABLE indicates that the string should
+ * be explicitly marked as mutable. This will happen from
+ * `--disable-frozen-string-literal` or `# frozen_string_literal: false`. This
+ * is necessary to indicate that the string should be left up to the runtime,
+ * which could potentially use a chilled string otherwise.
  */
 static inline void
-pm_interpolated_string_node_append(pm_parser_t *parser, pm_interpolated_string_node_t *node, pm_node_t *part) {
+pm_interpolated_string_node_append(pm_interpolated_string_node_t *node, pm_node_t *part) {
+#define CLEAR_FLAGS(node) \
+    node->base.flags = (pm_node_flags_t) (node->base.flags & ~(PM_NODE_FLAG_STATIC_LITERAL | PM_INTERPOLATED_STRING_NODE_FLAGS_FROZEN | PM_INTERPOLATED_STRING_NODE_FLAGS_MUTABLE))
+
+#define MUTABLE_FLAGS(node) \
+    node->base.flags = (pm_node_flags_t) ((node->base.flags | PM_INTERPOLATED_STRING_NODE_FLAGS_MUTABLE) & ~PM_INTERPOLATED_STRING_NODE_FLAGS_FROZEN);
+
     if (node->parts.size == 0 && node->opening_loc.start == NULL) {
         node->base.location.start = part->location.start;
     }
 
-    if (PM_NODE_TYPE_P(part, PM_STRING_NODE)) {
-        pm_node_flag_set(part, PM_NODE_FLAG_STATIC_LITERAL | PM_STRING_FLAGS_FROZEN);
-    }
+    node->base.location.end = MAX(node->base.location.end, part->location.end);
+
+    switch (PM_NODE_TYPE(part)) {
+        case PM_STRING_NODE:
+            pm_node_flag_set(part, PM_NODE_FLAG_STATIC_LITERAL | PM_STRING_FLAGS_FROZEN);
+            break;
+        case PM_INTERPOLATED_STRING_NODE:
+            if (PM_NODE_FLAG_P(part, PM_NODE_FLAG_STATIC_LITERAL)) {
+                // If the string that we're concatenating is a static literal,
+                // then we can keep the static literal flag for this string.
+            } else {
+                // Otherwise, we lose the static literal flag here and we should
+                // also clear the mutability flags.
+                CLEAR_FLAGS(node);
+            }
+            break;
+        case PM_EMBEDDED_STATEMENTS_NODE: {
+            pm_embedded_statements_node_t *cast = (pm_embedded_statements_node_t *) part;
+            pm_node_t *embedded = (cast->statements != NULL && cast->statements->body.size == 1) ? cast->statements->body.nodes[0] : NULL;
+
+            if (embedded == NULL) {
+                // If we're embedding multiple statements or no statements, then
+                // the string is not longer a static literal.
+                CLEAR_FLAGS(node);
+            } else if (PM_NODE_TYPE_P(embedded, PM_STRING_NODE)) {
+                // If the embedded statement is a string, then we can make that
+                // string as frozen and static literal, and not touch the static
+                // literal status of this string.
+                pm_node_flag_set(embedded, PM_NODE_FLAG_STATIC_LITERAL | PM_STRING_FLAGS_FROZEN);
+
+                if (PM_NODE_FLAG_P(node, PM_NODE_FLAG_STATIC_LITERAL)) {
+                    MUTABLE_FLAGS(node);
+                }
+            } else if (PM_NODE_TYPE_P(embedded, PM_INTERPOLATED_STRING_NODE) && PM_NODE_FLAG_P(embedded, PM_NODE_FLAG_STATIC_LITERAL)) {
+                // If the embedded statement is an interpolated string, but that
+                // string is marked as static literal, then we can keep our
+                // static literal status for this string.
+                if (PM_NODE_FLAG_P(node, PM_NODE_FLAG_STATIC_LITERAL)) {
+                    MUTABLE_FLAGS(node);
+                }
+            } else {
+                // In all other cases, we lose the static literal flag here and
+                // become mutable.
+                CLEAR_FLAGS(node);
+            }
 
-    if (!PM_NODE_FLAG_P(part, PM_NODE_FLAG_STATIC_LITERAL)) {
-        pm_node_flag_unset((pm_node_t *) node, PM_NODE_FLAG_STATIC_LITERAL | PM_INTERPOLATED_STRING_NODE_FLAGS_FROZEN | PM_INTERPOLATED_STRING_NODE_FLAGS_MUTABLE);
+            break;
+        }
+        case PM_EMBEDDED_VARIABLE_NODE:
+            // Embedded variables clear static literal, which means we also
+            // should clear the mutability flags.
+            CLEAR_FLAGS(node);
+            break;
+        default:
+            assert(false && "unexpected node type");
+            break;
     }
 
     pm_node_list_append(&node->parts, part);
-    node->base.location.end = MAX(node->base.location.end, part->location.end);
 
-    if (PM_NODE_FLAG_P(node, PM_NODE_FLAG_STATIC_LITERAL)) {
-        switch (parser->frozen_string_literal) {
-            case PM_OPTIONS_FROZEN_STRING_LITERAL_DISABLED:
-                pm_node_flag_set((pm_node_t *) node, PM_INTERPOLATED_STRING_NODE_FLAGS_MUTABLE);
-                break;
-            case PM_OPTIONS_FROZEN_STRING_LITERAL_ENABLED:
-                pm_node_flag_set((pm_node_t *) node, PM_INTERPOLATED_STRING_NODE_FLAGS_FROZEN);
-                break;
-        }
-    }
+#undef CLEAR_FLAGS
+#undef MUTABLE_FLAGS
 }
 
 /**
@@ -5122,11 +5225,21 @@ pm_interpolated_string_node_append(pm_parser_t *parser, pm_interpolated_string_n
 static pm_interpolated_string_node_t *
 pm_interpolated_string_node_create(pm_parser_t *parser, const pm_token_t *opening, const pm_node_list_t *parts, const pm_token_t *closing) {
     pm_interpolated_string_node_t *node = PM_ALLOC_NODE(parser, pm_interpolated_string_node_t);
+    pm_node_flags_t flags = PM_NODE_FLAG_STATIC_LITERAL;
+
+    switch (parser->frozen_string_literal) {
+        case PM_OPTIONS_FROZEN_STRING_LITERAL_DISABLED:
+            flags |= PM_INTERPOLATED_STRING_NODE_FLAGS_MUTABLE;
+            break;
+        case PM_OPTIONS_FROZEN_STRING_LITERAL_ENABLED:
+            flags |= PM_INTERPOLATED_STRING_NODE_FLAGS_FROZEN;
+            break;
+    }
 
     *node = (pm_interpolated_string_node_t) {
         {
             .type = PM_INTERPOLATED_STRING_NODE,
-            .flags = PM_NODE_FLAG_STATIC_LITERAL,
+            .flags = flags,
             .location = {
                 .start = opening->start,
                 .end = closing->end,
@@ -5140,7 +5253,7 @@ pm_interpolated_string_node_create(pm_parser_t *parser, const pm_token_t *openin
     if (parts != NULL) {
         pm_node_t *part;
         PM_NODE_LIST_FOREACH(parts, index, part) {
-            pm_interpolated_string_node_append(parser, node, part);
+            pm_interpolated_string_node_append(node, part);
         }
     }
 
@@ -5162,15 +5275,7 @@ pm_interpolated_symbol_node_append(pm_interpolated_symbol_node_t *node, pm_node_
         node->base.location.start = part->location.start;
     }
 
-    if (PM_NODE_TYPE_P(part, PM_STRING_NODE)) {
-        pm_node_flag_set(part, PM_NODE_FLAG_STATIC_LITERAL | PM_STRING_FLAGS_FROZEN);
-    }
-
-    if (!PM_NODE_FLAG_P(part, PM_NODE_FLAG_STATIC_LITERAL)) {
-        pm_node_flag_unset((pm_node_t *) node, PM_NODE_FLAG_STATIC_LITERAL);
-    }
-
-    pm_node_list_append(&node->parts, part);
+    pm_interpolated_node_append((pm_node_t *) node, &node->parts, part);
     node->base.location.end = MAX(node->base.location.end, part->location.end);
 }
 
@@ -5236,11 +5341,7 @@ pm_interpolated_xstring_node_create(pm_parser_t *parser, const pm_token_t *openi
 
 static inline void
 pm_interpolated_xstring_node_append(pm_interpolated_x_string_node_t *node, pm_node_t *part) {
-    if (PM_NODE_TYPE_P(part, PM_STRING_NODE)) {
-        pm_node_flag_set(part, PM_NODE_FLAG_STATIC_LITERAL | PM_STRING_FLAGS_FROZEN);
-    }
-
-    pm_node_list_append(&node->parts, part);
+    pm_interpolated_node_append((pm_node_t *) node, &node->parts, part);
     node->base.location.end = part->location.end;
 }
 
@@ -16597,11 +16698,11 @@ parse_strings(pm_parser_t *parser, pm_node_t *current) {
                 pm_token_t bounds = not_provided(parser);
 
                 pm_interpolated_string_node_t *container = pm_interpolated_string_node_create(parser, &bounds, NULL, &bounds);
-                pm_interpolated_string_node_append(parser, container, current);
+                pm_interpolated_string_node_append(container, current);
                 current = (pm_node_t *) container;
             }
 
-            pm_interpolated_string_node_append(parser, (pm_interpolated_string_node_t *) current, node);
+            pm_interpolated_string_node_append((pm_interpolated_string_node_t *) current, node);
         }
     }
 
@@ -18786,15 +18887,15 @@ parse_expression_prefix(pm_parser_t *parser, pm_binding_power_t binding_power, b
                             // If we hit string content and the current node is
                             // an interpolated string, then we need to append
                             // the string content to the list of child nodes.
-                            pm_interpolated_string_node_append(parser, (pm_interpolated_string_node_t *) current, string);
+                            pm_interpolated_string_node_append((pm_interpolated_string_node_t *) current, string);
                         } else if (PM_NODE_TYPE_P(current, PM_STRING_NODE)) {
                             // If we hit string content and the current node is
                             // a string node, then we need to convert the
                             // current node into an interpolated string and add
                             // the string content to the list of child nodes.
                             pm_interpolated_string_node_t *interpolated = pm_interpolated_string_node_create(parser, &opening, NULL, &closing);
-                            pm_interpolated_string_node_append(parser, interpolated, current);
-                            pm_interpolated_string_node_append(parser, interpolated, string);
+                            pm_interpolated_string_node_append(interpolated, current);
+                            pm_interpolated_string_node_append(interpolated, string);
                             current = (pm_node_t *) interpolated;
                         } else {
                             assert(false && "unreachable");
@@ -18819,7 +18920,7 @@ parse_expression_prefix(pm_parser_t *parser, pm_binding_power_t binding_power, b
                             pm_token_t opening = not_provided(parser);
                             pm_token_t closing = not_provided(parser);
                             pm_interpolated_string_node_t *interpolated = pm_interpolated_string_node_create(parser, &opening, NULL, &closing);
-                            pm_interpolated_string_node_append(parser, interpolated, current);
+                            pm_interpolated_string_node_append(interpolated, current);
                             current = (pm_node_t *) interpolated;
                         } else {
                             // If we hit an embedded variable and the current
@@ -18828,7 +18929,7 @@ parse_expression_prefix(pm_parser_t *parser, pm_binding_power_t binding_power, b
                         }
 
                         pm_node_t *part = parse_string_part(parser);
-                        pm_interpolated_string_node_append(parser, (pm_interpolated_string_node_t *) current, part);
+                        pm_interpolated_string_node_append((pm_interpolated_string_node_t *) current, part);
                         break;
                     }
                     case PM_TOKEN_EMBEXPR_BEGIN: {
@@ -18848,7 +18949,7 @@ parse_expression_prefix(pm_parser_t *parser, pm_binding_power_t binding_power, b
                             pm_token_t opening = not_provided(parser);
                             pm_token_t closing = not_provided(parser);
                             pm_interpolated_string_node_t *interpolated = pm_interpolated_string_node_create(parser, &opening, NULL, &closing);
-                            pm_interpolated_string_node_append(parser, interpolated, current);
+                            pm_interpolated_string_node_append(interpolated, current);
                             current = (pm_node_t *) interpolated;
                         } else if (PM_NODE_TYPE_P(current, PM_INTERPOLATED_STRING_NODE)) {
                             // If we hit an embedded expression and the current
@@ -18859,7 +18960,7 @@ parse_expression_prefix(pm_parser_t *parser, pm_binding_power_t binding_power, b
                         }
 
                         pm_node_t *part = parse_string_part(parser);
-                        pm_interpolated_string_node_append(parser, (pm_interpolated_string_node_t *) current, part);
+                        pm_interpolated_string_node_append((pm_interpolated_string_node_t *) current, part);
                         break;
                     }
                     default:


Test patch:
diff --git a/test/prism/snapshots/heredocs_nested.txt b/test/prism/snapshots/heredocs_nested.txt
index f830b028..da13e48c 100644
--- a/test/prism/snapshots/heredocs_nested.txt
+++ b/test/prism/snapshots/heredocs_nested.txt
@@ -4,7 +4,7 @@
     @ StatementsNode (location: (1,0)-(12,4))
     └── body: (length: 2)
         ├── @ InterpolatedStringNode (location: (1,0)-(1,7))
-        │   ├── flags: ∅
+        │   ├── flags: mutable
         │   ├── opening_loc: (1,0)-(1,7) = "<<~RUBY"
         │   ├── parts: (length: 4)
         │   │   ├── @ StringNode (location: (2,0)-(3,0))
@@ -19,7 +19,7 @@
         │   │   │   │   @ StatementsNode (location: (4,0)-(4,6))
         │   │   │   │   └── body: (length: 1)
         │   │   │   │       └── @ StringNode (location: (4,0)-(4,6))
-        │   │   │   │           ├── flags: ∅
+        │   │   │   │           ├── flags: frozen
         │   │   │   │           ├── opening_loc: (4,0)-(4,6) = "<<RUBY"
         │   │   │   │           ├── content_loc: (5,0)-(6,0) = "  hello\n"
         │   │   │   │           ├── closing_loc: (6,0)-(7,0) = "RUBY\n"
diff --git a/test/prism/snapshots/seattlerb/dstr_evstr.txt b/test/prism/snapshots/seattlerb/dstr_evstr.txt
index 8d771e88..add8ad6f 100644
--- a/test/prism/snapshots/seattlerb/dstr_evstr.txt
+++ b/test/prism/snapshots/seattlerb/dstr_evstr.txt
@@ -13,7 +13,7 @@
             │   │   │   @ StatementsNode (location: (1,3)-(1,6))
             │   │   │   └── body: (length: 1)
             │   │   │       └── @ StringNode (location: (1,3)-(1,6))
-            │   │   │           ├── flags: ∅
+            │   │   │           ├── flags: frozen
             │   │   │           ├── opening_loc: (1,3)-(1,4) = "'"
             │   │   │           ├── content_loc: (1,4)-(1,5) = "a"
             │   │   │           ├── closing_loc: (1,5)-(1,6) = "'"
diff --git a/test/prism/snapshots/seattlerb/dstr_str.txt b/test/prism/snapshots/seattlerb/dstr_str.txt
index 70b5752c..6fe07818 100644
--- a/test/prism/snapshots/seattlerb/dstr_str.txt
+++ b/test/prism/snapshots/seattlerb/dstr_str.txt
@@ -4,7 +4,7 @@
     @ StatementsNode (location: (1,0)-(1,10))
     └── body: (length: 1)
         └── @ InterpolatedStringNode (location: (1,0)-(1,10))
-            ├── flags: ∅
+            ├── flags: mutable
             ├── opening_loc: (1,0)-(1,1) = "\""
             ├── parts: (length: 2)
             │   ├── @ EmbeddedStatementsNode (location: (1,1)-(1,7))
@@ -13,7 +13,7 @@
             │   │   │   @ StatementsNode (location: (1,3)-(1,6))
             │   │   │   └── body: (length: 1)
             │   │   │       └── @ StringNode (location: (1,3)-(1,6))
-            │   │   │           ├── flags: ∅
+            │   │   │           ├── flags: frozen
             │   │   │           ├── opening_loc: (1,3)-(1,4) = "'"
             │   │   │           ├── content_loc: (1,4)-(1,5) = "a"
             │   │   │           ├── closing_loc: (1,5)-(1,6) = "'"
diff --git a/test/prism/snapshots/seattlerb/heredoc_nested.txt b/test/prism/snapshots/seattlerb/heredoc_nested.txt
index 26d533a3..a2322b96 100644
--- a/test/prism/snapshots/seattlerb/heredoc_nested.txt
+++ b/test/prism/snapshots/seattlerb/heredoc_nested.txt
@@ -7,7 +7,7 @@
             ├── flags: ∅
             ├── elements: (length: 2)
             │   ├── @ InterpolatedStringNode (location: (1,1)-(1,4))
-            │   │   ├── flags: ∅
+            │   │   ├── flags: mutable
             │   │   ├── opening_loc: (1,1)-(1,4) = "<<A"
             │   │   ├── parts: (length: 3)
             │   │   │   ├── @ EmbeddedStatementsNode (location: (2,0)-(2,6))
@@ -16,7 +16,7 @@
             │   │   │   │   │   @ StatementsNode (location: (2,2)-(2,5))
             │   │   │   │   │   └── body: (length: 1)
             │   │   │   │   │       └── @ StringNode (location: (2,2)-(2,5))
-            │   │   │   │   │           ├── flags: ∅
+            │   │   │   │   │           ├── flags: frozen
             │   │   │   │   │           ├── opening_loc: (2,2)-(2,5) = "<<B"
             │   │   │   │   │           ├── content_loc: (3,0)-(4,0) = "b\n"
             │   │   │   │   │           ├── closing_loc: (4,0)-(5,0) = "B\n"
diff --git a/test/prism/snapshots/seattlerb/pct_w_heredoc_interp_nested.txt b/test/prism/snapshots/seattlerb/pct_w_heredoc_interp_nested.txt
index c4bc53b7..1b8ec69b 100644
--- a/test/prism/snapshots/seattlerb/pct_w_heredoc_interp_nested.txt
+++ b/test/prism/snapshots/seattlerb/pct_w_heredoc_interp_nested.txt
@@ -13,7 +13,7 @@
             │   │   ├── closing_loc: ∅
             │   │   └── unescaped: "1"
             │   ├── @ InterpolatedStringNode (location: (1,6)-(1,12))
-            │   │   ├── flags: ∅
+            │   │   ├── flags: mutable
             │   │   ├── opening_loc: ∅
             │   │   ├── parts: (length: 1)
             │   │   │   └── @ EmbeddedStatementsNode (location: (1,6)-(1,12))
@@ -22,7 +22,7 @@
             │   │   │       │   @ StatementsNode (location: (1,8)-(1,11))
             │   │   │       │   └── body: (length: 1)
             │   │   │       │       └── @ StringNode (location: (1,8)-(1,11))
-            │   │   │       │           ├── flags: ∅
+            │   │   │       │           ├── flags: frozen
             │   │   │       │           ├── opening_loc: (1,8)-(1,11) = "<<A"
             │   │   │       │           ├── content_loc: (2,0)-(3,0) = "2\n"
             │   │   │       │           ├── closing_loc: (3,0)-(4,0) = "A\n"
diff --git a/test/prism/snapshots/seattlerb/str_str.txt b/test/prism/snapshots/seattlerb/str_str.txt
index f3f1213a..97031c8a 100644
--- a/test/prism/snapshots/seattlerb/str_str.txt
+++ b/test/prism/snapshots/seattlerb/str_str.txt
@@ -4,7 +4,7 @@
     @ StatementsNode (location: (1,0)-(1,10))
     └── body: (length: 1)
         └── @ InterpolatedStringNode (location: (1,0)-(1,10))
-            ├── flags: ∅
+            ├── flags: mutable
             ├── opening_loc: (1,0)-(1,1) = "\""
             ├── parts: (length: 2)
             │   ├── @ StringNode (location: (1,1)-(1,3))
@@ -19,7 +19,7 @@
             │       │   @ StatementsNode (location: (1,5)-(1,8))
             │       │   └── body: (length: 1)
             │       │       └── @ StringNode (location: (1,5)-(1,8))
-            │       │           ├── flags: ∅
+            │       │           ├── flags: frozen
             │       │           ├── opening_loc: (1,5)-(1,6) = "'"
             │       │           ├── content_loc: (1,6)-(1,7) = "b"
             │       │           ├── closing_loc: (1,7)-(1,8) = "'"
diff --git a/test/prism/snapshots/seattlerb/str_str_str.txt b/test/prism/snapshots/seattlerb/str_str_str.txt
index b01f2b57..b592d380 100644
--- a/test/prism/snapshots/seattlerb/str_str_str.txt
+++ b/test/prism/snapshots/seattlerb/str_str_str.txt
@@ -4,7 +4,7 @@
     @ StatementsNode (location: (1,0)-(1,12))
     └── body: (length: 1)
         └── @ InterpolatedStringNode (location: (1,0)-(1,12))
-            ├── flags: ∅
+            ├── flags: mutable
             ├── opening_loc: (1,0)-(1,1) = "\""
             ├── parts: (length: 3)
             │   ├── @ StringNode (location: (1,1)-(1,3))
@@ -19,7 +19,7 @@
             │   │   │   @ StatementsNode (location: (1,5)-(1,8))
             │   │   │   └── body: (length: 1)
             │   │   │       └── @ StringNode (location: (1,5)-(1,8))
-            │   │   │           ├── flags: ∅
+            │   │   │           ├── flags: frozen
             │   │   │           ├── opening_loc: (1,5)-(1,6) = "'"
             │   │   │           ├── content_loc: (1,6)-(1,7) = "b"
             │   │   │           ├── closing_loc: (1,7)-(1,8) = "'"
diff --git a/test/prism/snapshots/unparser/corpus/literal/literal.txt b/test/prism/snapshots/unparser/corpus/literal/literal.txt
index dcf00bf4..98b88e11 100644
--- a/test/prism/snapshots/unparser/corpus/literal/literal.txt
+++ b/test/prism/snapshots/unparser/corpus/literal/literal.txt
@@ -635,7 +635,7 @@
         │   │       │   @ StatementsNode (location: (53,3)-(53,11))
         │   │       │   └── body: (length: 1)
         │   │       │       └── @ StringNode (location: (53,3)-(53,11))
-        │   │       │           ├── flags: ∅
+        │   │       │           ├── flags: frozen
         │   │       │           ├── opening_loc: (53,3)-(53,4) = "\""
         │   │       │           ├── content_loc: (53,4)-(53,10) = "\\u0000"
         │   │       │           ├── closing_loc: (53,10)-(53,11) = "\""
@@ -707,7 +707,7 @@
         │   │       │   @ StatementsNode (location: (59,4)-(59,9))
         │   │       │   └── body: (length: 1)
         │   │       │       └── @ StringNode (location: (59,4)-(59,9))
-        │   │       │           ├── flags: ∅
+        │   │       │           ├── flags: frozen
         │   │       │           ├── opening_loc: (59,4)-(59,5) = "\""
         │   │       │           ├── content_loc: (59,5)-(59,8) = "foo"
         │   │       │           ├── closing_loc: (59,8)-(59,9) = "\""
diff --git a/test/prism/snapshots/whitequark/dedenting_heredoc.txt b/test/prism/snapshots/whitequark/dedenting_heredoc.txt
index acb79e83..67896b24 100644
--- a/test/prism/snapshots/whitequark/dedenting_heredoc.txt
+++ b/test/prism/snapshots/whitequark/dedenting_heredoc.txt
@@ -15,7 +15,7 @@
         │   │   ├── flags: ∅
         │   │   └── arguments: (length: 1)
         │   │       └── @ InterpolatedStringNode (location: (1,2)-(1,8))
-        │   │           ├── flags: ∅
+        │   │           ├── flags: mutable
         │   │           ├── opening_loc: (1,2)-(1,8) = "<<~\"E\""
         │   │           ├── parts: (length: 3)
         │   │           │   ├── @ StringNode (location: (2,0)-(3,0))
@@ -30,7 +30,7 @@
         │   │           │   │   │   @ StatementsNode (location: (3,4)-(3,9))
         │   │           │   │   │   └── body: (length: 1)
         │   │           │   │   │       └── @ StringNode (location: (3,4)-(3,9))
-        │   │           │   │   │           ├── flags: ∅
+        │   │           │   │   │           ├── flags: frozen
         │   │           │   │   │           ├── opening_loc: (3,4)-(3,5) = "\""
         │   │           │   │   │           ├── content_loc: (3,5)-(3,8) = "  y"
         │   │           │   │   │           ├── closing_loc: (3,8)-(3,9) = "\""