Repository: ruby/prism
Pull request #1886 (2023-11-20 14:39:10)

feat: adds encodings for ibm869
Adds encoding for IBM869 per https://github.com/ruby/prism/issues/1843

Documentation: https://en.wikipedia.org/wiki/Code_page_869


Patch:
diff --git a/bin/encodings b/bin/encodings
index 819da7d2..2a6eb629 100755
--- a/bin/encodings
+++ b/bin/encodings
@@ -63,11 +63,13 @@ when "ibm852"       then table(Encoding::IBM852)
 when "ibm855"       then table(Encoding::IBM855)
 when "ibm857"       then table(Encoding::IBM857)
 when "ibm860"       then table(Encoding::IBM860)
+when "ibm863"       then table(Encoding::IBM863)
 when "ibm861"       then table(Encoding::IBM861)
 when "ibm862"       then table(Encoding::IBM862)
 when "ibm864"       then table(Encoding::IBM864)
 when "ibm865"       then table(Encoding::IBM865)
 when "ibm866"       then table(Encoding::IBM866)
+when "ibm869"       then table(Encoding::IBM869)
 when "iso-8859-1"   then table(Encoding::ISO8859_1)
 when "iso-8859-2"   then table(Encoding::ISO8859_2)
 when "iso-8859-3"   then table(Encoding::ISO8859_3)
@@ -85,6 +87,7 @@ when "iso-8859-15"  then table(Encoding::ISO8859_15)
 when "iso-8859-16"  then table(Encoding::ISO8859_16)
 when "koi8-r"       then table(Encoding::KOI8_R)
 when "maccenteuro"  then table(Encoding::MACCENTEURO)
+when "maccroatian"  then table(Encoding::MACCROATIAN)
 when "maccyrillic"  then table(Encoding::MACCYRILLIC)
 when "macgreek"     then table(Encoding::MACGREEK)
 when "maciceland"   then table(Encoding::MACICELAND)
diff --git a/docs/encoding.md b/docs/encoding.md
index 794d64ee..9abbcf9c 100644
--- a/docs/encoding.md
+++ b/docs/encoding.md
@@ -34,6 +34,7 @@ The key of the comment can be either "encoding" or "coding". The value of the co
 * `IBM864`
 * `IBM865`
 * `IBM866`
+* `IBM869`
 * `ISO-8859-1`
 * `ISO-8859-2`
 * `ISO-8859-3`
@@ -51,6 +52,7 @@ The key of the comment can be either "encoding" or "coding". The value of the co
 * `ISO-8859-16`
 * `KOI8-R`
 * `macCentEuro`
+* `macCroatian`
 * `macCyrillic`
 * `macGreek`
 * `macIceland`
diff --git a/include/prism/diagnostic.h b/include/prism/diagnostic.h
index 10b547ae..c963289c 100644
--- a/include/prism/diagnostic.h
+++ b/include/prism/diagnostic.h
@@ -40,12 +40,14 @@ typedef enum {
     PM_ERR_ALIAS_ARGUMENT,
     PM_ERR_AMPAMPEQ_MULTI_ASSIGN,
     PM_ERR_ARGUMENT_AFTER_BLOCK,
+    PM_ERR_ARGUMENT_AFTER_FORWARDING_ELLIPSES,
     PM_ERR_ARGUMENT_BARE_HASH,
     PM_ERR_ARGUMENT_BLOCK_MULTI,
     PM_ERR_ARGUMENT_FORMAL_CLASS,
     PM_ERR_ARGUMENT_FORMAL_CONSTANT,
     PM_ERR_ARGUMENT_FORMAL_GLOBAL,
     PM_ERR_ARGUMENT_FORMAL_IVAR,
+    PM_ERR_ARGUMENT_FORWARDING_UNBOUND,
     PM_ERR_ARGUMENT_NO_FORWARDING_AMP,
     PM_ERR_ARGUMENT_NO_FORWARDING_ELLIPSES,
     PM_ERR_ARGUMENT_NO_FORWARDING_STAR,
diff --git a/include/prism/enc/pm_encoding.h b/include/prism/enc/pm_encoding.h
index a062bbe5..1785a842 100644
--- a/include/prism/enc/pm_encoding.h
+++ b/include/prism/enc/pm_encoding.h
@@ -176,9 +176,11 @@ extern pm_encoding_t pm_encoding_ibm857;
 extern pm_encoding_t pm_encoding_ibm860;
 extern pm_encoding_t pm_encoding_ibm861;
 extern pm_encoding_t pm_encoding_ibm862;
+extern pm_encoding_t pm_encoding_ibm863;
 extern pm_encoding_t pm_encoding_ibm864;
 extern pm_encoding_t pm_encoding_ibm865;
 extern pm_encoding_t pm_encoding_ibm866;
+extern pm_encoding_t pm_encoding_ibm869;
 extern pm_encoding_t pm_encoding_iso_8859_1;
 extern pm_encoding_t pm_encoding_iso_8859_2;
 extern pm_encoding_t pm_encoding_iso_8859_3;
@@ -196,6 +198,7 @@ extern pm_encoding_t pm_encoding_iso_8859_15;
 extern pm_encoding_t pm_encoding_iso_8859_16;
 extern pm_encoding_t pm_encoding_koi8_r;
 extern pm_encoding_t pm_encoding_mac_cent_euro;
+extern pm_encoding_t pm_encoding_mac_croatian;
 extern pm_encoding_t pm_encoding_mac_cyrillic;
 extern pm_encoding_t pm_encoding_mac_greek;
 extern pm_encoding_t pm_encoding_mac_iceland;
diff --git a/lib/prism/ffi.rb b/lib/prism/ffi.rb
index e1d3e0dc..c910fd3a 100644
--- a/lib/prism/ffi.rb
+++ b/lib/prism/ffi.rb
@@ -230,7 +230,7 @@ module Prism
         loader = Serialize::Loader.new(source, buffer.read)
 
         loader.load_header
-        loader.load_force_encoding
+        loader.load_encoding
         loader.load_start_line
         loader.load_comments
       end
diff --git a/lib/prism/parse_result.rb b/lib/prism/parse_result.rb
index 170a529b..50c23bce 100644
--- a/lib/prism/parse_result.rb
+++ b/lib/prism/parse_result.rb
@@ -25,40 +25,50 @@ module Prism
 
     # Perform a byteslice on the source code using the given byte offset and
     # byte length.
-    def slice(offset, length)
-      source.byteslice(offset, length)
+    def slice(byte_offset, length)
+      source.byteslice(byte_offset, length)
     end
 
     # Binary search through the offsets to find the line number for the given
     # byte offset.
-    def line(value)
-      start_line + find_line(value)
+    def line(byte_offset)
+      start_line + find_line(byte_offset)
     end
 
     # Return the byte offset of the start of the line corresponding to the given
     # byte offset.
-    def line_offset(value)
-      offsets[find_line(value)]
+    def line_start(byte_offset)
+      offsets[find_line(byte_offset)]
     end
 
     # Return the column number for the given byte offset.
-    def column(value)
-      value - offsets[find_line(value)]
+    def column(byte_offset)
+      byte_offset - line_start(byte_offset)
+    end
+
+    # Return the character offset for the given byte offset.
+    def character_offset(byte_offset)
+      source.byteslice(0, byte_offset).length
+    end
+
+    # Return the column number in characters for the given byte offset.
+    def character_column(byte_offset)
+      character_offset(byte_offset) - character_offset(line_start(byte_offset))
     end
 
     private
 
     # Binary search through the offsets to find the line number for the given
     # byte offset.
-    def find_line(value)
+    def find_line(byte_offset)
       left = 0
       right = offsets.length - 1
 
       while left <= right
         mid = left + (right - left) / 2
-        return mid if offsets[mid] == value
+        return mid if offsets[mid] == byte_offset
 
-        if offsets[mid] < value
+        if offsets[mid] < byte_offset
           left = mid + 1
         else
           right = mid - 1
@@ -121,11 +131,23 @@ module Prism
       source.slice(start_offset, length)
     end
 
+    # The character offset from the beginning of the source where this location
+    # starts.
+    def start_character_offset
+      source.character_offset(start_offset)
+    end
+
     # The byte offset from the beginning of the source where this location ends.
     def end_offset
       start_offset + length
     end
 
+    # The character offset from the beginning of the source where this location
+    # ends.
+    def end_character_offset
+      source.character_offset(end_offset)
+    end
+
     # The line number where this location starts.
     def start_line
       source.line(start_offset)
@@ -133,7 +155,7 @@ module Prism
 
     # The content of the line where this location starts before this location.
     def start_line_slice
-      offset = source.line_offset(start_offset)
+      offset = source.line_start(start_offset)
       source.slice(offset, start_offset - offset)
     end
 
@@ -148,12 +170,24 @@ module Prism
       source.column(start_offset)
     end
 
+    # The column number in characters where this location ends from the start of
+    # the line.
+    def start_character_column
+      source.character_column(start_offset)
+    end
+
     # The column number in bytes where this location ends from the start of the
     # line.
     def end_column
       source.column(end_offset)
     end
 
+    # The column number in characters where this location ends from the start of
+    # the line.
+    def end_character_column
+      source.character_column(end_offset)
+    end
+
     # Implement the hash pattern matching interface for Location.
     def deconstruct_keys(keys)
       { start_offset: start_offset, end_offset: end_offset }
diff --git a/src/diagnostic.c b/src/diagnostic.c
index 2c59e4ef..c1d930a4 100644
--- a/src/diagnostic.c
+++ b/src/diagnostic.c
@@ -54,12 +54,14 @@ static const char* const diagnostic_messages[PM_DIAGNOSTIC_ID_LEN] = {
     [PM_ERR_ALIAS_ARGUMENT]                     = "Invalid argument being passed to `alias`; expected a bare word, symbol, constant, or global variable",
     [PM_ERR_AMPAMPEQ_MULTI_ASSIGN]              = "Unexpected `&&=` in a multiple assignment",
     [PM_ERR_ARGUMENT_AFTER_BLOCK]               = "Unexpected argument after a block argument",
+    [PM_ERR_ARGUMENT_AFTER_FORWARDING_ELLIPSES] = "Unexpected argument after `...`",
     [PM_ERR_ARGUMENT_BARE_HASH]                 = "Unexpected bare hash argument",
     [PM_ERR_ARGUMENT_BLOCK_MULTI]               = "Multiple block arguments; only one block is allowed",
     [PM_ERR_ARGUMENT_FORMAL_CLASS]              = "Invalid formal argument; formal argument cannot be a class variable",
     [PM_ERR_ARGUMENT_FORMAL_CONSTANT]           = "Invalid formal argument; formal argument cannot be a constant",
     [PM_ERR_ARGUMENT_FORMAL_GLOBAL]             = "Invalid formal argument; formal argument cannot be a global variable",
     [PM_ERR_ARGUMENT_FORMAL_IVAR]               = "Invalid formal argument; formal argument cannot be an instance variable",
+    [PM_ERR_ARGUMENT_FORWARDING_UNBOUND]        = "Unexpected `...` in an non-parenthesized call",
     [PM_ERR_ARGUMENT_NO_FORWARDING_AMP]         = "Unexpected `&` when the parent method is not forwarding",
     [PM_ERR_ARGUMENT_NO_FORWARDING_ELLIPSES]    = "Unexpected `...` when the parent method is not forwarding",
     [PM_ERR_ARGUMENT_NO_FORWARDING_STAR]        = "Unexpected `*` when the parent method is not forwarding",
diff --git a/src/enc/pm_tables.c b/src/enc/pm_tables.c
index 87c7bfb4..70a44720 100644
--- a/src/enc/pm_tables.c
+++ b/src/enc/pm_tables.c
@@ -360,6 +360,30 @@ static uint8_t pm_encoding_ibm862_table[256] = {
     0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Fx
 };
 
+/**
+ * Each element of the following table contains a bitfield that indicates a
+ * piece of information about the corresponding IBM863 character.
+ */
+static uint8_t pm_encoding_ibm863_table[256] = {
+//  0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F
+    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0x
+    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 1x
+    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 2x
+    2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, // 3x
+    0, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, // 4x
+    7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 0, 0, 0, 0, 0, // 5x
+    0, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, // 6x
+    3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 0, 0, 0, 0, 0, // 7x
+    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 8x
+    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 9x
+    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Ax
+    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Bx
+    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Cx
+    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Dx
+    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Ex
+    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Fx
+};
+
 /**
  * Each element of the following table contains a bitfield that indicates a
  * piece of information about the corresponding IBM864 character.
@@ -432,6 +456,30 @@ static uint8_t pm_encoding_ibm866_table[256] = {
     0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Fx
 };
 
+/**
+ * Each element of the following table contains a bitfield that indicates a
+ * piece of information about the corresponding IBM869 character.
+ */
+static uint8_t pm_encoding_ibm869_table[256] = {
+//  0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F
+    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0x
+    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 1x
+    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 2x
+    2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, // 3x
+    0, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, // 4x
+    7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 0, 0, 0, 0, 0, // 5x
+    0, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, // 6x
+    3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 0, 0, 0, 0, 0, // 7x
+    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 8x
+    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 9x
+    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Ax
+    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Bx
+    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Cx
+    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Dx
+    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Ex
+    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Fx
+};
+
 /**
  * Each element of the following table contains a bitfield that indicates a
  * piece of information about the corresponding ISO-8859-1 character.
@@ -841,6 +889,30 @@ static uint8_t pm_encoding_mac_cent_euro_table[256] = {
 };
 
 /**
+ * Each element of the following table contains a bitfield that indicates a
+ * piece of information about the corresponding macCroatian character.
+ */
+static uint8_t pm_encoding_mac_croatian_table[256] = {
+//  0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F
+    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0x
+    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 1x
+    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 2x
+    2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, // 3x
+    0, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, // 4x
+    7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 0, 0, 0, 0, 0, // 5x
+    0, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, // 6x
+    3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 0, 0, 0, 0, 0, // 7x
+    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 8x
+    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 9x
+    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Ax
+    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Bx
+    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Cx
+    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Dx
+    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Ex
+    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Fx
+};
+
+ /**
  * Each element of the following table contains a bitfield that indicates a
  * piece of information about the corresponding macCyrillic character.
  */
@@ -1351,9 +1423,11 @@ PRISM_ENCODING_TABLE(ibm857)
 PRISM_ENCODING_TABLE(ibm860)
 PRISM_ENCODING_TABLE(ibm861)
 PRISM_ENCODING_TABLE(ibm862)
+PRISM_ENCODING_TABLE(ibm863)
 PRISM_ENCODING_TABLE(ibm864)
 PRISM_ENCODING_TABLE(ibm865)
 PRISM_ENCODING_TABLE(ibm866)
+PRISM_ENCODING_TABLE(ibm869)
 PRISM_ENCODING_TABLE(iso_8859_1)
 PRISM_ENCODING_TABLE(iso_8859_2)
 PRISM_ENCODING_TABLE(iso_8859_3)
@@ -1371,6 +1445,7 @@ PRISM_ENCODING_TABLE(iso_8859_15)
 PRISM_ENCODING_TABLE(iso_8859_16)
 PRISM_ENCODING_TABLE(koi8_r)
 PRISM_ENCODING_TABLE(mac_cent_euro)
+PRISM_ENCODING_TABLE(mac_croatian)
 PRISM_ENCODING_TABLE(mac_cyrillic)
 PRISM_ENCODING_TABLE(mac_greek)
 PRISM_ENCODING_TABLE(mac_iceland)
@@ -1552,6 +1627,16 @@ pm_encoding_t pm_encoding_ibm862 = {
     .multibyte = false
 };
 
+/** IBM863 */
+pm_encoding_t pm_encoding_ibm863 = {
+    .name = "IBM863",
+    .char_width = pm_encoding_single_char_width,
+    .alnum_char = pm_encoding_ibm863_alnum_char,
+    .alpha_char = pm_encoding_ibm863_alpha_char,
+    .isupper_char = pm_encoding_ibm863_isupper_char,
+    .multibyte = false
+};
+
 /** IBM864 */
 pm_encoding_t pm_encoding_ibm864 = {
     .name = "IBM864",
@@ -1582,6 +1667,16 @@ pm_encoding_t pm_encoding_ibm866 = {
     .multibyte = false
 };
 
+/** IBM869 */
+pm_encoding_t pm_encoding_ibm869 = {
+    .name = "IBM869",
+    .char_width = pm_encoding_single_char_width,
+    .alnum_char = pm_encoding_ibm869_alnum_char,
+    .alpha_char = pm_encoding_ibm869_alpha_char,
+    .isupper_char = pm_encoding_ibm869_isupper_char,
+    .multibyte = false
+};
+
 /** ISO-8859-1 */
 pm_encoding_t pm_encoding_iso_8859_1 = {
     .name = "ISO-8859-1",
@@ -1752,6 +1847,16 @@ pm_encoding_t pm_encoding_mac_cent_euro = {
     .multibyte = false
 };
 
+/** macCroatian */
+pm_encoding_t pm_encoding_mac_croatian = {
+    .name = "macCroatian",
+    .char_width = pm_encoding_single_char_width,
+    .alnum_char = pm_encoding_mac_croatian_alnum_char,
+    .alpha_char = pm_encoding_mac_croatian_alpha_char,
+    .isupper_char = pm_encoding_mac_croatian_isupper_char,
+    .multibyte = false
+};
+
 /** macCyrillic */
 pm_encoding_t pm_encoding_mac_cyrillic = {
     .name = "macCyrillic",
diff --git a/src/prism.c b/src/prism.c
index 86d9fce0..f041fb51 100644
--- a/src/prism.c
+++ b/src/prism.c
@@ -4007,21 +4007,33 @@ pm_local_variable_write_node_create(pm_parser_t *parser, pm_constant_id_t name,
     return node;
 }
 
+/**
+ * Returns true if the given bounds comprise a numbered parameter (i.e., they
+ * are of the form /^_\d$/).
+ */
 static inline bool
-token_is_numbered_parameter(const uint8_t *start, const uint8_t *end) {
+pm_token_is_numbered_parameter(const uint8_t *start, const uint8_t *end) {
     return (end - start == 2) && (start[0] == '_') && (start[1] != '0') && (pm_char_is_decimal_digit(start[1]));
 }
 
+/**
+ * Ensure the given bounds do not comprise a numbered parameter. If they do, add
+ * an appropriate error message to the parser.
+ */
+static inline void
+pm_refute_numbered_parameter(pm_parser_t *parser, const uint8_t *start, const uint8_t *end) {
+    if (pm_token_is_numbered_parameter(start, end)) {
+        pm_parser_err(parser, start, end, PM_ERR_PARAMETER_NUMBERED_RESERVED);
+    }
+}
+
 /**
  * Allocate and initialize a new LocalVariableTargetNode node.
  */
 static pm_local_variable_target_node_t *
 pm_local_variable_target_node_create(pm_parser_t *parser, const pm_token_t *name) {
     pm_local_variable_target_node_t *node = PM_ALLOC_NODE(parser, pm_local_variable_target_node_t);
-
-    if (token_is_numbered_parameter(name->start, name->end)) {
-        pm_parser_err_token(parser, name, PM_ERR_PARAMETER_NUMBERED_RESERVED);
-    }
+    pm_refute_numbered_parameter(parser, name->start, name->end);
 
     *node = (pm_local_variable_target_node_t) {
         {
@@ -5752,9 +5764,7 @@ static void
 pm_parser_parameter_name_check(pm_parser_t *parser, const pm_token_t *name) {
     // We want to check whether the parameter name is a numbered parameter or
     // not.
-    if (token_is_numbered_parameter(name->start, name->end)) {
-        pm_parser_err_token(parser, name, PM_ERR_PARAMETER_NUMBERED_RESERVED);
-    }
+    pm_refute_numbered_parameter(parser, name->start, name->end);
 
     // We want to ignore any parameter name that starts with an underscore.
     if ((name->start < name->end) && (*name->start == '_')) return;
@@ -6079,8 +6089,10 @@ parser_lex_magic_comment_encoding_value(pm_parser_t *parser, const uint8_t *star
                 ENCODING1("CP864", pm_encoding_ibm864);
                 ENCODING1("CP865", pm_encoding_ibm865);
                 ENCODING1("CP866", pm_encoding_ibm866);
+                ENCODING1("CP869", pm_encoding_ibm869);
                 ENCODING1("CP874", pm_encoding_windows_874);
                 ENCODING1("CP878", pm_encoding_koi8_r);
+                ENCODING1("CP863", pm_encoding_ibm863);
                 ENCODING2("CP932", "csWindows31J", pm_encoding_windows_31j);
                 ENCODING1("CP936", pm_encoding_gbk);
                 ENCODING1("CP1250", pm_encoding_windows_1250);
@@ -6118,9 +6130,11 @@ parser_lex_magic_comment_encoding_value(pm_parser_t *parser, const uint8_t *star
                 ENCODING1("IBM860", pm_encoding_ibm860);
                 ENCODING1("IBM861", pm_encoding_ibm861);
                 ENCODING1("IBM862", pm_encoding_ibm862);
+                ENCODING1("IBM863", pm_encoding_ibm863);
                 ENCODING1("IBM864", pm_encoding_ibm864);
                 ENCODING1("IBM865", pm_encoding_ibm865);
                 ENCODING1("IBM866", pm_encoding_ibm866);
+                ENCODING1("IBM869", pm_encoding_ibm869);
                 ENCODING2("ISO-8859-1", "ISO8859-1", pm_encoding_iso_8859_1);
                 ENCODING2("ISO-8859-2", "ISO8859-2", pm_encoding_iso_8859_2);
                 ENCODING2("ISO-8859-3", "ISO8859-3", pm_encoding_iso_8859_3);
@@ -6145,6 +6159,7 @@ parser_lex_magic_comment_encoding_value(pm_parser_t *parser, const uint8_t *star
                 break;
             case 'M': case 'm':
                 ENCODING1("macCentEuro", pm_encoding_mac_cent_euro);
+                ENCODING1("macCroatian", pm_encoding_mac_croatian);
                 ENCODING1("macCyrillic", pm_encoding_mac_cyrillic);
                 ENCODING1("macGreek", pm_encoding_mac_greek);
                 ENCODING1("macIceland", pm_encoding_mac_iceland);
@@ -9149,7 +9164,7 @@ parser_lex(pm_parser_t *parser) {
                         !(last_state & (PM_LEX_STATE_DOT | PM_LEX_STATE_FNAME)) &&
                         (type == PM_TOKEN_IDENTIFIER) &&
                         ((pm_parser_local_depth(parser, &parser->current) != -1) ||
-                         token_is_numbered_parameter(parser->current.start, parser->current.end))
+                         pm_token_is_numbered_parameter(parser->current.start, parser->current.end))
                     ) {
                         lex_state_set(parser, PM_LEX_STATE_END | PM_LEX_STATE_LABEL);
                     }
@@ -10023,7 +10038,7 @@ typedef enum {
     PM_BINDING_POWER_MODIFIER_RESCUE =  6, // rescue
     PM_BINDING_POWER_COMPOSITION =      8, // and or
     PM_BINDING_POWER_NOT =             10, // not
-    PM_BINDING_POWER_MATCH =           12, // =>
+    PM_BINDING_POWER_MATCH =           12, // => in
     PM_BINDING_POWER_DEFINED =         14, // defined?
     PM_BINDING_POWER_ASSIGNMENT =      16, // = += -= *= /= %= &= |= ^= &&= ||= <<= >>= **=
     PM_BINDING_POWER_TERNARY =         18, // ?:
@@ -10082,12 +10097,7 @@ pm_binding_powers_t pm_binding_powers[PM_TOKEN_MAXIMUM] = {
     [PM_TOKEN_KEYWORD_WHILE_MODIFIER] = LEFT_ASSOCIATIVE(PM_BINDING_POWER_MODIFIER),
 
     // rescue
-    [PM_TOKEN_KEYWORD_RESCUE_MODIFIER] = {
-        PM_BINDING_POWER_ASSIGNMENT,
-        PM_BINDING_POWER_MODIFIER_RESCUE + 1,
-        true,
-        false
-    },
+    [PM_TOKEN_KEYWORD_RESCUE_MODIFIER] = LEFT_ASSOCIATIVE(PM_BINDING_POWER_MODIFIER_RESCUE),
 
     // and or
     [PM_TOKEN_KEYWORD_AND] = LEFT_ASSOCIATIVE(PM_BINDING_POWER_COMPOSITION),
@@ -10492,7 +10502,7 @@ parse_target(pm_parser_t *parser, pm_node_t *target) {
             target->type = PM_GLOBAL_VARIABLE_TARGET_NODE;
             return target;
         case PM_LOCAL_VARIABLE_READ_NODE:
-            if (token_is_numbered_parameter(target->location.start, target->location.end)) {
+            if (pm_token_is_numbered_parameter(target->location.start, target->location.end)) {
                 pm_parser_err_node(parser, target, PM_ERR_PARAMETER_NUMBERED_RESERVED);
             } else {
                 assert(sizeof(pm_local_variable_target_node_t) == sizeof(pm_local_variable_read_node_t));
@@ -10551,10 +10561,7 @@ parse_target(pm_parser_t *parser, pm_node_t *target) {
                     assert(sizeof(pm_local_variable_target_node_t) == sizeof(pm_local_variable_read_node_t));
                     target->type = PM_LOCAL_VARIABLE_TARGET_NODE;
 
-                    if (token_is_numbered_parameter(message.start, message.end)) {
-                        pm_parser_err_location(parser, &message, PM_ERR_PARAMETER_NUMBERED_RESERVED);
-                    }
-
+                    pm_refute_numbered_parameter(parser, message.start, message.end);
                     return target;
                 }
 
@@ -10635,10 +10642,7 @@ parse_write(pm_parser_t *parser, pm_node_t *target, pm_token_t *operator, pm_nod
             return (pm_node_t *) node;
         }
         case PM_LOCAL_VARIABLE_READ_NODE: {
-            if (token_is_numbered_parameter(target->location.start, target->location.end)) {
-                pm_parser_err_node(parser, target, PM_ERR_PARAMETER_NUMBERED_RESERVED);
-            }
-
+            pm_refute_numbered_parameter(parser, target->location.start, target->location.end);
             pm_local_variable_read_node_t *local_read = (pm_local_variable_read_node_t *) target;
 
             pm_constant_id_t constant_id = local_read->name;
@@ -10700,10 +10704,7 @@ parse_write(pm_parser_t *parser, pm_node_t *target, pm_token_t *operator, pm_nod
                     pm_constant_id_t constant_id = pm_parser_constant_id_location(parser, message.start, message.end);
                     target = (pm_node_t *) pm_local_variable_write_node_create(parser, constant_id, 0, value, &message, operator);
 
-                    if (token_is_numbered_parameter(message.start, message.end)) {
-                        pm_parser_err_location(parser, &message, PM_ERR_PARAMETER_NUMBERED_RESERVED);
-                    }
-
+                    pm_refute_numbered_parameter(parser, message.start, message.end);
                     return target;
                 }
 
@@ -11039,13 +11040,18 @@ parse_arguments(pm_parser_t *parser, pm_arguments_t *arguments, bool accepts_for
         return;
     }
 
+    bool parsed_first_argument = false;
     bool parsed_bare_hash = false;
     bool parsed_block_argument = false;
+    bool parsed_forwarding_arguments = false;
 
     while (!match1(parser, PM_TOKEN_EOF)) {
         if (parsed_block_argument) {
             pm_parser_err_current(parser, PM_ERR_ARGUMENT_AFTER_BLOCK);
         }
+        if (parsed_forwarding_arguments) {
+            pm_parser_err_current(parser, PM_ERR_ARGUMENT_AFTER_FORWARDING_ELLIPSES);
+        }
 
         pm_node_t *argument = NULL;
 
@@ -11129,9 +11135,13 @@ parse_arguments(pm_parser_t *parser, pm_arguments_t *arguments, bool accepts_for
                         if (pm_parser_local_depth(parser, &parser->previous) == -1) {
                             pm_parser_err_previous(parser, PM_ERR_ARGUMENT_NO_FORWARDING_ELLIPSES);
                         }
+                        if (parsed_first_argument && terminator == PM_TOKEN_EOF) {
+                            pm_parser_err_previous(parser, PM_ERR_ARGUMENT_FORWARDING_UNBOUND);
+                        }
 
                         argument = (pm_node_t *) pm_forwarding_arguments_node_create(parser, &parser->previous);
                         parse_arguments_append(parser, arguments, argument);
+                        parsed_forwarding_arguments = true;
                         break;
                     }
                 }
@@ -11183,6 +11193,8 @@ parse_arguments(pm_parser_t *parser, pm_arguments_t *arguments, bool accepts_for
             }
         }
 
+        parsed_first_argument = true;
+
         // If parsing the argument failed, we need to stop parsing arguments.
         if (PM_NODE_TYPE_P(argument, PM_MISSING_NODE) || parser->recovering) break;
 
@@ -12499,7 +12511,7 @@ parse_variable_call(pm_parser_t *parser) {
             return (pm_node_t *) pm_local_variable_read_node_create(parser, &parser->previous, (uint32_t) depth);
         }
 
-        if (!parser->current_scope->closed && token_is_numbered_parameter(parser->previous.start, parser->previous.end)) {
+        if (!parser->current_scope->closed && pm_token_is_numbered_parameter(parser->previous.start, parser->previous.end)) {
             // Indicate that this scope is using numbered params so that child
             // scopes cannot.
             parser->current_scope->numbered_params = true;
@@ -12546,15 +12558,23 @@ parse_variable_call(pm_parser_t *parser) {
     return (pm_node_t *) node;
 }
 
+/**
+ * Parse the method definition name based on the current token available on the
+ * parser. If it does not match a valid method definition name, then a missing
+ * token is returned.
+ */
 static inline pm_token_t
 parse_method_definition_name(pm_parser_t *parser) {
     switch (parser->current.type) {
         case PM_CASE_KEYWORD:
         case PM_TOKEN_CONSTANT:
-        case PM_TOKEN_IDENTIFIER:
         case PM_TOKEN_METHOD_NAME:
             parser_lex(parser);
             return parser->previous;
+        case PM_TOKEN_IDENTIFIER:
+            pm_refute_numbered_parameter(parser, parser->current.start, parser->current.end);
+            parser_lex(parser);
+            return parser->previous;
         case PM_CASE_OPERATOR:
             lex_state_set(parser, PM_LEX_STATE_ENDFN);
             parser_lex(parser);
@@ -14506,6 +14526,7 @@ parse_expression_prefix(pm_parser_t *parser, pm_binding_power_t binding_power) {
                         operator = parser->previous;
                         name = parse_method_definition_name(parser);
                     } else {
+                        pm_refute_numbered_parameter(parser, parser->previous.start, parser->previous.end);
                         pm_parser_scope_push(parser, true);
                         name = parser->previous;
                     }
@@ -15646,10 +15667,29 @@ parse_expression_prefix(pm_parser_t *parser, pm_binding_power_t binding_power) {
 }
 
 static inline pm_node_t *
-parse_assignment_value(pm_parser_t *parser, pm_binding_power_t previous_binding_power, pm_binding_power_t binding_power, pm_diagnostic_id_t diag_id) {
+parse_assignment_value(pm_parser_t *parser, pm_binding_power_t binding_power, pm_diagnostic_id_t diag_id) {
+    pm_node_t *value = parse_value_expression(parser, binding_power, diag_id);
+
+    // Contradicting binding powers, the right-hand-side value of rthe assignment allows the `rescue` modifier.
+    if (match1(parser, PM_TOKEN_KEYWORD_RESCUE_MODIFIER)) {
+        pm_token_t rescue = parser->current;
+        parser_lex(parser);
+        pm_node_t *right = parse_expression(parser, binding_power, PM_ERR_RESCUE_MODIFIER_VALUE);
+
+        return (pm_node_t *) pm_rescue_modifier_node_create(parser, value, &rescue, right);
+    }
+
+    return value;
+}
+
+
+static inline pm_node_t *
+parse_assignment_values(pm_parser_t *parser, pm_binding_power_t previous_binding_power, pm_binding_power_t binding_power, pm_diagnostic_id_t diag_id) {
     pm_node_t *value = parse_starred_expression(parser, binding_power, diag_id);
 
+    bool is_single_value = true;
     if (previous_binding_power == PM_BINDING_POWER_STATEMENT && (PM_NODE_TYPE_P(value, PM_SPLAT_NODE) || match1(parser, PM_TOKEN_COMMA))) {
+        is_single_value = false;
         pm_token_t opening = not_provided(parser);
         pm_array_node_t *array = pm_array_node_create(parser, &opening);
 
@@ -15663,6 +15703,15 @@ parse_assignment_value(pm_parser_t *parser, pm_binding_power_t previous_binding_
         }
     }
 
+    // Contradicting binding powers, the right-hand-side value of the assignment allows the `rescue` modifier.
+    if (is_single_value && match1(parser, PM_TOKEN_KEYWORD_RESCUE_MODIFIER)) {
+        pm_token_t rescue = parser->current;
+        parser_lex(parser);
+        pm_node_t *right = parse_expression(parser, binding_power, PM_ERR_RESCUE_MODIFIER_VALUE);
+
+        return (pm_node_t *) pm_rescue_modifier_node_create(parser, value, &rescue, right);
+    }
+
     return value;
 }
 
@@ -15736,10 +15785,7 @@ parse_regular_expression_named_captures(pm_parser_t *parser, const pm_string_t *
                 // If the unescaped string is a slice of the source, then we can
                 // copy the names directly. The pointers will line up.
                 local = pm_parser_local_add_location(parser, source, source + length);
-
-                if (token_is_numbered_parameter(source, source + length)) {
-                    pm_parser_err(parser, source, source + length, PM_ERR_PARAMETER_NUMBERED_RESERVED);
-                }
+                pm_refute_numbered_parameter(parser, source, source + length);
             } else {
                 // Otherwise, the name is a slice of the malloc-ed owned string,
                 // in which case we need to copy it out into a new string.
@@ -15752,7 +15798,7 @@ parse_regular_expression_named_captures(pm_parser_t *parser, const pm_string_t *
                 // NOLINTNEXTLINE(clang-analyzer-*)
                 local = pm_parser_local_add_owned(parser, (const uint8_t *) memory, length);
 
-                if (token_is_numbered_parameter(source, source + length)) {
+                if (pm_token_is_numbered_parameter(source, source + length)) {
                     const pm_location_t *location = &call->receiver->location;
                     pm_parser_err_location(parser, location, PM_ERR_PARAMETER_NUMBERED_RESERVED);
                 }
@@ -15802,7 +15848,7 @@ parse_expression_infix(pm_parser_t *parser, pm_node_t *node, pm_binding_power_t
                 /* fallthrough */
                 case PM_CASE_WRITABLE: {
                     parser_lex(parser);
-                    pm_node_t *value = parse_assignment_value(parser, previous_binding_power, binding_power, PM_ERR_EXPECT_EXPRESSION_AFTER_EQUAL);
+                    pm_node_t *value = parse_assignment_values(parser, previous_binding_power, binding_power, PM_ERR_EXPECT_EXPRESSION_AFTER_EQUAL);
                     return parse_write(parser, node, &token, value);
                 }
                 case PM_SPLAT_NODE: {
@@ -15810,7 +15856,7 @@ parse_expression_infix(pm_parser_t *parser, pm_node_t *node, pm_binding_power_t
                     pm_multi_target_node_targets_append(parser, multi_target, node);
 
                     parser_lex(parser);
-                    pm_node_t *value = parse_assignment_value(parser, previous_binding_power, binding_power, PM_ERR_EXPECT_EXPRESSION_AFTER_EQUAL);
+                    pm_node_t *value = parse_assignment_values(parser, previous_binding_power, binding_power, PM_ERR_EXPECT_EXPRESSION_AFTER_EQUAL);
                     return parse_write(parser, (pm_node_t *) multi_target, &token, value);
                 }
                 default:
@@ -15832,7 +15878,7 @@ parse_expression_infix(pm_parser_t *parser, pm_node_t *node, pm_binding_power_t
                 case PM_GLOBAL_VARIABLE_READ_NODE: {
                     parser_lex(parser);
 
-                    pm_node_t *value = parse_expression(parser, binding_power, PM_ERR_EXPECT_EXPRESSION_AFTER_AMPAMPEQ);
+                    pm_node_t *value = parse_assignment_value(parser, binding_power, PM_ERR_EXPECT_EXPRESSION_AFTER_AMPAMPEQ);
                     pm_node_t *result = (pm_node_t *) pm_global_variable_and_write_node_create(parser, node, &token, value);
 
                     pm_node_destroy(parser, node);
@@ -15841,7 +15887,7 @@ parse_expression_infix(pm_parser_t *parser, pm_node_t *node, pm_binding_power_t
                 case PM_CLASS_VARIABLE_READ_NODE: {
                     parser_lex(parser);
 
-                    pm_node_t *value = parse_expression(parser, binding_power, PM_ERR_EXPECT_EXPRESSION_AFTER_AMPAMPEQ);
+                    pm_node_t *value = parse_assignment_value(parser, binding_power, PM_ERR_EXPECT_EXPRESSION_AFTER_AMPAMPEQ);
                     pm_node_t *result = (pm_node_t *) pm_class_variable_and_write_node_create(parser, (pm_class_variable_read_node_t *) node, &token, value);
 
                     pm_node_destroy(parser, node);
@@ -15850,13 +15896,13 @@ parse_expression_infix(pm_parser_t *parser, pm_node_t *node, pm_binding_power_t
                 case PM_CONSTANT_PATH_NODE: {
                     parser_lex(parser);
 
-                    pm_node_t *value = parse_expression(parser, binding_power, PM_ERR_EXPECT_EXPRESSION_AFTER_AMPAMPEQ);
+                    pm_node_t *value = parse_assignment_value(parser, binding_power, PM_ERR_EXPECT_EXPRESSION_AFTER_AMPAMPEQ);
                     return (pm_node_t *) pm_constant_path_and_write_node_create(parser, (pm_constant_path_node_t *) node, &token, value);
                 }
                 case PM_CONSTANT_READ_NODE: {
                     parser_lex(parser);
 
-                    pm_node_t *value = parse_expression(parser, binding_power, PM_ERR_EXPECT_EXPRESSION_AFTER_AMPAMPEQ);
+                    pm_node_t *value = parse_assignment_value(parser, binding_power, PM_ERR_EXPECT_EXPRESSION_AFTER_AMPAMPEQ);
                     pm_node_t *result = (pm_node_t *) pm_constant_and_write_node_create(parser, (pm_constant_read_node_t *) node, &token, value);
 
                     pm_node_destroy(parser, node);
@@ -15865,7 +15911,7 @@ parse_expression_infix(pm_parser_t *parser, pm_node_t *node, pm_binding_power_t
                 case PM_INSTANCE_VARIABLE_READ_NODE: {
                     parser_lex(parser);
 
-                    pm_node_t *value = parse_expression(parser, binding_power, PM_ERR_EXPECT_EXPRESSION_AFTER_AMPAMPEQ);
+                    pm_node_t *value = parse_assignment_value(parser, binding_power, PM_ERR_EXPECT_EXPRESSION_AFTER_AMPAMPEQ);
                     pm_node_t *result = (pm_node_t *) pm_instance_variable_and_write_node_create(parser, (pm_instance_variable_read_node_t *) node, &token, value);
 
                     pm_node_destroy(parser, node);
@@ -15875,7 +15921,7 @@ parse_expression_infix(pm_parser_t *parser, pm_node_t *node, pm_binding_power_t
                     pm_local_variable_read_node_t *cast = (pm_local_variable_read_node_t *) node;
                     parser_lex(parser);
 
-                    pm_node_t *value = parse_expression(parser, binding_power, PM_ERR_EXPECT_EXPRESSION_AFTER_AMPAMPEQ);
+                    pm_node_t *value = parse_assignment_value(parser, binding_power, PM_ERR_EXPECT_EXPRESSION_AFTER_AMPAMPEQ);
                     pm_node_t *result = (pm_node_t *) pm_local_variable_and_write_node_create(parser, node, &token, value, cast->name, cast->depth);
 
                     pm_node_destroy(parser, node);
@@ -15889,14 +15935,11 @@ parse_expression_infix(pm_parser_t *parser, pm_node_t *node, pm_binding_power_t
                     // receiver that could have been a local variable) then we
                     // will transform it into a local variable write.
                     if (pm_call_node_variable_call_p(cast)) {
-                        pm_location_t message_loc = cast->message_loc;
-                        pm_constant_id_t constant_id = pm_parser_local_add_location(parser, message_loc.start, message_loc.end);
+                        pm_location_t *message_loc = &cast->message_loc;
+                        pm_refute_numbered_parameter(parser, message_loc->start, message_loc->end);
 
-                        if (token_is_numbered_parameter(message_loc.start, message_loc.end)) {
-                            pm_parser_err_location(parser, &message_loc, PM_ERR_PARAMETER_NUMBERED_RESERVED);
-                        }
-
-                        pm_node_t *value = parse_expression(parser, binding_power, PM_ERR_EXPECT_EXPRESSION_AFTER_AMPAMPEQ);
+                        pm_constant_id_t constant_id = pm_parser_local_add_location(parser, message_loc->start, message_loc->end);
+                        pm_node_t *value = parse_assignment_value(parser, binding_power, PM_ERR_EXPECT_EXPRESSION_AFTER_AMPAMPEQ);
                         pm_node_t *result = (pm_node_t *) pm_local_variable_and_write_node_create(parser, (pm_node_t *) cast, &token, value, constant_id, 0);
 
                         pm_node_destroy(parser, (pm_node_t *) cast);
@@ -15907,7 +15950,7 @@ parse_expression_infix(pm_parser_t *parser, pm_node_t *node, pm_binding_power_t
                     // this is an aref expression, and we can transform it into
                     // an aset expression.
                     if (pm_call_node_index_p(cast)) {
-                        pm_node_t *value = parse_expression(parser, binding_power, PM_ERR_EXPECT_EXPRESSION_AFTER_AMPAMPEQ);
+                        pm_node_t *value = parse_assignment_value(parser, binding_power, PM_ERR_EXPECT_EXPRESSION_AFTER_AMPAMPEQ);
                         return (pm_node_t *) pm_index_and_write_node_create(parser, cast, &token, value);
                     }
 
@@ -15919,7 +15962,7 @@ parse_expression_infix(pm_parser_t *parser, pm_node_t *node, pm_binding_power_t
                     }
 
                     parse_call_operator_write(parser, cast, &token);
-                    pm_node_t *value = parse_expression(parser, binding_power, PM_ERR_EXPECT_EXPRESSION_AFTER_AMPAMPEQ);
+                    pm_node_t *value = parse_assignment_value(parser, binding_power, PM_ERR_EXPECT_EXPRESSION_AFTER_AMPAMPEQ);
                     return (pm_node_t *) pm_call_and_write_node_create(parser, cast, &token, value);
                 }
                 case PM_MULTI_WRITE_NODE: {
@@ -15946,7 +15989,7 @@ parse_expression_infix(pm_parser_t *parser, pm_node_t *node, pm_binding_power_t
                 case PM_GLOBAL_VARIABLE_READ_NODE: {
                     parser_lex(parser);
 
-                    pm_node_t *value = parse_expression(parser, binding_power, PM_ERR_EXPECT_EXPRESSION_AFTER_PIPEPIPEEQ);
+                    pm_node_t *value = parse_assignment_value(parser, binding_power, PM_ERR_EXPECT_EXPRESSION_AFTER_PIPEPIPEEQ);
                     pm_node_t *result = (pm_node_t *) pm_global_variable_or_write_node_create(parser, node, &token, value);
 
                     pm_node_destroy(parser, node);
@@ -15955,7 +15998,7 @@ parse_expression_infix(pm_parser_t *parser, pm_node_t *node, pm_binding_power_t
                 case PM_CLASS_VARIABLE_READ_NODE: {
                     parser_lex(parser);
 
-                    pm_node_t *value = parse_expression(parser, binding_power, PM_ERR_EXPECT_EXPRESSION_AFTER_PIPEPIPEEQ);
+                    pm_node_t *value = parse_assignment_value(parser, binding_power, PM_ERR_EXPECT_EXPRESSION_AFTER_PIPEPIPEEQ);
                     pm_node_t *result = (pm_node_t *) pm_class_variable_or_write_node_create(parser, (pm_class_variable_read_node_t *) node, &token, value);
 
                     pm_node_destroy(parser, node);
@@ -15964,13 +16007,13 @@ parse_expression_infix(pm_parser_t *parser, pm_node_t *node, pm_binding_power_t
                 case PM_CONSTANT_PATH_NODE: {
                     parser_lex(parser);
 
-                    pm_node_t *value = parse_expression(parser, binding_power, PM_ERR_EXPECT_EXPRESSION_AFTER_PIPEPIPEEQ);
+                    pm_node_t *value = parse_assignment_value(parser, binding_power, PM_ERR_EXPECT_EXPRESSION_AFTER_PIPEPIPEEQ);
                     return (pm_node_t *) pm_constant_path_or_write_node_create(parser, (pm_constant_path_node_t *) node, &token, value);
                 }
                 case PM_CONSTANT_READ_NODE: {
                     parser_lex(parser);
 
-                    pm_node_t *value = parse_expression(parser, binding_power, PM_ERR_EXPECT_EXPRESSION_AFTER_PIPEPIPEEQ);
+                    pm_node_t *value = parse_assignment_value(parser, binding_power, PM_ERR_EXPECT_EXPRESSION_AFTER_PIPEPIPEEQ);
                     pm_node_t *result = (pm_node_t *) pm_constant_or_write_node_create(parser, (pm_constant_read_node_t *) node, &token, value);
 
                     pm_node_destroy(parser, node);
@@ -15979,7 +16022,7 @@ parse_expression_infix(pm_parser_t *parser, pm_node_t *node, pm_binding_power_t
                 case PM_INSTANCE_VARIABLE_READ_NODE: {
                     parser_lex(parser);
 
-                    pm_node_t *value = parse_expression(parser, binding_power, PM_ERR_EXPECT_EXPRESSION_AFTER_PIPEPIPEEQ);
+                    pm_node_t *value = parse_assignment_value(parser, binding_power, PM_ERR_EXPECT_EXPRESSION_AFTER_PIPEPIPEEQ);
                     pm_node_t *result = (pm_node_t *) pm_instance_variable_or_write_node_create(parser, (pm_instance_variable_read_node_t *) node, &token, value);
 
                     pm_node_destroy(parser, node);
@@ -15989,7 +16032,7 @@ parse_expression_infix(pm_parser_t *parser, pm_node_t *node, pm_binding_power_t
                     pm_local_variable_read_node_t *cast = (pm_local_variable_read_node_t *) node;
                     parser_lex(parser);
 
-                    pm_node_t *value = parse_expression(parser, binding_power, PM_ERR_EXPECT_EXPRESSION_AFTER_PIPEPIPEEQ);
+                    pm_node_t *value = parse_assignment_value(parser, binding_power, PM_ERR_EXPECT_EXPRESSION_AFTER_PIPEPIPEEQ);
                     pm_node_t *result = (pm_node_t *) pm_local_variable_or_write_node_create(parser, node, &token, value, cast->name, cast->depth);
 
                     pm_node_destroy(parser, node);
@@ -16003,14 +16046,11 @@ parse_expression_infix(pm_parser_t *parser, pm_node_t *node, pm_binding_power_t
                     // receiver that could have been a local variable) then we
                     // will transform it into a local variable write.
                     if (pm_call_node_variable_call_p(cast)) {
-                        pm_location_t message_loc = cast->message_loc;
-                        pm_constant_id_t constant_id = pm_parser_local_add_location(parser, message_loc.start, message_loc.end);
-
-                        if (token_is_numbered_parameter(message_loc.start, message_loc.end)) {
-                            pm_parser_err_location(parser, &message_loc, PM_ERR_PARAMETER_NUMBERED_RESERVED);
-                        }
+                        pm_location_t *message_loc = &cast->message_loc;
+                        pm_refute_numbered_parameter(parser, message_loc->start, message_loc->end);
 
-                        pm_node_t *value = parse_expression(parser, binding_power, PM_ERR_EXPECT_EXPRESSION_AFTER_PIPEPIPEEQ);
+                        pm_constant_id_t constant_id = pm_parser_local_add_location(parser, message_loc->start, message_loc->end);
+                        pm_node_t *value = parse_assignment_value(parser, binding_power, PM_ERR_EXPECT_EXPRESSION_AFTER_PIPEPIPEEQ);
                         pm_node_t *result = (pm_node_t *) pm_local_variable_or_write_node_create(parser, (pm_node_t *) cast, &token, value, constant_id, 0);
 
                         pm_node_destroy(parser, (pm_node_t *) cast);
@@ -16021,7 +16061,7 @@ parse_expression_infix(pm_parser_t *parser, pm_node_t *node, pm_binding_power_t
                     // this is an aref expression, and we can transform it into
                     // an aset expression.
                     if (pm_call_node_index_p(cast)) {
-                        pm_node_t *value = parse_expression(parser, binding_power, PM_ERR_EXPECT_EXPRESSION_AFTER_PIPEPIPEEQ);
+                        pm_node_t *value = parse_assignment_value(parser, binding_power, PM_ERR_EXPECT_EXPRESSION_AFTER_PIPEPIPEEQ);
                         return (pm_node_t *) pm_index_or_write_node_create(parser, cast, &token, value);
                     }
 
@@ -16033,7 +16073,7 @@ parse_expression_infix(pm_parser_t *parser, pm_node_t *node, pm_binding_power_t
                     }
 
                     parse_call_operator_write(parser, cast, &token);
-                    pm_node_t *value = parse_expression(parser, binding_power, PM_ERR_EXPECT_EXPRESSION_AFTER_PIPEPIPEEQ);
+                    pm_node_t *value = parse_assignment_value(parser, binding_power, PM_ERR_EXPECT_EXPRESSION_AFTER_PIPEPIPEEQ);
                     return (pm_node_t *) pm_call_or_write_node_create(parser, cast, &token, value);
                 }
                 case PM_MULTI_WRITE_NODE: {
@@ -16070,7 +16110,7 @@ parse_expression_infix(pm_parser_t *parser, pm_node_t *node, pm_binding_power_t
                 case PM_GLOBAL_VARIABLE_READ_NODE: {
                     parser_lex(parser);
 
-                    pm_node_t *value = parse_expression(parser, binding_power, PM_ERR_EXPECT_EXPRESSION_AFTER_OPERATOR);
+                    pm_node_t *value = parse_assignment_value(parser, binding_power, PM_ERR_EXPECT_EXPRESSION_AFTER_OPERATOR);
                     pm_node_t *result = (pm_node_t *) pm_global_variable_operator_write_node_create(parser, node, &token, value);
 
                     pm_node_destroy(parser, node);
@@ -16079,7 +16119,7 @@ parse_expression_infix(pm_parser_t *parser, pm_node_t *node, pm_binding_power_t
                 case PM_CLASS_VARIABLE_READ_NODE: {
                     parser_lex(parser);
 
-                    pm_node_t *value = parse_expression(parser, binding_power, PM_ERR_EXPECT_EXPRESSION_AFTER_OPERATOR);
+                    pm_node_t *value = parse_assignment_value(parser, binding_power, PM_ERR_EXPECT_EXPRESSION_AFTER_OPERATOR);
                     pm_node_t *result = (pm_node_t *) pm_class_variable_operator_write_node_create(parser, (pm_class_variable_read_node_t *) node, &token, value);
 
                     pm_node_destroy(parser, node);
@@ -16088,13 +16128,13 @@ parse_expression_infix(pm_parser_t *parser, pm_node_t *node, pm_binding_power_t
                 case PM_CONSTANT_PATH_NODE: {
                     parser_lex(parser);
 
-                    pm_node_t *value = parse_expression(parser, binding_power, PM_ERR_EXPECT_EXPRESSION_AFTER_OPERATOR);
+                    pm_node_t *value = parse_assignment_value(parser, binding_power, PM_ERR_EXPECT_EXPRESSION_AFTER_OPERATOR);
                     return (pm_node_t *) pm_constant_path_operator_write_node_create(parser, (pm_constant_path_node_t *) node, &token, value);
                 }
                 case PM_CONSTANT_READ_NODE: {
                     parser_lex(parser);
 
-                    pm_node_t *value = parse_expression(parser, binding_power, PM_ERR_EXPECT_EXPRESSION_AFTER_OPERATOR);
+                    pm_node_t *value = parse_assignment_value(parser, binding_power, PM_ERR_EXPECT_EXPRESSION_AFTER_OPERATOR);
                     pm_node_t *result = (pm_node_t *) pm_constant_operator_write_node_create(parser, (pm_constant_read_node_t *) node, &token, value);
 
                     pm_node_destroy(parser, node);
@@ -16103,7 +16143,7 @@ parse_expression_infix(pm_parser_t *parser, pm_node_t *node, pm_binding_power_t
                 case PM_INSTANCE_VARIABLE_READ_NODE: {
                     parser_lex(parser);
 
-                    pm_node_t *value = parse_expression(parser, binding_power, PM_ERR_EXPECT_EXPRESSION_AFTER_OPERATOR);
+                    pm_node_t *value = parse_assignment_value(parser, binding_power, PM_ERR_EXPECT_EXPRESSION_AFTER_OPERATOR);
                     pm_node_t *result = (pm_node_t *) pm_instance_variable_operator_write_node_create(parser, (pm_instance_variable_read_node_t *) node, &token, value);
 
                     pm_node_destroy(parser, node);
@@ -16113,7 +16153,7 @@ parse_expression_infix(pm_parser_t *parser, pm_node_t *node, pm_binding_power_t
                     pm_local_variable_read_node_t *cast = (pm_local_variable_read_node_t *) node;
                     parser_lex(parser);
 
-                    pm_node_t *value = parse_expression(parser, binding_power, PM_ERR_EXPECT_EXPRESSION_AFTER_OPERATOR);
+                    pm_node_t *value = parse_assignment_value(parser, binding_power, PM_ERR_EXPECT_EXPRESSION_AFTER_OPERATOR);
                     pm_node_t *result = (pm_node_t *) pm_local_variable_operator_write_node_create(parser, node, &token, value, cast->name, cast->depth);
 
                     pm_node_destroy(parser, node);
@@ -16127,14 +16167,11 @@ parse_expression_infix(pm_parser_t *parser, pm_node_t *node, pm_binding_power_t
                     // receiver that could have been a local variable) then we
                     // will transform it into a local variable write.
                     if (pm_call_node_variable_call_p(cast)) {
-                        pm_location_t message_loc = cast->message_loc;
-                        pm_constant_id_t constant_id = pm_parser_local_add_location(parser, message_loc.start, message_loc.end);
-
-                        if (token_is_numbered_parameter(message_loc.start, message_loc.end)) {
-                            pm_parser_err_location(parser, &message_loc, PM_ERR_PARAMETER_NUMBERED_RESERVED);
-                        }
+                        pm_location_t *message_loc = &cast->message_loc;
+                        pm_refute_numbered_parameter(parser, message_loc->start, message_loc->end);
 
-                        pm_node_t *value = parse_expression(parser, binding_power, PM_ERR_EXPECT_EXPRESSION_AFTER_OPERATOR);
+                        pm_constant_id_t constant_id = pm_parser_local_add_location(parser, message_loc->start, message_loc->end);
+                        pm_node_t *value = parse_assignment_value(parser, binding_power, PM_ERR_EXPECT_EXPRESSION_AFTER_OPERATOR);
                         pm_node_t *result = (pm_node_t *) pm_local_variable_operator_write_node_create(parser, (pm_node_t *) cast, &token, value, constant_id, 0);
 
                         pm_node_destroy(parser, (pm_node_t *) cast);
@@ -16145,7 +16182,7 @@ parse_expression_infix(pm_parser_t *parser, pm_node_t *node, pm_binding_power_t
                     // this is an aref expression, and we can transform it into
                     // an aset expression.
                     if (pm_call_node_index_p(cast)) {
-                        pm_node_t *value = parse_expression(parser, binding_power, PM_ERR_EXPECT_EXPRESSION_AFTER_OPERATOR);
+                        pm_node_t *value = parse_assignment_value(parser, binding_power, PM_ERR_EXPECT_EXPRESSION_AFTER_OPERATOR);
                         return (pm_node_t *) pm_index_operator_write_node_create(parser, cast, &token, value);
                     }
 
@@ -16157,7 +16194,7 @@ parse_expression_infix(pm_parser_t *parser, pm_node_t *node, pm_binding_power_t
                     }
 
                     parse_call_operator_write(parser, cast, &token);
-                    pm_node_t *value = parse_expression(parser, binding_power, PM_ERR_EXPECT_EXPRESSION_AFTER_OPERATOR);
+                    pm_node_t *value = parse_assignment_value(parser, binding_power, PM_ERR_EXPECT_EXPRESSION_AFTER_OPERATOR);
                     return (pm_node_t *) pm_call_operator_write_node_create(parser, cast, &token, value);
                 }
                 case PM_MULTI_WRITE_NODE: {
diff --git a/src/regexp.c b/src/regexp.c
index 22833d17..cd52857f 100644
--- a/src/regexp.c
+++ b/src/regexp.c
@@ -116,18 +116,18 @@ pm_regexp_char_find(pm_regexp_parser_t *parser, uint8_t value) {
  * The properly track everything, we're going to build a little state machine.
  * It looks something like the following:
  *
- *                  ┌───────┐                 ┌─────────┐ ────────────┐
- * ──── lbrace ───> │ start │ ──── digit ───> │ minimum │             │
- *                  └───────┘                 └─────────┘ <─── digit ─┘
- *                      │                       │    │
- *   ┌───────┐          │                       │  rbrace
- *   │ comma │ <───── comma  ┌──── comma ───────┘    │
- *   └───────┘               V                       V
- *      │             ┌─────────┐               ┌─────────┐
- *      └── digit ──> │ maximum │ ── rbrace ──> │| final |│
- *                    └─────────┘               └─────────┘
- *                    │         ^
- *                    └─ digit ─┘
+ *                  +-------+                 +---------+ ------------+
+ * ---- lbrace ---> | start | ---- digit ---> | minimum |             |
+ *                  +-------+                 +---------+ <--- digit -+
+ *                      |                       |    |
+ *   +-------+          |                       |  rbrace
+ *   | comma | <----- comma  +---- comma -------+    |
+ *   +-------+               V                       V
+ *      |             +---------+               +---------+
+ *      +-- digit --> | maximum | -- rbrace --> || final ||
+ *                    +---------+               +---------+
+ *                    |         ^
+ *                    +- digit -+
  *
  * Note that by the time we've hit this function, the lbrace has already been
  * consumed so we're in the start state.
diff --git a/templates/lib/prism/serialize.rb.erb b/templates/lib/prism/serialize.rb.erb
index 28375045..e5a88ae9 100644
--- a/templates/lib/prism/serialize.rb.erb
+++ b/templates/lib/prism/serialize.rb.erb
@@ -73,12 +73,9 @@ module Prism
       end
 
       def load_encoding
-        Encoding.find(io.read(load_varint))
-      end
-
-      def load_force_encoding
-        @encoding = load_encoding
+        @encoding = Encoding.find(io.read(load_varint))
         @input = input.force_encoding(@encoding).freeze
+        @encoding
       end
 
       def load_start_line
@@ -121,10 +118,7 @@ module Prism
         encoding = load_encoding
         load_start_line
         comments, magic_comments, errors, warnings = load_metadata
-
-        if encoding != @encoding
-          tokens.each { |token,| token.value.force_encoding(encoding) }
-        end
+        tokens.each { |token,| token.value.force_encoding(encoding) }
 
         raise "Expected to consume all bytes while deserializing" unless @io.eof?
         Prism::ParseResult.new(tokens, comments, magic_comments, errors, warnings, @source)
@@ -132,7 +126,7 @@ module Prism
 
       def load_nodes
         load_header
-        load_force_encoding
+        load_encoding
         load_start_line
 
         comments, magic_comments, errors, warnings = load_metadata
diff --git a/templates/src/prettyprint.c.erb b/templates/src/prettyprint.c.erb
index d5bf196e..ed820d5e 100644
--- a/templates/src/prettyprint.c.erb
+++ b/templates/src/prettyprint.c.erb
@@ -1,3 +1,4 @@
+<%# encoding: ASCII -%>
 #include "prism/prettyprint.h"
 
 static void
@@ -63,29 +64,29 @@ prettyprint_node(pm_buffer_t *output_buffer, const pm_parser_t *parser, const pm
             prettyprint_location(output_buffer, parser, &node->location);
             pm_buffer_append_string(output_buffer, ")\n", 2);
             <%- node.fields.each_with_index do |field, index| -%>
-            <%- pointer, preadd = index == node.fields.length - 1 ? ["└── ", "    "] : ["├── ", "│   "] -%>
+            <%- pointer, preadd, preadd_bytesize = index == node.fields.length - 1 ? ["\\xe2\\x94\\x94\\xe2\\x94\\x80\\xe2\\x94\\x80 ", "    ", 4] : ["\\xe2\\x94\\x9c\\xe2\\x94\\x80\\xe2\\x94\\x80 ", "\\xe2\\x94\\x82   ", 6] -%>
 
             // <%= field.name %>
             {
                 pm_buffer_concat(output_buffer, prefix_buffer);
-                pm_buffer_append_string(output_buffer, "<%= pointer %><%= field.name %>:", <%= pointer.bytesize + field.name.length + 1 %>);
+                pm_buffer_append_string(output_buffer, "<%= pointer %><%= field.name %>:", <%= 10 + field.name.length + 1 %>);
             <%- case field -%>
             <%- when Prism::NodeField -%>
                 pm_buffer_append_byte(output_buffer, '\n');
 
                 size_t prefix_length = prefix_buffer->length;
-                pm_buffer_append_string(prefix_buffer, "<%= preadd %>", <%= preadd.bytesize %>);
+                pm_buffer_append_string(prefix_buffer, "<%= preadd %>", <%= preadd_bytesize %>);
                 pm_buffer_concat(output_buffer, prefix_buffer);
                 prettyprint_node(output_buffer, parser, (pm_node_t *) cast-><%= field.name %>, prefix_buffer);
                 prefix_buffer->length = prefix_length;
             <%- when Prism::OptionalNodeField -%>
                 if (cast-><%= field.name %> == NULL) {
-                    pm_buffer_append_string(output_buffer, " ∅\n", 5);
+                    pm_buffer_append_string(output_buffer, " \xe2\x88\x85\n", 5);
                 } else {
                     pm_buffer_append_byte(output_buffer, '\n');
 
                     size_t prefix_length = prefix_buffer->length;
-                    pm_buffer_append_string(prefix_buffer, "<%= preadd %>", <%= preadd.bytesize %>);
+                    pm_buffer_append_string(prefix_buffer, "<%= preadd %>", <%= preadd_bytesize %>);
                     pm_buffer_concat(output_buffer, prefix_buffer);
                     prettyprint_node(output_buffer, parser, (pm_node_t *) cast-><%= field.name %>, prefix_buffer);
                     prefix_buffer->length = prefix_length;
@@ -100,15 +101,15 @@ prettyprint_node(pm_buffer_t *output_buffer, const pm_parser_t *parser, const pm
                 size_t last_index = cast-><%= field.name %>.size;
                 for (uint32_t index = 0; index < last_index; index++) {
                     size_t prefix_length = prefix_buffer->length;
-                    pm_buffer_append_string(prefix_buffer, "<%= preadd %>", <%= preadd.bytesize %>);
+                    pm_buffer_append_string(prefix_buffer, "<%= preadd %>", <%= preadd_bytesize %>);
                     pm_buffer_concat(output_buffer, prefix_buffer);
 
                     if (index == last_index - 1) {
-                        pm_buffer_append_string(output_buffer, "└── ", 10);
+                        pm_buffer_append_string(output_buffer, "\xe2\x94\x94\xe2\x94\x80\xe2\x94\x80 ", 10);
                         pm_buffer_append_string(prefix_buffer, "    ", 4);
                     } else {
-                        pm_buffer_append_string(output_buffer, "├── ", 10);
-                        pm_buffer_append_string(prefix_buffer, "│   ", 6);
+                        pm_buffer_append_string(output_buffer, "\xe2\x94\x9c\xe2\x94\x80\xe2\x94\x80 ", 10);
+                        pm_buffer_append_string(prefix_buffer, "\xe2\x94\x82   ", 6);
                     }
 
                     prettyprint_node(output_buffer, parser, (pm_node_t *) cast-><%= field.name %>.nodes[index], prefix_buffer);
@@ -120,7 +121,7 @@ prettyprint_node(pm_buffer_t *output_buffer, const pm_parser_t *parser, const pm
                 pm_buffer_append_byte(output_buffer, '\n');
             <%- when Prism::OptionalConstantField -%>
                 if (cast-><%= field.name %> == 0) {
-                    pm_buffer_append_string(output_buffer, " ∅\n", 5);
+                    pm_buffer_append_string(output_buffer, " \xe2\x88\x85\n", 5);
                 } else {
                     pm_buffer_append_byte(output_buffer, ' ');
                     prettyprint_constant(output_buffer, parser, cast-><%= field.name %>);
@@ -143,7 +144,7 @@ prettyprint_node(pm_buffer_t *output_buffer, const pm_parser_t *parser, const pm
             <%- when Prism::OptionalLocationField -%>
                 pm_location_t *location = &cast-><%= field.name %>;
                 if (location->start == NULL) {
-                    pm_buffer_append_string(output_buffer, " ∅\n", 5);
+                    pm_buffer_append_string(output_buffer, " \xe2\x88\x85\n", 5);
                 } else {
                     pm_buffer_append_byte(output_buffer, ' ');
                     prettyprint_location(output_buffer, parser, location);
@@ -163,7 +164,7 @@ prettyprint_node(pm_buffer_t *output_buffer, const pm_parser_t *parser, const pm
                     found = true;
                 }
                 <%- end -%>
-                if (!found) pm_buffer_append_string(output_buffer, " ∅", 4);
+                if (!found) pm_buffer_append_string(output_buffer, " \xe2\x88\x85", 4);
                 pm_buffer_append_byte(output_buffer, '\n');
             <%- else -%>
             <%- raise -%>
diff --git a/templates/template.rb b/templates/template.rb
index 777f8e51..204cb7a8 100755
--- a/templates/template.rb
+++ b/templates/template.rb
@@ -374,6 +374,14 @@ module Prism
       write_to ||= File.expand_path("../#{name}", __dir__)
       contents = heading + erb.result_with_hash(locals)
 
+      if (extension == ".c" || extension == ".h") && !contents.ascii_only?
+        # Enforce that we only have ASCII characters here. This is necessary
+        # for some locales that only allow ASCII characters in C source files.
+        contents.each_line.with_index(1) do |line, line_number|
+          raise "Non-ASCII character on line #{line_number} of #{write_to}" unless line.ascii_only?
+        end
+      end
+
       FileUtils.mkdir_p(File.dirname(write_to))
       File.write(write_to, contents)
     end


Test patch:
diff --git a/test/prism/encoding_test.rb b/test/prism/encoding_test.rb
index bc9aae8e..8704945d 100644
--- a/test/prism/encoding_test.rb
+++ b/test/prism/encoding_test.rb
@@ -23,9 +23,11 @@ module Prism
       Encoding::IBM860 =>       0x00...0x100,
       Encoding::IBM861 =>       0x00...0x100,
       Encoding::IBM862 =>       0x00...0x100,
+      Encoding::IBM863 =>       0x00...0x100,
       Encoding::IBM864 =>       0x00...0x100,
       Encoding::IBM865 =>       0x00...0x100,
       Encoding::IBM866 =>       0x00...0x100,
+      Encoding::IBM869 =>       0x00...0x100,
       Encoding::ISO_8859_1 =>   0x00...0x100,
       Encoding::ISO_8859_2 =>   0x00...0x100,
       Encoding::ISO_8859_3 =>   0x00...0x100,
@@ -43,6 +45,7 @@ module Prism
       Encoding::ISO_8859_16 =>  0x00...0x100,
       Encoding::KOI8_R =>       0x00...0x100,
       Encoding::MACCENTEURO =>  0x00...0x100,
+      Encoding::MACCROATIAN =>  0x00...0x100,
       Encoding::MACCYRILLIC =>  0x00...0x100,
       Encoding::MACGREEK =>     0x00...0x100,
       Encoding::MACICELAND =>   0x00...0x100,
diff --git a/test/prism/errors_test.rb b/test/prism/errors_test.rb
index f13327c1..070b36de 100644
--- a/test/prism/errors_test.rb
+++ b/test/prism/errors_test.rb
@@ -1263,6 +1263,13 @@ module Prism
       ]
     end
 
+    def test_defining_numbered_parameter
+      error_messages = ["Token reserved for a numbered parameter"]
+
+      assert_error_messages "def _1; end", error_messages
+      assert_error_messages "def self._1; end", error_messages
+    end
+
     def test_double_scope_numbered_parameters
       source = "-> { _1 + -> { _2 } }"
       errors = [["Numbered parameter is already used in outer scope", 15..17]]
@@ -1682,6 +1689,20 @@ module Prism
       ]
     end
 
+    def test_argument_after_ellipsis
+      source = 'def foo(...); foo(..., 1); end'
+      assert_errors expression(source), source, [
+        ['Unexpected argument after `...`', 23..24]
+      ]
+    end
+
+    def test_ellipsis_in_no_paren_call
+      source = 'def foo(...); foo 1, ...; end'
+      assert_errors expression(source), source, [
+        ['Unexpected `...` in an non-parenthesized call', 21..24]
+      ]
+    end
+
     private
 
     def assert_errors(expected, source, errors, compare_ripper: RUBY_ENGINE == "ruby")
diff --git a/test/prism/ruby_api_test.rb b/test/prism/ruby_api_test.rb
index a61282cc..cd87a813 100644
--- a/test/prism/ruby_api_test.rb
+++ b/test/prism/ruby_api_test.rb
@@ -71,6 +71,38 @@ module Prism
       end
     end
 
+    def test_location_character_offsets
+      program = Prism.parse("😀 + 😀\n😍 ||= 😍").value
+
+      # first 😀
+      location = program.statements.body.first.receiver.location
+      assert_equal 0, location.start_character_offset
+      assert_equal 1, location.end_character_offset
+      assert_equal 0, location.start_character_column
+      assert_equal 1, location.end_character_column
+
+      # second 😀
+      location = program.statements.body.first.arguments.arguments.first.location
+      assert_equal 4, location.start_character_offset
+      assert_equal 5, location.end_character_offset
+      assert_equal 4, location.start_character_column
+      assert_equal 5, location.end_character_column
+
+      # first 😍
+      location = program.statements.body.last.name_loc
+      assert_equal 6, location.start_character_offset
+      assert_equal 7, location.end_character_offset
+      assert_equal 0, location.start_character_column
+      assert_equal 1, location.end_character_column
+
+      # second 😍
+      location = program.statements.body.last.value.location
+      assert_equal 12, location.start_character_offset
+      assert_equal 13, location.end_character_offset
+      assert_equal 6, location.start_character_column
+      assert_equal 7, location.end_character_column
+    end
+
     private
 
     def parse_expression(source)
diff --git a/test/prism/snapshots/rescue.txt b/test/prism/snapshots/rescue.txt
index 0574af2f..be411671 100644
--- a/test/prism/snapshots/rescue.txt
+++ b/test/prism/snapshots/rescue.txt
@@ -291,26 +291,26 @@
         │   ├── body:
         │   │   @ StatementsNode (location: (26,18)-(26,44))
         │   │   └── body: (length: 1)
-        │   │       └── @ CallNode (location: (26,18)-(26,44))
-        │   │           ├── receiver: ∅
-        │   │           ├── call_operator_loc: ∅
-        │   │           ├── message_loc: (26,18)-(26,30) = "other_method"
-        │   │           ├── opening_loc: ∅
-        │   │           ├── arguments:
-        │   │           │   @ ArgumentsNode (location: (26,31)-(26,44))
-        │   │           │   ├── arguments: (length: 1)
-        │   │           │   │   └── @ RescueModifierNode (location: (26,31)-(26,44))
-        │   │           │   │       ├── expression:
-        │   │           │   │       │   @ IntegerNode (location: (26,31)-(26,33))
-        │   │           │   │       │   └── flags: decimal
-        │   │           │   │       ├── keyword_loc: (26,34)-(26,40) = "rescue"
-        │   │           │   │       └── rescue_expression:
-        │   │           │   │           @ NilNode (location: (26,41)-(26,44))
-        │   │           │   └── flags: ∅
-        │   │           ├── closing_loc: ∅
-        │   │           ├── block: ∅
-        │   │           ├── flags: ∅
-        │   │           └── name: :other_method
+        │   │       └── @ RescueModifierNode (location: (26,18)-(26,44))
+        │   │           ├── expression:
+        │   │           │   @ CallNode (location: (26,18)-(26,33))
+        │   │           │   ├── receiver: ∅
+        │   │           │   ├── call_operator_loc: ∅
+        │   │           │   ├── message_loc: (26,18)-(26,30) = "other_method"
+        │   │           │   ├── opening_loc: ∅
+        │   │           │   ├── arguments:
+        │   │           │   │   @ ArgumentsNode (location: (26,31)-(26,33))
+        │   │           │   │   ├── arguments: (length: 1)
+        │   │           │   │   │   └── @ IntegerNode (location: (26,31)-(26,33))
+        │   │           │   │   │       └── flags: decimal
+        │   │           │   │   └── flags: ∅
+        │   │           │   ├── closing_loc: ∅
+        │   │           │   ├── block: ∅
+        │   │           │   ├── flags: ∅
+        │   │           │   └── name: :other_method
+        │   │           ├── keyword_loc: (26,34)-(26,40) = "rescue"
+        │   │           └── rescue_expression:
+        │   │               @ NilNode (location: (26,41)-(26,44))
         │   ├── locals: []
         │   ├── def_keyword_loc: (26,0)-(26,3) = "def"
         │   ├── operator_loc: ∅
diff --git a/test/prism/snapshots/seattlerb/defn_endless_command_rescue.txt b/test/prism/snapshots/seattlerb/defn_endless_command_rescue.txt
index c8ebc98a..ace1227a 100644
--- a/test/prism/snapshots/seattlerb/defn_endless_command_rescue.txt
+++ b/test/prism/snapshots/seattlerb/defn_endless_command_rescue.txt
@@ -11,27 +11,27 @@
             ├── body:
             │   @ StatementsNode (location: (1,18)-(1,43))
             │   └── body: (length: 1)
-            │       └── @ CallNode (location: (1,18)-(1,43))
-            │           ├── receiver: ∅
-            │           ├── call_operator_loc: ∅
-            │           ├── message_loc: (1,18)-(1,30) = "other_method"
-            │           ├── opening_loc: ∅
-            │           ├── arguments:
-            │           │   @ ArgumentsNode (location: (1,31)-(1,43))
-            │           │   ├── arguments: (length: 1)
-            │           │   │   └── @ RescueModifierNode (location: (1,31)-(1,43))
-            │           │   │       ├── expression:
-            │           │   │       │   @ IntegerNode (location: (1,31)-(1,33))
-            │           │   │       │   └── flags: decimal
-            │           │   │       ├── keyword_loc: (1,34)-(1,40) = "rescue"
-            │           │   │       └── rescue_expression:
-            │           │   │           @ IntegerNode (location: (1,41)-(1,43))
-            │           │   │           └── flags: decimal
-            │           │   └── flags: ∅
-            │           ├── closing_loc: ∅
-            │           ├── block: ∅
-            │           ├── flags: ∅
-            │           └── name: :other_method
+            │       └── @ RescueModifierNode (location: (1,18)-(1,43))
+            │           ├── expression:
+            │           │   @ CallNode (location: (1,18)-(1,33))
+            │           │   ├── receiver: ∅
+            │           │   ├── call_operator_loc: ∅
+            │           │   ├── message_loc: (1,18)-(1,30) = "other_method"
+            │           │   ├── opening_loc: ∅
+            │           │   ├── arguments:
+            │           │   │   @ ArgumentsNode (location: (1,31)-(1,33))
+            │           │   │   ├── arguments: (length: 1)
+            │           │   │   │   └── @ IntegerNode (location: (1,31)-(1,33))
+            │           │   │   │       └── flags: decimal
+            │           │   │   └── flags: ∅
+            │           │   ├── closing_loc: ∅
+            │           │   ├── block: ∅
+            │           │   ├── flags: ∅
+            │           │   └── name: :other_method
+            │           ├── keyword_loc: (1,34)-(1,40) = "rescue"
+            │           └── rescue_expression:
+            │               @ IntegerNode (location: (1,41)-(1,43))
+            │               └── flags: decimal
             ├── locals: []
             ├── def_keyword_loc: (1,0)-(1,3) = "def"
             ├── operator_loc: ∅
diff --git a/test/prism/snapshots/seattlerb/defs_endless_command_rescue.txt b/test/prism/snapshots/seattlerb/defs_endless_command_rescue.txt
index 26995bb9..cfa1d120 100644
--- a/test/prism/snapshots/seattlerb/defs_endless_command_rescue.txt
+++ b/test/prism/snapshots/seattlerb/defs_endless_command_rescue.txt
@@ -21,27 +21,27 @@
             ├── body:
             │   @ StatementsNode (location: (1,20)-(1,45))
             │   └── body: (length: 1)
-            │       └── @ CallNode (location: (1,20)-(1,45))
-            │           ├── receiver: ∅
-            │           ├── call_operator_loc: ∅
-            │           ├── message_loc: (1,20)-(1,32) = "other_method"
-            │           ├── opening_loc: ∅
-            │           ├── arguments:
-            │           │   @ ArgumentsNode (location: (1,33)-(1,45))
-            │           │   ├── arguments: (length: 1)
-            │           │   │   └── @ RescueModifierNode (location: (1,33)-(1,45))
-            │           │   │       ├── expression:
-            │           │   │       │   @ IntegerNode (location: (1,33)-(1,35))
-            │           │   │       │   └── flags: decimal
-            │           │   │       ├── keyword_loc: (1,36)-(1,42) = "rescue"
-            │           │   │       └── rescue_expression:
-            │           │   │           @ IntegerNode (location: (1,43)-(1,45))
-            │           │   │           └── flags: decimal
-            │           │   └── flags: ∅
-            │           ├── closing_loc: ∅
-            │           ├── block: ∅
-            │           ├── flags: ∅
-            │           └── name: :other_method
+            │       └── @ RescueModifierNode (location: (1,20)-(1,45))
+            │           ├── expression:
+            │           │   @ CallNode (location: (1,20)-(1,35))
+            │           │   ├── receiver: ∅
+            │           │   ├── call_operator_loc: ∅
+            │           │   ├── message_loc: (1,20)-(1,32) = "other_method"
+            │           │   ├── opening_loc: ∅
+            │           │   ├── arguments:
+            │           │   │   @ ArgumentsNode (location: (1,33)-(1,35))
+            │           │   │   ├── arguments: (length: 1)
+            │           │   │   │   └── @ IntegerNode (location: (1,33)-(1,35))
+            │           │   │   │       └── flags: decimal
+            │           │   │   └── flags: ∅
+            │           │   ├── closing_loc: ∅
+            │           │   ├── block: ∅
+            │           │   ├── flags: ∅
+            │           │   └── name: :other_method
+            │           ├── keyword_loc: (1,36)-(1,42) = "rescue"
+            │           └── rescue_expression:
+            │               @ IntegerNode (location: (1,43)-(1,45))
+            │               └── flags: decimal
             ├── locals: []
             ├── def_keyword_loc: (1,0)-(1,3) = "def"
             ├── operator_loc: (1,5)-(1,6) = "."
diff --git a/test/prism/snapshots/seattlerb/lasgn_call_nobracket_rescue_arg.txt b/test/prism/snapshots/seattlerb/lasgn_call_nobracket_rescue_arg.txt
index 44e5abb2..a54b70da 100644
--- a/test/prism/snapshots/seattlerb/lasgn_call_nobracket_rescue_arg.txt
+++ b/test/prism/snapshots/seattlerb/lasgn_call_nobracket_rescue_arg.txt
@@ -8,25 +8,25 @@
             ├── depth: 0
             ├── name_loc: (1,0)-(1,1) = "a"
             ├── value:
-            │   @ CallNode (location: (1,4)-(1,16))
-            │   ├── receiver: ∅
-            │   ├── call_operator_loc: ∅
-            │   ├── message_loc: (1,4)-(1,5) = "b"
-            │   ├── opening_loc: ∅
-            │   ├── arguments:
-            │   │   @ ArgumentsNode (location: (1,6)-(1,16))
-            │   │   ├── arguments: (length: 1)
-            │   │   │   └── @ RescueModifierNode (location: (1,6)-(1,16))
-            │   │   │       ├── expression:
-            │   │   │       │   @ IntegerNode (location: (1,6)-(1,7))
-            │   │   │       │   └── flags: decimal
-            │   │   │       ├── keyword_loc: (1,8)-(1,14) = "rescue"
-            │   │   │       └── rescue_expression:
-            │   │   │           @ IntegerNode (location: (1,15)-(1,16))
-            │   │   │           └── flags: decimal
-            │   │   └── flags: ∅
-            │   ├── closing_loc: ∅
-            │   ├── block: ∅
-            │   ├── flags: ∅
-            │   └── name: :b
+            │   @ RescueModifierNode (location: (1,4)-(1,16))
+            │   ├── expression:
+            │   │   @ CallNode (location: (1,4)-(1,7))
+            │   │   ├── receiver: ∅
+            │   │   ├── call_operator_loc: ∅
+            │   │   ├── message_loc: (1,4)-(1,5) = "b"
+            │   │   ├── opening_loc: ∅
+            │   │   ├── arguments:
+            │   │   │   @ ArgumentsNode (location: (1,6)-(1,7))
+            │   │   │   ├── arguments: (length: 1)
+            │   │   │   │   └── @ IntegerNode (location: (1,6)-(1,7))
+            │   │   │   │       └── flags: decimal
+            │   │   │   └── flags: ∅
+            │   │   ├── closing_loc: ∅
+            │   │   ├── block: ∅
+            │   │   ├── flags: ∅
+            │   │   └── name: :b
+            │   ├── keyword_loc: (1,8)-(1,14) = "rescue"
+            │   └── rescue_expression:
+            │       @ IntegerNode (location: (1,15)-(1,16))
+            │       └── flags: decimal
             └── operator_loc: (1,2)-(1,3) = "="
diff --git a/test/prism/snapshots/whitequark/endless_method_command_syntax.txt b/test/prism/snapshots/whitequark/endless_method_command_syntax.txt
index e04bfc43..b63bfe81 100644
--- a/test/prism/snapshots/whitequark/endless_method_command_syntax.txt
+++ b/test/prism/snapshots/whitequark/endless_method_command_syntax.txt
@@ -269,48 +269,48 @@
         │   ├── body:
         │   │   @ StatementsNode (location: (13,17)-(13,60))
         │   │   └── body: (length: 1)
-        │   │       └── @ CallNode (location: (13,17)-(13,60))
-        │   │           ├── receiver: ∅
-        │   │           ├── call_operator_loc: ∅
-        │   │           ├── message_loc: (13,17)-(13,22) = "raise"
-        │   │           ├── opening_loc: ∅
-        │   │           ├── arguments:
-        │   │           │   @ ArgumentsNode (location: (13,23)-(13,60))
-        │   │           │   ├── arguments: (length: 1)
-        │   │           │   │   └── @ RescueModifierNode (location: (13,23)-(13,60))
-        │   │           │   │       ├── expression:
-        │   │           │   │       │   @ StringNode (location: (13,23)-(13,37))
-        │   │           │   │       │   ├── flags: ∅
-        │   │           │   │       │   ├── opening_loc: (13,23)-(13,24) = "\""
-        │   │           │   │       │   ├── content_loc: (13,24)-(13,36) = "to be caught"
-        │   │           │   │       │   ├── closing_loc: (13,36)-(13,37) = "\""
-        │   │           │   │       │   └── unescaped: "to be caught"
-        │   │           │   │       ├── keyword_loc: (13,38)-(13,44) = "rescue"
-        │   │           │   │       └── rescue_expression:
-        │   │           │   │           @ InterpolatedStringNode (location: (13,45)-(13,60))
-        │   │           │   │           ├── opening_loc: (13,45)-(13,46) = "\""
-        │   │           │   │           ├── parts: (length: 2)
-        │   │           │   │           │   ├── @ StringNode (location: (13,46)-(13,55))
-        │   │           │   │           │   │   ├── flags: ∅
-        │   │           │   │           │   │   ├── opening_loc: ∅
-        │   │           │   │           │   │   ├── content_loc: (13,46)-(13,55) = "instance "
-        │   │           │   │           │   │   ├── closing_loc: ∅
-        │   │           │   │           │   │   └── unescaped: "instance "
-        │   │           │   │           │   └── @ EmbeddedStatementsNode (location: (13,55)-(13,59))
-        │   │           │   │           │       ├── opening_loc: (13,55)-(13,57) = "\#{"
-        │   │           │   │           │       ├── statements:
-        │   │           │   │           │       │   @ StatementsNode (location: (13,57)-(13,58))
-        │   │           │   │           │       │   └── body: (length: 1)
-        │   │           │   │           │       │       └── @ LocalVariableReadNode (location: (13,57)-(13,58))
-        │   │           │   │           │       │           ├── name: :x
-        │   │           │   │           │       │           └── depth: 0
-        │   │           │   │           │       └── closing_loc: (13,58)-(13,59) = "}"
-        │   │           │   │           └── closing_loc: (13,59)-(13,60) = "\""
-        │   │           │   └── flags: ∅
-        │   │           ├── closing_loc: ∅
-        │   │           ├── block: ∅
-        │   │           ├── flags: ∅
-        │   │           └── name: :raise
+        │   │       └── @ RescueModifierNode (location: (13,17)-(13,60))
+        │   │           ├── expression:
+        │   │           │   @ CallNode (location: (13,17)-(13,37))
+        │   │           │   ├── receiver: ∅
+        │   │           │   ├── call_operator_loc: ∅
+        │   │           │   ├── message_loc: (13,17)-(13,22) = "raise"
+        │   │           │   ├── opening_loc: ∅
+        │   │           │   ├── arguments:
+        │   │           │   │   @ ArgumentsNode (location: (13,23)-(13,37))
+        │   │           │   │   ├── arguments: (length: 1)
+        │   │           │   │   │   └── @ StringNode (location: (13,23)-(13,37))
+        │   │           │   │   │       ├── flags: ∅
+        │   │           │   │   │       ├── opening_loc: (13,23)-(13,24) = "\""
+        │   │           │   │   │       ├── content_loc: (13,24)-(13,36) = "to be caught"
+        │   │           │   │   │       ├── closing_loc: (13,36)-(13,37) = "\""
+        │   │           │   │   │       └── unescaped: "to be caught"
+        │   │           │   │   └── flags: ∅
+        │   │           │   ├── closing_loc: ∅
+        │   │           │   ├── block: ∅
+        │   │           │   ├── flags: ∅
+        │   │           │   └── name: :raise
+        │   │           ├── keyword_loc: (13,38)-(13,44) = "rescue"
+        │   │           └── rescue_expression:
+        │   │               @ InterpolatedStringNode (location: (13,45)-(13,60))
+        │   │               ├── opening_loc: (13,45)-(13,46) = "\""
+        │   │               ├── parts: (length: 2)
+        │   │               │   ├── @ StringNode (location: (13,46)-(13,55))
+        │   │               │   │   ├── flags: ∅
+        │   │               │   │   ├── opening_loc: ∅
+        │   │               │   │   ├── content_loc: (13,46)-(13,55) = "instance "
+        │   │               │   │   ├── closing_loc: ∅
+        │   │               │   │   └── unescaped: "instance "
+        │   │               │   └── @ EmbeddedStatementsNode (location: (13,55)-(13,59))
+        │   │               │       ├── opening_loc: (13,55)-(13,57) = "\#{"
+        │   │               │       ├── statements:
+        │   │               │       │   @ StatementsNode (location: (13,57)-(13,58))
+        │   │               │       │   └── body: (length: 1)
+        │   │               │       │       └── @ LocalVariableReadNode (location: (13,57)-(13,58))
+        │   │               │       │           ├── name: :x
+        │   │               │       │           └── depth: 0
+        │   │               │       └── closing_loc: (13,58)-(13,59) = "}"
+        │   │               └── closing_loc: (13,59)-(13,60) = "\""
         │   ├── locals: [:x]
         │   ├── def_keyword_loc: (13,0)-(13,3) = "def"
         │   ├── operator_loc: ∅
@@ -337,48 +337,48 @@
             ├── body:
             │   @ StatementsNode (location: (15,22)-(15,62))
             │   └── body: (length: 1)
-            │       └── @ CallNode (location: (15,22)-(15,62))
-            │           ├── receiver: ∅
-            │           ├── call_operator_loc: ∅
-            │           ├── message_loc: (15,22)-(15,27) = "raise"
-            │           ├── opening_loc: ∅
-            │           ├── arguments:
-            │           │   @ ArgumentsNode (location: (15,28)-(15,62))
-            │           │   ├── arguments: (length: 1)
-            │           │   │   └── @ RescueModifierNode (location: (15,28)-(15,62))
-            │           │   │       ├── expression:
-            │           │   │       │   @ StringNode (location: (15,28)-(15,42))
-            │           │   │       │   ├── flags: ∅
-            │           │   │       │   ├── opening_loc: (15,28)-(15,29) = "\""
-            │           │   │       │   ├── content_loc: (15,29)-(15,41) = "to be caught"
-            │           │   │       │   ├── closing_loc: (15,41)-(15,42) = "\""
-            │           │   │       │   └── unescaped: "to be caught"
-            │           │   │       ├── keyword_loc: (15,43)-(15,49) = "rescue"
-            │           │   │       └── rescue_expression:
-            │           │   │           @ InterpolatedStringNode (location: (15,50)-(15,62))
-            │           │   │           ├── opening_loc: (15,50)-(15,51) = "\""
-            │           │   │           ├── parts: (length: 2)
-            │           │   │           │   ├── @ StringNode (location: (15,51)-(15,57))
-            │           │   │           │   │   ├── flags: ∅
-            │           │   │           │   │   ├── opening_loc: ∅
-            │           │   │           │   │   ├── content_loc: (15,51)-(15,57) = "class "
-            │           │   │           │   │   ├── closing_loc: ∅
-            │           │   │           │   │   └── unescaped: "class "
-            │           │   │           │   └── @ EmbeddedStatementsNode (location: (15,57)-(15,61))
-            │           │   │           │       ├── opening_loc: (15,57)-(15,59) = "\#{"
-            │           │   │           │       ├── statements:
-            │           │   │           │       │   @ StatementsNode (location: (15,59)-(15,60))
-            │           │   │           │       │   └── body: (length: 1)
-            │           │   │           │       │       └── @ LocalVariableReadNode (location: (15,59)-(15,60))
-            │           │   │           │       │           ├── name: :x
-            │           │   │           │       │           └── depth: 0
-            │           │   │           │       └── closing_loc: (15,60)-(15,61) = "}"
-            │           │   │           └── closing_loc: (15,61)-(15,62) = "\""
-            │           │   └── flags: ∅
-            │           ├── closing_loc: ∅
-            │           ├── block: ∅
-            │           ├── flags: ∅
-            │           └── name: :raise
+            │       └── @ RescueModifierNode (location: (15,22)-(15,62))
+            │           ├── expression:
+            │           │   @ CallNode (location: (15,22)-(15,42))
+            │           │   ├── receiver: ∅
+            │           │   ├── call_operator_loc: ∅
+            │           │   ├── message_loc: (15,22)-(15,27) = "raise"
+            │           │   ├── opening_loc: ∅
+            │           │   ├── arguments:
+            │           │   │   @ ArgumentsNode (location: (15,28)-(15,42))
+            │           │   │   ├── arguments: (length: 1)
+            │           │   │   │   └── @ StringNode (location: (15,28)-(15,42))
+            │           │   │   │       ├── flags: ∅
+            │           │   │   │       ├── opening_loc: (15,28)-(15,29) = "\""
+            │           │   │   │       ├── content_loc: (15,29)-(15,41) = "to be caught"
+            │           │   │   │       ├── closing_loc: (15,41)-(15,42) = "\""
+            │           │   │   │       └── unescaped: "to be caught"
+            │           │   │   └── flags: ∅
+            │           │   ├── closing_loc: ∅
+            │           │   ├── block: ∅
+            │           │   ├── flags: ∅
+            │           │   └── name: :raise
+            │           ├── keyword_loc: (15,43)-(15,49) = "rescue"
+            │           └── rescue_expression:
+            │               @ InterpolatedStringNode (location: (15,50)-(15,62))
+            │               ├── opening_loc: (15,50)-(15,51) = "\""
+            │               ├── parts: (length: 2)
+            │               │   ├── @ StringNode (location: (15,51)-(15,57))
+            │               │   │   ├── flags: ∅
+            │               │   │   ├── opening_loc: ∅
+            │               │   │   ├── content_loc: (15,51)-(15,57) = "class "
+            │               │   │   ├── closing_loc: ∅
+            │               │   │   └── unescaped: "class "
+            │               │   └── @ EmbeddedStatementsNode (location: (15,57)-(15,61))
+            │               │       ├── opening_loc: (15,57)-(15,59) = "\#{"
+            │               │       ├── statements:
+            │               │       │   @ StatementsNode (location: (15,59)-(15,60))
+            │               │       │   └── body: (length: 1)
+            │               │       │       └── @ LocalVariableReadNode (location: (15,59)-(15,60))
+            │               │       │           ├── name: :x
+            │               │       │           └── depth: 0
+            │               │       └── closing_loc: (15,60)-(15,61) = "}"
+            │               └── closing_loc: (15,61)-(15,62) = "\""
             ├── locals: [:x]
             ├── def_keyword_loc: (15,0)-(15,3) = "def"
             ├── operator_loc: (15,8)-(15,9) = "."
diff --git a/test/prism/snapshots/whitequark/ruby_bug_12402.txt b/test/prism/snapshots/whitequark/ruby_bug_12402.txt
index 258f7b68..78a33693 100644
--- a/test/prism/snapshots/whitequark/ruby_bug_12402.txt
+++ b/test/prism/snapshots/whitequark/ruby_bug_12402.txt
@@ -7,34 +7,34 @@
         │   ├── name_loc: (1,0)-(1,3) = "foo"
         │   ├── operator_loc: (1,4)-(1,6) = "+="
         │   ├── value:
-        │   │   @ CallNode (location: (1,7)-(1,27))
-        │   │   ├── receiver: ∅
-        │   │   ├── call_operator_loc: ∅
-        │   │   ├── message_loc: (1,7)-(1,12) = "raise"
-        │   │   ├── opening_loc: ∅
-        │   │   ├── arguments:
-        │   │   │   @ ArgumentsNode (location: (1,13)-(1,27))
-        │   │   │   ├── arguments: (length: 1)
-        │   │   │   │   └── @ RescueModifierNode (location: (1,13)-(1,27))
-        │   │   │   │       ├── expression:
-        │   │   │   │       │   @ CallNode (location: (1,13)-(1,16))
-        │   │   │   │       │   ├── receiver: ∅
-        │   │   │   │       │   ├── call_operator_loc: ∅
-        │   │   │   │       │   ├── message_loc: (1,13)-(1,16) = "bar"
-        │   │   │   │       │   ├── opening_loc: ∅
-        │   │   │   │       │   ├── arguments: ∅
-        │   │   │   │       │   ├── closing_loc: ∅
-        │   │   │   │       │   ├── block: ∅
-        │   │   │   │       │   ├── flags: variable_call
-        │   │   │   │       │   └── name: :bar
-        │   │   │   │       ├── keyword_loc: (1,17)-(1,23) = "rescue"
-        │   │   │   │       └── rescue_expression:
-        │   │   │   │           @ NilNode (location: (1,24)-(1,27))
-        │   │   │   └── flags: ∅
-        │   │   ├── closing_loc: ∅
-        │   │   ├── block: ∅
-        │   │   ├── flags: ∅
-        │   │   └── name: :raise
+        │   │   @ RescueModifierNode (location: (1,7)-(1,27))
+        │   │   ├── expression:
+        │   │   │   @ CallNode (location: (1,7)-(1,16))
+        │   │   │   ├── receiver: ∅
+        │   │   │   ├── call_operator_loc: ∅
+        │   │   │   ├── message_loc: (1,7)-(1,12) = "raise"
+        │   │   │   ├── opening_loc: ∅
+        │   │   │   ├── arguments:
+        │   │   │   │   @ ArgumentsNode (location: (1,13)-(1,16))
+        │   │   │   │   ├── arguments: (length: 1)
+        │   │   │   │   │   └── @ CallNode (location: (1,13)-(1,16))
+        │   │   │   │   │       ├── receiver: ∅
+        │   │   │   │   │       ├── call_operator_loc: ∅
+        │   │   │   │   │       ├── message_loc: (1,13)-(1,16) = "bar"
+        │   │   │   │   │       ├── opening_loc: ∅
+        │   │   │   │   │       ├── arguments: ∅
+        │   │   │   │   │       ├── closing_loc: ∅
+        │   │   │   │   │       ├── block: ∅
+        │   │   │   │   │       ├── flags: variable_call
+        │   │   │   │   │       └── name: :bar
+        │   │   │   │   └── flags: ∅
+        │   │   │   ├── closing_loc: ∅
+        │   │   │   ├── block: ∅
+        │   │   │   ├── flags: ∅
+        │   │   │   └── name: :raise
+        │   │   ├── keyword_loc: (1,17)-(1,23) = "rescue"
+        │   │   └── rescue_expression:
+        │   │       @ NilNode (location: (1,24)-(1,27))
         │   ├── name: :foo
         │   ├── operator: :+
         │   └── depth: 0
@@ -78,34 +78,34 @@
         │   ├── depth: 0
         │   ├── name_loc: (5,0)-(5,3) = "foo"
         │   ├── value:
-        │   │   @ CallNode (location: (5,6)-(5,26))
-        │   │   ├── receiver: ∅
-        │   │   ├── call_operator_loc: ∅
-        │   │   ├── message_loc: (5,6)-(5,11) = "raise"
-        │   │   ├── opening_loc: ∅
-        │   │   ├── arguments:
-        │   │   │   @ ArgumentsNode (location: (5,12)-(5,26))
-        │   │   │   ├── arguments: (length: 1)
-        │   │   │   │   └── @ RescueModifierNode (location: (5,12)-(5,26))
-        │   │   │   │       ├── expression:
-        │   │   │   │       │   @ CallNode (location: (5,12)-(5,15))
-        │   │   │   │       │   ├── receiver: ∅
-        │   │   │   │       │   ├── call_operator_loc: ∅
-        │   │   │   │       │   ├── message_loc: (5,12)-(5,15) = "bar"
-        │   │   │   │       │   ├── opening_loc: ∅
-        │   │   │   │       │   ├── arguments: ∅
-        │   │   │   │       │   ├── closing_loc: ∅
-        │   │   │   │       │   ├── block: ∅
-        │   │   │   │       │   ├── flags: variable_call
-        │   │   │   │       │   └── name: :bar
-        │   │   │   │       ├── keyword_loc: (5,16)-(5,22) = "rescue"
-        │   │   │   │       └── rescue_expression:
-        │   │   │   │           @ NilNode (location: (5,23)-(5,26))
-        │   │   │   └── flags: ∅
-        │   │   ├── closing_loc: ∅
-        │   │   ├── block: ∅
-        │   │   ├── flags: ∅
-        │   │   └── name: :raise
+        │   │   @ RescueModifierNode (location: (5,6)-(5,26))
+        │   │   ├── expression:
+        │   │   │   @ CallNode (location: (5,6)-(5,15))
+        │   │   │   ├── receiver: ∅
+        │   │   │   ├── call_operator_loc: ∅
+        │   │   │   ├── message_loc: (5,6)-(5,11) = "raise"
+        │   │   │   ├── opening_loc: ∅
+        │   │   │   ├── arguments:
+        │   │   │   │   @ ArgumentsNode (location: (5,12)-(5,15))
+        │   │   │   │   ├── arguments: (length: 1)
+        │   │   │   │   │   └── @ CallNode (location: (5,12)-(5,15))
+        │   │   │   │   │       ├── receiver: ∅
+        │   │   │   │   │       ├── call_operator_loc: ∅
+        │   │   │   │   │       ├── message_loc: (5,12)-(5,15) = "bar"
+        │   │   │   │   │       ├── opening_loc: ∅
+        │   │   │   │   │       ├── arguments: ∅
+        │   │   │   │   │       ├── closing_loc: ∅
+        │   │   │   │   │       ├── block: ∅
+        │   │   │   │   │       ├── flags: variable_call
+        │   │   │   │   │       └── name: :bar
+        │   │   │   │   └── flags: ∅
+        │   │   │   ├── closing_loc: ∅
+        │   │   │   ├── block: ∅
+        │   │   │   ├── flags: ∅
+        │   │   │   └── name: :raise
+        │   │   ├── keyword_loc: (5,16)-(5,22) = "rescue"
+        │   │   └── rescue_expression:
+        │   │       @ NilNode (location: (5,23)-(5,26))
         │   └── operator_loc: (5,4)-(5,5) = "="
         ├── @ LocalVariableWriteNode (location: (7,0)-(7,27))
         │   ├── name: :foo
@@ -154,34 +154,34 @@
         │   ├── operator: :+
         │   ├── operator_loc: (9,6)-(9,8) = "+="
         │   └── value:
-        │       @ CallNode (location: (9,9)-(9,29))
-        │       ├── receiver: ∅
-        │       ├── call_operator_loc: ∅
-        │       ├── message_loc: (9,9)-(9,14) = "raise"
-        │       ├── opening_loc: ∅
-        │       ├── arguments:
-        │       │   @ ArgumentsNode (location: (9,15)-(9,29))
-        │       │   ├── arguments: (length: 1)
-        │       │   │   └── @ RescueModifierNode (location: (9,15)-(9,29))
-        │       │   │       ├── expression:
-        │       │   │       │   @ CallNode (location: (9,15)-(9,18))
-        │       │   │       │   ├── receiver: ∅
-        │       │   │       │   ├── call_operator_loc: ∅
-        │       │   │       │   ├── message_loc: (9,15)-(9,18) = "bar"
-        │       │   │       │   ├── opening_loc: ∅
-        │       │   │       │   ├── arguments: ∅
-        │       │   │       │   ├── closing_loc: ∅
-        │       │   │       │   ├── block: ∅
-        │       │   │       │   ├── flags: variable_call
-        │       │   │       │   └── name: :bar
-        │       │   │       ├── keyword_loc: (9,19)-(9,25) = "rescue"
-        │       │   │       └── rescue_expression:
-        │       │   │           @ NilNode (location: (9,26)-(9,29))
-        │       │   └── flags: ∅
-        │       ├── closing_loc: ∅
-        │       ├── block: ∅
-        │       ├── flags: ∅
-        │       └── name: :raise
+        │       @ RescueModifierNode (location: (9,9)-(9,29))
+        │       ├── expression:
+        │       │   @ CallNode (location: (9,9)-(9,18))
+        │       │   ├── receiver: ∅
+        │       │   ├── call_operator_loc: ∅
+        │       │   ├── message_loc: (9,9)-(9,14) = "raise"
+        │       │   ├── opening_loc: ∅
+        │       │   ├── arguments:
+        │       │   │   @ ArgumentsNode (location: (9,15)-(9,18))
+        │       │   │   ├── arguments: (length: 1)
+        │       │   │   │   └── @ CallNode (location: (9,15)-(9,18))
+        │       │   │   │       ├── receiver: ∅
+        │       │   │   │       ├── call_operator_loc: ∅
+        │       │   │   │       ├── message_loc: (9,15)-(9,18) = "bar"
+        │       │   │   │       ├── opening_loc: ∅
+        │       │   │   │       ├── arguments: ∅
+        │       │   │   │       ├── closing_loc: ∅
+        │       │   │   │       ├── block: ∅
+        │       │   │   │       ├── flags: variable_call
+        │       │   │   │       └── name: :bar
+        │       │   │   └── flags: ∅
+        │       │   ├── closing_loc: ∅
+        │       │   ├── block: ∅
+        │       │   ├── flags: ∅
+        │       │   └── name: :raise
+        │       ├── keyword_loc: (9,19)-(9,25) = "rescue"
+        │       └── rescue_expression:
+        │           @ NilNode (location: (9,26)-(9,29))
         ├── @ CallOperatorWriteNode (location: (11,0)-(11,30))
         │   ├── receiver:
         │   │   @ LocalVariableReadNode (location: (11,0)-(11,3))
@@ -236,34 +236,34 @@
         │   ├── operator: :+
         │   ├── operator_loc: (13,6)-(13,8) = "+="
         │   └── value:
-        │       @ CallNode (location: (13,9)-(13,29))
-        │       ├── receiver: ∅
-        │       ├── call_operator_loc: ∅
-        │       ├── message_loc: (13,9)-(13,14) = "raise"
-        │       ├── opening_loc: ∅
-        │       ├── arguments:
-        │       │   @ ArgumentsNode (location: (13,15)-(13,29))
-        │       │   ├── arguments: (length: 1)
-        │       │   │   └── @ RescueModifierNode (location: (13,15)-(13,29))
-        │       │   │       ├── expression:
-        │       │   │       │   @ CallNode (location: (13,15)-(13,18))
-        │       │   │       │   ├── receiver: ∅
-        │       │   │       │   ├── call_operator_loc: ∅
-        │       │   │       │   ├── message_loc: (13,15)-(13,18) = "bar"
-        │       │   │       │   ├── opening_loc: ∅
-        │       │   │       │   ├── arguments: ∅
-        │       │   │       │   ├── closing_loc: ∅
-        │       │   │       │   ├── block: ∅
-        │       │   │       │   ├── flags: variable_call
-        │       │   │       │   └── name: :bar
-        │       │   │       ├── keyword_loc: (13,19)-(13,25) = "rescue"
-        │       │   │       └── rescue_expression:
-        │       │   │           @ NilNode (location: (13,26)-(13,29))
-        │       │   └── flags: ∅
-        │       ├── closing_loc: ∅
-        │       ├── block: ∅
-        │       ├── flags: ∅
-        │       └── name: :raise
+        │       @ RescueModifierNode (location: (13,9)-(13,29))
+        │       ├── expression:
+        │       │   @ CallNode (location: (13,9)-(13,18))
+        │       │   ├── receiver: ∅
+        │       │   ├── call_operator_loc: ∅
+        │       │   ├── message_loc: (13,9)-(13,14) = "raise"
+        │       │   ├── opening_loc: ∅
+        │       │   ├── arguments:
+        │       │   │   @ ArgumentsNode (location: (13,15)-(13,18))
+        │       │   │   ├── arguments: (length: 1)
+        │       │   │   │   └── @ CallNode (location: (13,15)-(13,18))
+        │       │   │   │       ├── receiver: ∅
+        │       │   │   │       ├── call_operator_loc: ∅
+        │       │   │   │       ├── message_loc: (13,15)-(13,18) = "bar"
+        │       │   │   │       ├── opening_loc: ∅
+        │       │   │   │       ├── arguments: ∅
+        │       │   │   │       ├── closing_loc: ∅
+        │       │   │   │       ├── block: ∅
+        │       │   │   │       ├── flags: variable_call
+        │       │   │   │       └── name: :bar
+        │       │   │   └── flags: ∅
+        │       │   ├── closing_loc: ∅
+        │       │   ├── block: ∅
+        │       │   ├── flags: ∅
+        │       │   └── name: :raise
+        │       ├── keyword_loc: (13,19)-(13,25) = "rescue"
+        │       └── rescue_expression:
+        │           @ NilNode (location: (13,26)-(13,29))
         ├── @ CallOperatorWriteNode (location: (15,0)-(15,30))
         │   ├── receiver:
         │   │   @ LocalVariableReadNode (location: (15,0)-(15,3))
@@ -318,34 +318,34 @@
         │   │   └── delimiter_loc: (17,3)-(17,5) = "::"
         │   ├── operator_loc: (17,7)-(17,10) = "||="
         │   └── value:
-        │       @ CallNode (location: (17,11)-(17,31))
-        │       ├── receiver: ∅
-        │       ├── call_operator_loc: ∅
-        │       ├── message_loc: (17,11)-(17,16) = "raise"
-        │       ├── opening_loc: ∅
-        │       ├── arguments:
-        │       │   @ ArgumentsNode (location: (17,17)-(17,31))
-        │       │   ├── arguments: (length: 1)
-        │       │   │   └── @ RescueModifierNode (location: (17,17)-(17,31))
-        │       │   │       ├── expression:
-        │       │   │       │   @ CallNode (location: (17,17)-(17,20))
-        │       │   │       │   ├── receiver: ∅
-        │       │   │       │   ├── call_operator_loc: ∅
-        │       │   │       │   ├── message_loc: (17,17)-(17,20) = "bar"
-        │       │   │       │   ├── opening_loc: ∅
-        │       │   │       │   ├── arguments: ∅
-        │       │   │       │   ├── closing_loc: ∅
-        │       │   │       │   ├── block: ∅
-        │       │   │       │   ├── flags: variable_call
-        │       │   │       │   └── name: :bar
-        │       │   │       ├── keyword_loc: (17,21)-(17,27) = "rescue"
-        │       │   │       └── rescue_expression:
-        │       │   │           @ NilNode (location: (17,28)-(17,31))
-        │       │   └── flags: ∅
-        │       ├── closing_loc: ∅
-        │       ├── block: ∅
-        │       ├── flags: ∅
-        │       └── name: :raise
+        │       @ RescueModifierNode (location: (17,11)-(17,31))
+        │       ├── expression:
+        │       │   @ CallNode (location: (17,11)-(17,20))
+        │       │   ├── receiver: ∅
+        │       │   ├── call_operator_loc: ∅
+        │       │   ├── message_loc: (17,11)-(17,16) = "raise"
+        │       │   ├── opening_loc: ∅
+        │       │   ├── arguments:
+        │       │   │   @ ArgumentsNode (location: (17,17)-(17,20))
+        │       │   │   ├── arguments: (length: 1)
+        │       │   │   │   └── @ CallNode (location: (17,17)-(17,20))
+        │       │   │   │       ├── receiver: ∅
+        │       │   │   │       ├── call_operator_loc: ∅
+        │       │   │   │       ├── message_loc: (17,17)-(17,20) = "bar"
+        │       │   │   │       ├── opening_loc: ∅
+        │       │   │   │       ├── arguments: ∅
+        │       │   │   │       ├── closing_loc: ∅
+        │       │   │   │       ├── block: ∅
+        │       │   │   │       ├── flags: variable_call
+        │       │   │   │       └── name: :bar
+        │       │   │   └── flags: ∅
+        │       │   ├── closing_loc: ∅
+        │       │   ├── block: ∅
+        │       │   ├── flags: ∅
+        │       │   └── name: :raise
+        │       ├── keyword_loc: (17,21)-(17,27) = "rescue"
+        │       └── rescue_expression:
+        │           @ NilNode (location: (17,28)-(17,31))
         ├── @ ConstantPathOrWriteNode (location: (19,0)-(19,32))
         │   ├── target:
         │   │   @ ConstantPathNode (location: (19,0)-(19,6))
@@ -400,34 +400,34 @@
         │   ├── operator: :+
         │   ├── operator_loc: (21,7)-(21,9) = "+="
         │   └── value:
-        │       @ CallNode (location: (21,10)-(21,30))
-        │       ├── receiver: ∅
-        │       ├── call_operator_loc: ∅
-        │       ├── message_loc: (21,10)-(21,15) = "raise"
-        │       ├── opening_loc: ∅
-        │       ├── arguments:
-        │       │   @ ArgumentsNode (location: (21,16)-(21,30))
-        │       │   ├── arguments: (length: 1)
-        │       │   │   └── @ RescueModifierNode (location: (21,16)-(21,30))
-        │       │   │       ├── expression:
-        │       │   │       │   @ CallNode (location: (21,16)-(21,19))
-        │       │   │       │   ├── receiver: ∅
-        │       │   │       │   ├── call_operator_loc: ∅
-        │       │   │       │   ├── message_loc: (21,16)-(21,19) = "bar"
-        │       │   │       │   ├── opening_loc: ∅
-        │       │   │       │   ├── arguments: ∅
-        │       │   │       │   ├── closing_loc: ∅
-        │       │   │       │   ├── block: ∅
-        │       │   │       │   ├── flags: variable_call
-        │       │   │       │   └── name: :bar
-        │       │   │       ├── keyword_loc: (21,20)-(21,26) = "rescue"
-        │       │   │       └── rescue_expression:
-        │       │   │           @ NilNode (location: (21,27)-(21,30))
-        │       │   └── flags: ∅
-        │       ├── closing_loc: ∅
-        │       ├── block: ∅
-        │       ├── flags: ∅
-        │       └── name: :raise
+        │       @ RescueModifierNode (location: (21,10)-(21,30))
+        │       ├── expression:
+        │       │   @ CallNode (location: (21,10)-(21,19))
+        │       │   ├── receiver: ∅
+        │       │   ├── call_operator_loc: ∅
+        │       │   ├── message_loc: (21,10)-(21,15) = "raise"
+        │       │   ├── opening_loc: ∅
+        │       │   ├── arguments:
+        │       │   │   @ ArgumentsNode (location: (21,16)-(21,19))
+        │       │   │   ├── arguments: (length: 1)
+        │       │   │   │   └── @ CallNode (location: (21,16)-(21,19))
+        │       │   │   │       ├── receiver: ∅
+        │       │   │   │       ├── call_operator_loc: ∅
+        │       │   │   │       ├── message_loc: (21,16)-(21,19) = "bar"
+        │       │   │   │       ├── opening_loc: ∅
+        │       │   │   │       ├── arguments: ∅
+        │       │   │   │       ├── closing_loc: ∅
+        │       │   │   │       ├── block: ∅
+        │       │   │   │       ├── flags: variable_call
+        │       │   │   │       └── name: :bar
+        │       │   │   └── flags: ∅
+        │       │   ├── closing_loc: ∅
+        │       │   ├── block: ∅
+        │       │   ├── flags: ∅
+        │       │   └── name: :raise
+        │       ├── keyword_loc: (21,20)-(21,26) = "rescue"
+        │       └── rescue_expression:
+        │           @ NilNode (location: (21,27)-(21,30))
         ├── @ CallOperatorWriteNode (location: (23,0)-(23,31))
         │   ├── receiver:
         │   │   @ LocalVariableReadNode (location: (23,0)-(23,3))
@@ -488,34 +488,34 @@
         │   ├── operator: :+
         │   ├── operator_loc: (25,7)-(25,9) = "+="
         │   └── value:
-        │       @ CallNode (location: (25,10)-(25,30))
-        │       ├── receiver: ∅
-        │       ├── call_operator_loc: ∅
-        │       ├── message_loc: (25,10)-(25,15) = "raise"
-        │       ├── opening_loc: ∅
-        │       ├── arguments:
-        │       │   @ ArgumentsNode (location: (25,16)-(25,30))
-        │       │   ├── arguments: (length: 1)
-        │       │   │   └── @ RescueModifierNode (location: (25,16)-(25,30))
-        │       │   │       ├── expression:
-        │       │   │       │   @ CallNode (location: (25,16)-(25,19))
-        │       │   │       │   ├── receiver: ∅
-        │       │   │       │   ├── call_operator_loc: ∅
-        │       │   │       │   ├── message_loc: (25,16)-(25,19) = "bar"
-        │       │   │       │   ├── opening_loc: ∅
-        │       │   │       │   ├── arguments: ∅
-        │       │   │       │   ├── closing_loc: ∅
-        │       │   │       │   ├── block: ∅
-        │       │   │       │   ├── flags: variable_call
-        │       │   │       │   └── name: :bar
-        │       │   │       ├── keyword_loc: (25,20)-(25,26) = "rescue"
-        │       │   │       └── rescue_expression:
-        │       │   │           @ NilNode (location: (25,27)-(25,30))
-        │       │   └── flags: ∅
-        │       ├── closing_loc: ∅
-        │       ├── block: ∅
-        │       ├── flags: ∅
-        │       └── name: :raise
+        │       @ RescueModifierNode (location: (25,10)-(25,30))
+        │       ├── expression:
+        │       │   @ CallNode (location: (25,10)-(25,19))
+        │       │   ├── receiver: ∅
+        │       │   ├── call_operator_loc: ∅
+        │       │   ├── message_loc: (25,10)-(25,15) = "raise"
+        │       │   ├── opening_loc: ∅
+        │       │   ├── arguments:
+        │       │   │   @ ArgumentsNode (location: (25,16)-(25,19))
+        │       │   │   ├── arguments: (length: 1)
+        │       │   │   │   └── @ CallNode (location: (25,16)-(25,19))
+        │       │   │   │       ├── receiver: ∅
+        │       │   │   │       ├── call_operator_loc: ∅
+        │       │   │   │       ├── message_loc: (25,16)-(25,19) = "bar"
+        │       │   │   │       ├── opening_loc: ∅
+        │       │   │   │       ├── arguments: ∅
+        │       │   │   │       ├── closing_loc: ∅
+        │       │   │   │       ├── block: ∅
+        │       │   │   │       ├── flags: variable_call
+        │       │   │   │       └── name: :bar
+        │       │   │   └── flags: ∅
+        │       │   ├── closing_loc: ∅
+        │       │   ├── block: ∅
+        │       │   ├── flags: ∅
+        │       │   └── name: :raise
+        │       ├── keyword_loc: (25,20)-(25,26) = "rescue"
+        │       └── rescue_expression:
+        │           @ NilNode (location: (25,27)-(25,30))
         └── @ IndexOperatorWriteNode (location: (27,0)-(27,31))
             ├── receiver:
             │   @ LocalVariableReadNode (location: (27,0)-(27,3))