Repository: ruby/prism
Pull request #2796 (2024-05-08 19:59:22)

Reconfigure rationals
This eliminates the subnode on RationalNode and replaces it with two integer fields, which represent the ratio for the rational. It also reduces those two integers if they both fit into 32 bits.
    
Importantly, this PR does not implement bignum reduction. That's something I'd like to consider for the future, but it's simple enough for now to leave them unreduced, which makes it more useful than it used to be.

Fixes https://github.com/ruby/prism/pull/2796.


Patch:
diff --git a/config.yml b/config.yml
index cfcfd2e7..6e0ce1ce 100644
--- a/config.yml
+++ b/config.yml
@@ -114,7 +114,6 @@ errors:
   - EXPRESSION_NOT_WRITABLE_FILE
   - EXPRESSION_NOT_WRITABLE_LINE
   - EXPRESSION_NOT_WRITABLE_NIL
-  - EXPRESSION_NOT_WRITABLE_NUMBERED
   - EXPRESSION_NOT_WRITABLE_SELF
   - EXPRESSION_NOT_WRITABLE_TRUE
   - FLOAT_PARSE
@@ -186,10 +185,9 @@ errors:
   - NO_LOCAL_VARIABLE
   - NOT_EXPRESSION
   - NUMBER_LITERAL_UNDERSCORE
-  - NUMBERED_PARAMETER_INNER_BLOCK
   - NUMBERED_PARAMETER_IT
   - NUMBERED_PARAMETER_ORDINARY
-  - NUMBERED_PARAMETER_OUTER_BLOCK
+  - NUMBERED_PARAMETER_OUTER_SCOPE
   - OPERATOR_MULTI_ASSIGN
   - OPERATOR_WRITE_ARGUMENTS
   - OPERATOR_WRITE_BLOCK
@@ -3231,8 +3229,21 @@ nodes:
                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   - name: RationalNode
     fields:
-      - name: numeric
-        type: node
+      - name: flags
+        type: flags
+        kind: IntegerBaseFlags
+      - name: numerator
+        type: integer
+        comment: |
+          The numerator of the rational number.
+
+              1.5r # numerator 3
+      - name: denominator
+        type: integer
+        comment: |
+          The denominator of the rational number.
+
+              1.5r # denominator 2
     comment: |
       Represents a rational number literal.
 
diff --git a/include/prism/parser.h b/include/prism/parser.h
index ff126184..8054e332 100644
--- a/include/prism/parser.h
+++ b/include/prism/parser.h
@@ -609,7 +609,6 @@ static const uint8_t PM_SCOPE_PARAMETERS_FORWARDING_KEYWORDS = 0x10;
 static const uint8_t PM_SCOPE_PARAMETERS_FORWARDING_BLOCK = 0x20;
 static const uint8_t PM_SCOPE_PARAMETERS_FORWARDING_ALL = 0x40;
 
-static const int8_t PM_SCOPE_NUMBERED_PARAMETERS_INNER = -2;
 static const int8_t PM_SCOPE_NUMBERED_PARAMETERS_DISALLOWED = -1;
 static const int8_t PM_SCOPE_NUMBERED_PARAMETERS_NONE = 0;
 
diff --git a/include/prism/util/pm_integer.h b/include/prism/util/pm_integer.h
index 7f172988..d2a1808b 100644
--- a/include/prism/util/pm_integer.h
+++ b/include/prism/util/pm_integer.h
@@ -48,6 +48,9 @@ typedef struct {
  * from the string itself.
  */
 typedef enum {
+    /** The default decimal base, with no prefix. Leading 0s will be ignored. */
+    PM_INTEGER_BASE_DEFAULT,
+
     /** The binary base, indicated by a 0b or 0B prefix. */
     PM_INTEGER_BASE_BINARY,
 
@@ -100,6 +103,18 @@ size_t pm_integer_memsize(const pm_integer_t *integer);
  */
 int pm_integer_compare(const pm_integer_t *left, const pm_integer_t *right);
 
+/**
+ * Reduce a ratio of integers to its simplest form.
+ *
+ * If either the numerator or denominator do not fit into a 32-bit integer, then
+ * this function is a no-op. In the future, we may consider reducing even the
+ * larger numbers, but for now we're going to keep it simple.
+ *
+ * @param numerator The numerator of the ratio.
+ * @param denominator The denominator of the ratio.
+ */
+void pm_integers_reduce(pm_integer_t *numerator, pm_integer_t *denominator);
+
 /**
  * Convert an integer to a decimal string.
  *
diff --git a/lib/prism/node_ext.rb b/lib/prism/node_ext.rb
index ceec76b8..cdc4b2bf 100644
--- a/lib/prism/node_ext.rb
+++ b/lib/prism/node_ext.rb
@@ -103,7 +103,19 @@ module Prism
   class RationalNode < Node
     # Returns the value of the node as a Ruby Rational.
     def value
-      Rational(numeric.is_a?(IntegerNode) ? numeric.value : slice.chomp("r"))
+      Rational(numerator, denominator)
+    end
+
+    # Returns the value of the node as an IntegerNode or a FloatNode. This
+    # method is deprecated in favor of #value or #numerator/#denominator.
+    def numeric
+      deprecated("value", "numerator", "denominator")
+
+      if denominator == 1
+        IntegerNode.new(source, flags, numerator, location.chop)
+      else
+        FloatNode.new(source, numerator.to_f / denominator, location.chop)
+      end
     end
   end
 
diff --git a/lib/prism/translation/parser/compiler.rb b/lib/prism/translation/parser/compiler.rb
index a6c3118e..c3fa6823 100644
--- a/lib/prism/translation/parser/compiler.rb
+++ b/lib/prism/translation/parser/compiler.rb
@@ -881,7 +881,7 @@ module Prism
         # 1i
         # ^^
         def visit_imaginary_node(node)
-          visit_numeric(node, builder.complex([imaginary_value(node), srange(node.location)]))
+          visit_numeric(node, builder.complex([Complex(0, node.numeric.value), srange(node.location)]))
         end
 
         # { foo: }
@@ -1514,7 +1514,7 @@ module Prism
         # 1r
         # ^^
         def visit_rational_node(node)
-          visit_numeric(node, builder.rational([rational_value(node), srange(node.location)]))
+          visit_numeric(node, builder.rational([node.value, srange(node.location)]))
         end
 
         # redo
@@ -1940,12 +1940,6 @@ module Prism
           forwarding
         end
 
-        # Because we have mutated the AST to allow for newlines in the middle of
-        # a rational, we need to manually handle the value here.
-        def imaginary_value(node)
-          Complex(0, node.numeric.is_a?(RationalNode) ? rational_value(node.numeric) : node.numeric.value)
-        end
-
         # Negate the value of a numeric node. This is a special case where you
         # have a negative sign on one line and then a number on the next line.
         # In normal Ruby, this will always be a method call. The parser gem,
@@ -1955,7 +1949,9 @@ module Prism
           case receiver.type
           when :integer_node, :float_node
             receiver.copy(value: -receiver.value, location: message_loc.join(receiver.location))
-          when :rational_node, :imaginary_node
+          when :rational_node
+            receiver.copy(numerator: -receiver.numerator, location: message_loc.join(receiver.location))
+          when :imaginary_node
             receiver.copy(numeric: numeric_negate(message_loc, receiver.numeric), location: message_loc.join(receiver.location))
           end
         end
@@ -1974,16 +1970,6 @@ module Prism
             parameters.block.nil?
         end
 
-        # Because we have mutated the AST to allow for newlines in the middle of
-        # a rational, we need to manually handle the value here.
-        def rational_value(node)
-          if node.numeric.is_a?(IntegerNode)
-            Rational(node.numeric.value)
-          else
-            Rational(node.slice.gsub(/\s/, "").chomp("r"))
-          end
-        end
-
         # Locations in the parser gem AST are generated using this class. We
         # store a reference to its constant to make it slightly faster to look
         # up.
diff --git a/src/prism.c b/src/prism.c
index e1467993..a3a8089f 100644
--- a/src/prism.c
+++ b/src/prism.c
@@ -4290,7 +4290,7 @@ pm_float_node_imaginary_create(pm_parser_t *parser, const pm_token_t *token) {
 }
 
 /**
- * Allocate and initialize a new FloatNode node from a FLOAT_RATIONAL token.
+ * Allocate and initialize a new RationalNode node from a FLOAT_RATIONAL token.
  */
 static pm_rational_node_t *
 pm_float_node_rational_create(pm_parser_t *parser, const pm_token_t *token) {
@@ -4300,16 +4300,44 @@ pm_float_node_rational_create(pm_parser_t *parser, const pm_token_t *token) {
     *node = (pm_rational_node_t) {
         {
             .type = PM_RATIONAL_NODE,
-            .flags = PM_NODE_FLAG_STATIC_LITERAL,
+            .flags = PM_INTEGER_BASE_FLAGS_DECIMAL | PM_NODE_FLAG_STATIC_LITERAL,
             .location = PM_LOCATION_TOKEN_VALUE(token)
         },
-        .numeric = (pm_node_t *) pm_float_node_create(parser, &((pm_token_t) {
-            .type = PM_TOKEN_FLOAT,
-            .start = token->start,
-            .end = token->end - 1
-        }))
+        .numerator = { 0 },
+        .denominator = { 0 }
     };
 
+    const uint8_t *start = token->start;
+    const uint8_t *end = token->end - 1; // r
+
+    while (start < end && *start == '0') start++; // 0.1 -> .1
+    while (end > start && end[-1] == '0') end--; // 1.0 -> 1.
+
+    size_t length = (size_t) (end - start);
+    if (length == 1) {
+        node->denominator.value = 1;
+        return node;
+    }
+
+    const uint8_t *point = memchr(start, '.', length);
+    assert(point && "should have a decimal point");
+
+    uint8_t *digits = malloc(length - 1);
+    if (digits == NULL) {
+        fputs("[pm_float_node_rational_create] Failed to allocate memory", stderr);
+        abort();
+    }
+
+    memcpy(digits, start, (unsigned long) (point - start));
+    memcpy(digits + (point - start), point + 1, (unsigned long) (end - point - 1));
+    pm_integer_parse(&node->numerator, PM_INTEGER_BASE_DEFAULT, digits, digits + length - 1);
+
+    digits[0] = '1';
+    memset(digits + 1, '0', (size_t) (end - point - 1));
+    pm_integer_parse(&node->denominator, PM_INTEGER_BASE_DEFAULT, digits, digits + (end - point));
+    free(digits);
+
+    pm_integers_reduce(&node->numerator, &node->denominator);
     return node;
 }
 
@@ -4943,7 +4971,7 @@ pm_integer_node_imaginary_create(pm_parser_t *parser, pm_node_flags_t base, cons
 }
 
 /**
- * Allocate and initialize a new IntegerNode node from an INTEGER_RATIONAL
+ * Allocate and initialize a new RationalNode node from an INTEGER_RATIONAL
  * token.
  */
 static pm_rational_node_t *
@@ -4954,16 +4982,24 @@ pm_integer_node_rational_create(pm_parser_t *parser, pm_node_flags_t base, const
     *node = (pm_rational_node_t) {
         {
             .type = PM_RATIONAL_NODE,
-            .flags = PM_NODE_FLAG_STATIC_LITERAL,
+            .flags = base | PM_NODE_FLAG_STATIC_LITERAL,
             .location = PM_LOCATION_TOKEN_VALUE(token)
         },
-        .numeric = (pm_node_t *) pm_integer_node_create(parser, base, &((pm_token_t) {
-            .type = PM_TOKEN_INTEGER,
-            .start = token->start,
-            .end = token->end - 1
-        }))
+        .numerator = { 0 },
+        .denominator = { .value = 1, 0 }
     };
 
+    pm_integer_base_t integer_base = PM_INTEGER_BASE_DECIMAL;
+    switch (base) {
+        case PM_INTEGER_BASE_FLAGS_BINARY: integer_base = PM_INTEGER_BASE_BINARY; break;
+        case PM_INTEGER_BASE_FLAGS_OCTAL: integer_base = PM_INTEGER_BASE_OCTAL; break;
+        case PM_INTEGER_BASE_FLAGS_DECIMAL: break;
+        case PM_INTEGER_BASE_FLAGS_HEXADECIMAL: integer_base = PM_INTEGER_BASE_HEXADECIMAL; break;
+        default: assert(false && "unreachable"); break;
+    }
+
+    pm_integer_parse(&node->numerator, integer_base, token->start, token->end - 1);
+
     return node;
 }
 
@@ -13365,20 +13401,14 @@ 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: {
+            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 name = local_read->name;
-            pm_location_t name_loc = target->location;
-
             uint32_t depth = local_read->depth;
-            pm_scope_t *scope = pm_parser_scope_find(parser, depth);
-
-            if (pm_token_is_numbered_parameter(target->location.start, target->location.end)) {
-                pm_diagnostic_id_t diag_id = scope->parameters > PM_SCOPE_NUMBERED_PARAMETERS_NONE ? PM_ERR_EXPRESSION_NOT_WRITABLE_NUMBERED : PM_ERR_PARAMETER_NUMBERED_RESERVED;
-                PM_PARSER_ERR_FORMAT(parser, target->location.start, target->location.end, diag_id, target->location.start);
-            }
+            pm_locals_unread(&pm_parser_scope_find(parser, depth)->locals, name);
 
-            pm_locals_unread(&scope->locals, name);
+            pm_location_t name_loc = target->location;
             pm_node_destroy(parser, target);
 
             return (pm_node_t *) pm_local_variable_write_node_create(parser, name, depth, value, &name_loc, operator);
@@ -15456,13 +15486,6 @@ parse_string_part(pm_parser_t *parser) {
             expect1(parser, PM_TOKEN_EMBEXPR_END, PM_ERR_EMBEXPR_END);
             pm_token_t closing = parser->previous;
 
-            // If this set of embedded statements only contains a single
-            // statement, then Ruby does not consider it as a possible statement
-            // that could emit a line event.
-            if (statements != NULL && statements->body.size == 1) {
-                pm_node_flag_unset(statements->body.nodes[0], PM_NODE_FLAG_NEWLINE);
-            }
-
             return (pm_node_t *) pm_embedded_statements_node_create(parser, &opening, statements, &closing);
         }
 
@@ -15835,34 +15858,17 @@ parse_variable(pm_parser_t *parser) {
         } else if (current_scope->parameters & PM_SCOPE_PARAMETERS_IT) {
             pm_parser_err_previous(parser, PM_ERR_NUMBERED_PARAMETER_IT);
         } else if (outer_scope_using_numbered_parameters_p(parser)) {
-            pm_parser_err_previous(parser, PM_ERR_NUMBERED_PARAMETER_OUTER_BLOCK);
-        } else if (current_scope->numbered_parameters == PM_SCOPE_NUMBERED_PARAMETERS_INNER) {
-            pm_parser_err_previous(parser, PM_ERR_NUMBERED_PARAMETER_INNER_BLOCK);
+            pm_parser_err_previous(parser, PM_ERR_NUMBERED_PARAMETER_OUTER_SCOPE);
         } else {
             // Indicate that this scope is using numbered params so that child
             // scopes cannot. We subtract the value for the character '0' to get
             // the actual integer value of the number (only _1 through _9 are
             // valid).
             int8_t numbered_parameters = (int8_t) (parser->previous.start[1] - '0');
+            current_scope->parameters |= PM_SCOPE_PARAMETERS_NUMBERED;
 
-            // If we're about to match an =, then this is an invalid use of
-            // numbered parameters. We'll create all of the necessary
-            // infrastructure around it, but not actually mark the scope as
-            // using numbered parameters so that we can get the right error
-            // message.
-            if (!match1(parser, PM_TOKEN_EQUAL)) {
-                current_scope->parameters |= PM_SCOPE_PARAMETERS_NUMBERED;
-
-                if (numbered_parameters > current_scope->numbered_parameters) {
-                    current_scope->numbered_parameters = numbered_parameters;
-                }
-
-                // Go through the parent scopes and mark them as being
-                // disallowed from using numbered parameters because this inner
-                // scope is using them.
-                for (pm_scope_t *scope = current_scope->previous; scope != NULL && !scope->closed; scope = scope->previous) {
-                    scope->numbered_parameters = PM_SCOPE_NUMBERED_PARAMETERS_INNER;
-                }
+            if (numbered_parameters > current_scope->numbered_parameters) {
+                current_scope->numbered_parameters = numbered_parameters;
             }
 
             // When you use a numbered parameter, it implies the existence
@@ -16887,10 +16893,12 @@ parse_negative_numeric(pm_node_t *node) {
             cast->value = -cast->value;
             break;
         }
-        case PM_RATIONAL_NODE:
-            node->location.start--;
-            parse_negative_numeric(((pm_rational_node_t *) node)->numeric);
+        case PM_RATIONAL_NODE: {
+            pm_rational_node_t *cast = (pm_rational_node_t *) node;
+            cast->base.location.start--;
+            cast->numerator.negative = true;
             break;
+        }
         case PM_IMAGINARY_NODE:
             node->location.start--;
             parse_negative_numeric(((pm_imaginary_node_t *) node)->numeric);
diff --git a/src/static_literals.c b/src/static_literals.c
index 08f61c81..754a2d26 100644
--- a/src/static_literals.c
+++ b/src/static_literals.c
@@ -58,6 +58,25 @@ murmur_hash(const uint8_t *key, size_t length) {
     return hash;
 }
 
+/**
+ * Hash the value of an integer and return it.
+ */
+static uint32_t
+integer_hash(const pm_integer_t *integer) {
+    uint32_t hash;
+    if (integer->values) {
+        hash = murmur_hash((const uint8_t *) integer->values, sizeof(uint32_t) * integer->length);
+    } else {
+        hash = murmur_hash((const uint8_t *) &integer->value, sizeof(uint32_t));
+    }
+
+    if (integer->negative) {
+        hash ^= murmur_scramble((uint32_t) 1);
+    }
+
+    return hash;
+}
+
 /**
  * Return the hash of the given node. It is important that nodes that have
  * equivalent static literal values have the same hash. This is because we use
@@ -68,19 +87,8 @@ node_hash(const pm_static_literals_metadata_t *metadata, const pm_node_t *node)
     switch (PM_NODE_TYPE(node)) {
         case PM_INTEGER_NODE: {
             // Integers hash their value.
-            const pm_integer_t *integer = &((const pm_integer_node_t *) node)->value;
-            uint32_t hash;
-            if (integer->values) {
-                hash = murmur_hash((const uint8_t *) integer->values, sizeof(uint32_t) * integer->length);
-            } else {
-                hash = murmur_hash((const uint8_t *) &integer->value, sizeof(uint32_t));
-            }
-
-            if (integer->negative) {
-                hash ^= murmur_scramble((uint32_t) 1);
-            }
-
-            return hash;
+            const pm_integer_node_t *cast = (const pm_integer_node_t *) node;
+            return integer_hash(&cast->value);
         }
         case PM_SOURCE_LINE_NODE: {
             // Source lines hash their line number.
@@ -94,11 +102,9 @@ node_hash(const pm_static_literals_metadata_t *metadata, const pm_node_t *node)
             return murmur_hash((const uint8_t *) value, sizeof(double));
         }
         case PM_RATIONAL_NODE: {
-            // Rationals hash their numeric value. Because their numeric value
-            // is stored as a subnode, we hash that node and then mix in the
-            // fact that this is a rational node.
-            const pm_node_t *numeric = ((const pm_rational_node_t *) node)->numeric;
-            return node_hash(metadata, numeric) ^ murmur_scramble((uint32_t) node->type);
+            // Rationals hash their numerator and denominator.
+            const pm_rational_node_t *cast = (const pm_rational_node_t *) node;
+            return integer_hash(&cast->numerator) ^ integer_hash(&cast->denominator) ^ murmur_scramble((uint32_t) cast->base.type);
         }
         case PM_IMAGINARY_NODE: {
             // Imaginaries hash their numeric value. Because their numeric value
@@ -275,8 +281,15 @@ pm_compare_number_nodes(const pm_static_literals_metadata_t *metadata, const pm_
     switch (PM_NODE_TYPE(left)) {
         case PM_IMAGINARY_NODE:
             return pm_compare_number_nodes(metadata, ((const pm_imaginary_node_t *) left)->numeric, ((const pm_imaginary_node_t *) right)->numeric);
-        case PM_RATIONAL_NODE:
-            return pm_compare_number_nodes(metadata, ((const pm_rational_node_t *) left)->numeric, ((const pm_rational_node_t *) right)->numeric);
+        case PM_RATIONAL_NODE: {
+            const pm_rational_node_t *left_rational = (const pm_rational_node_t *) left;
+            const pm_rational_node_t *right_rational = (const pm_rational_node_t *) right;
+
+            int result = pm_integer_compare(&left_rational->denominator, &right_rational->denominator);
+            if (result != 0) return result;
+
+            return pm_integer_compare(&left_rational->numerator, &right_rational->numerator);
+        }
         case PM_INTEGER_NODE:
             return pm_compare_integer_nodes(metadata, left, right);
         case PM_FLOAT_NODE:
@@ -456,7 +469,7 @@ pm_static_literal_positive_p(const pm_node_t *node) {
         case PM_INTEGER_NODE:
             return !((const pm_integer_node_t *) node)->value.negative;
         case PM_RATIONAL_NODE:
-            return pm_static_literal_positive_p(((const pm_rational_node_t *) node)->numeric);
+            return !((const pm_rational_node_t *) node)->numerator.negative;
         case PM_IMAGINARY_NODE:
             return pm_static_literal_positive_p(((const pm_imaginary_node_t *) node)->numeric);
         default:
@@ -465,43 +478,6 @@ pm_static_literal_positive_p(const pm_node_t *node) {
     }
 }
 
-/**
- * Inspect a rational node that wraps a float node. This is going to be a
- * poor-man's version of the Ruby `Rational#to_s` method, because we're not
- * going to try to reduce the rational by finding the GCD. We'll leave that for
- * a future improvement.
- */
-static void
-pm_rational_inspect(pm_buffer_t *buffer, pm_rational_node_t *node) {
-    const uint8_t *start = node->base.location.start;
-    const uint8_t *end = node->base.location.end - 1; // r
-
-    while (start < end && *start == '0') start++; // 0.1 -> .1
-    while (end > start && end[-1] == '0') end--; // 1.0 -> 1.
-    size_t length = (size_t) (end - start);
-
-    const uint8_t *point = memchr(start, '.', length);
-    assert(point && "should have a decimal point");
-
-    uint8_t *digits = malloc(length - 1);
-    if (digits == NULL) return;
-
-    memcpy(digits, start, (unsigned long) (point - start));
-    memcpy(digits + (point - start), point + 1, (unsigned long) (end - point - 1));
-
-    pm_integer_t numerator = { 0 };
-    pm_integer_parse(&numerator, PM_INTEGER_BASE_DECIMAL, digits, digits + length - 1);
-
-    pm_buffer_append_byte(buffer, '(');
-    pm_integer_string(buffer, &numerator);
-    pm_buffer_append_string(buffer, "/1", 2);
-    for (size_t index = 0; index < (size_t) (end - point - 1); index++) pm_buffer_append_byte(buffer, '0');
-    pm_buffer_append_byte(buffer, ')');
-
-    pm_integer_free(&numerator);
-    free(digits);
-}
-
 /**
  * Create a string-based representation of the given static literal.
  */
@@ -544,7 +520,9 @@ pm_static_literal_inspect_node(pm_buffer_t *buffer, const pm_static_literals_met
             pm_buffer_append_string(buffer, "(0", 2);
             if (pm_static_literal_positive_p(numeric)) pm_buffer_append_byte(buffer, '+');
             pm_static_literal_inspect_node(buffer, metadata, numeric);
-            if (PM_NODE_TYPE_P(numeric, PM_RATIONAL_NODE)) pm_buffer_append_byte(buffer, '*');
+            if (PM_NODE_TYPE_P(numeric, PM_RATIONAL_NODE)) {
+                pm_buffer_append_byte(buffer, '*');
+            }
             pm_buffer_append_string(buffer, "i)", 2);
             break;
         }
@@ -555,22 +533,12 @@ pm_static_literal_inspect_node(pm_buffer_t *buffer, const pm_static_literals_met
             pm_buffer_append_string(buffer, "nil", 3);
             break;
         case PM_RATIONAL_NODE: {
-            const pm_node_t *numeric = ((const pm_rational_node_t *) node)->numeric;
-
-            switch (PM_NODE_TYPE(numeric)) {
-                case PM_INTEGER_NODE:
-                    pm_buffer_append_byte(buffer, '(');
-                    pm_static_literal_inspect_node(buffer, metadata, numeric);
-                    pm_buffer_append_string(buffer, "/1)", 3);
-                    break;
-                case PM_FLOAT_NODE:
-                    pm_rational_inspect(buffer, (pm_rational_node_t *) node);
-                    break;
-                default:
-                    assert(false && "unreachable");
-                    break;
-            }
-
+            const pm_rational_node_t *rational = (const pm_rational_node_t *) node;
+            pm_buffer_append_byte(buffer, '(');
+            pm_integer_string(buffer, &rational->numerator);
+            pm_buffer_append_byte(buffer, '/');
+            pm_integer_string(buffer, &rational->denominator);
+            pm_buffer_append_byte(buffer, ')');
             break;
         }
         case PM_REGULAR_EXPRESSION_NODE: {
diff --git a/src/util/pm_integer.c b/src/util/pm_integer.c
index e523bae9..015789cc 100644
--- a/src/util/pm_integer.c
+++ b/src/util/pm_integer.c
@@ -473,13 +473,16 @@ pm_integer_parse_big(pm_integer_t *integer, uint32_t multiplier, const uint8_t *
  */
 PRISM_EXPORTED_FUNCTION void
 pm_integer_parse(pm_integer_t *integer, pm_integer_base_t base, const uint8_t *start, const uint8_t *end) {
-    // Ignore unary +. Unary + is parsed differently and will not end up here.
+    // Ignore unary +. Unary - is parsed differently and will not end up here.
     // Instead, it will modify the parsed integer later.
     if (*start == '+') start++;
 
     // Determine the multiplier from the base, and skip past any prefixes.
     uint32_t multiplier = 10;
     switch (base) {
+        case PM_INTEGER_BASE_DEFAULT:
+            while (*start == '0') start++; // 01 -> 1
+            break;
         case PM_INTEGER_BASE_BINARY:
             start += 2; // 0b
             multiplier = 2;
@@ -572,6 +575,39 @@ pm_integer_compare(const pm_integer_t *left, const pm_integer_t *right) {
     return 0;
 }
 
+/**
+ * Reduce a ratio of integers to its simplest form.
+ */
+void pm_integers_reduce(pm_integer_t *numerator, pm_integer_t *denominator) {
+    // If either the numerator or denominator do not fit into a 32-bit integer,
+    // then this function is a no-op. In the future, we may consider reducing
+    // even the larger numbers, but for now we're going to keep it simple.
+    if (
+        // If the numerator doesn't fit into a 32-bit integer, return early.
+        numerator->length != 0 ||
+        // If the denominator doesn't fit into a 32-bit integer, return early.
+        denominator->length != 0 ||
+        // If the numerator is 0, then return early.
+        numerator->value == 0 ||
+        // If the denominator is 1, then return early.
+        denominator->value == 1
+    ) return;
+
+    // Find the greatest common divisor of the numerator and denominator.
+    uint32_t divisor = numerator->value;
+    uint32_t remainder = denominator->value;
+
+    while (remainder != 0) {
+        uint32_t temporary = remainder;
+        remainder = divisor % remainder;
+        divisor = temporary;
+    }
+
+    // Divide the numerator and denominator by the greatest common divisor.
+    numerator->value /= divisor;
+    denominator->value /= divisor;
+}
+
 /**
  * Convert an integer to a decimal string.
  */
diff --git a/templates/src/diagnostic.c.erb b/templates/src/diagnostic.c.erb
index 30d6b0ff..57c52602 100644
--- a/templates/src/diagnostic.c.erb
+++ b/templates/src/diagnostic.c.erb
@@ -197,7 +197,6 @@ static const pm_diagnostic_data_t diagnostic_messages[PM_DIAGNOSTIC_ID_MAX] = {
     [PM_ERR_EXPRESSION_NOT_WRITABLE_FILE]       = { "Can't assign to __FILE__", PM_ERROR_LEVEL_SYNTAX },
     [PM_ERR_EXPRESSION_NOT_WRITABLE_LINE]       = { "Can't assign to __LINE__", PM_ERROR_LEVEL_SYNTAX },
     [PM_ERR_EXPRESSION_NOT_WRITABLE_NIL]        = { "Can't assign to nil", PM_ERROR_LEVEL_SYNTAX },
-    [PM_ERR_EXPRESSION_NOT_WRITABLE_NUMBERED]   = { "Can't assign to numbered parameter %.2s", PM_ERROR_LEVEL_SYNTAX },
     [PM_ERR_EXPRESSION_NOT_WRITABLE_SELF]       = { "Can't change the value of self", PM_ERROR_LEVEL_SYNTAX },
     [PM_ERR_EXPRESSION_NOT_WRITABLE_TRUE]       = { "Can't assign to true", PM_ERROR_LEVEL_SYNTAX },
     [PM_ERR_FLOAT_PARSE]                        = { "could not parse the float '%.*s'", PM_ERROR_LEVEL_SYNTAX },
@@ -268,10 +267,9 @@ static const pm_diagnostic_data_t diagnostic_messages[PM_DIAGNOSTIC_ID_MAX] = {
     [PM_ERR_NOT_EXPRESSION]                     = { "expected an expression after `not`", PM_ERROR_LEVEL_SYNTAX },
     [PM_ERR_NO_LOCAL_VARIABLE]                  = { "%.*s: no such local variable", PM_ERROR_LEVEL_SYNTAX },
     [PM_ERR_NUMBER_LITERAL_UNDERSCORE]          = { "number literal ending with a `_`", PM_ERROR_LEVEL_SYNTAX },
-    [PM_ERR_NUMBERED_PARAMETER_INNER_BLOCK]     = { "numbered parameter is already used in inner block", PM_ERROR_LEVEL_SYNTAX },
     [PM_ERR_NUMBERED_PARAMETER_IT]              = { "numbered parameters are not allowed when an 'it' parameter is defined", PM_ERROR_LEVEL_SYNTAX },
     [PM_ERR_NUMBERED_PARAMETER_ORDINARY]        = { "numbered parameters are not allowed when an ordinary parameter is defined", PM_ERROR_LEVEL_SYNTAX },
-    [PM_ERR_NUMBERED_PARAMETER_OUTER_BLOCK]     = { "numbered parameter is already used in outer block", PM_ERROR_LEVEL_SYNTAX },
+    [PM_ERR_NUMBERED_PARAMETER_OUTER_SCOPE]     = { "numbered parameter is already used in outer scope", PM_ERROR_LEVEL_SYNTAX },
     [PM_ERR_OPERATOR_MULTI_ASSIGN]              = { "unexpected operator for a multiple assignment", PM_ERROR_LEVEL_SYNTAX },
     [PM_ERR_OPERATOR_WRITE_ARGUMENTS]           = { "unexpected operator after a call with arguments", PM_ERROR_LEVEL_SYNTAX },
     [PM_ERR_OPERATOR_WRITE_BLOCK]               = { "unexpected operator after a call with a block", PM_ERROR_LEVEL_SYNTAX },


Test patch:
diff --git a/test/prism/errors_test.rb b/test/prism/errors_test.rb
index 6f926452..3670b90d 100644
--- a/test/prism/errors_test.rb
+++ b/test/prism/errors_test.rb
@@ -1377,7 +1377,7 @@ module Prism
 
     def test_double_scope_numbered_parameters
       source = "-> { _1 + -> { _2 } }"
-      errors = [["numbered parameter is already used in outer block", 15..17]]
+      errors = [["numbered parameter is already used in outer scope", 15..17]]
 
       assert_errors expression(source), source, errors
     end
diff --git a/test/prism/snapshots/numbers.txt b/test/prism/snapshots/numbers.txt
index 740f3f5a..58aea454 100644
--- a/test/prism/snapshots/numbers.txt
+++ b/test/prism/snapshots/numbers.txt
@@ -65,52 +65,48 @@
         │       ├── flags: decimal
         │       └── value: 1
         ├── @ RationalNode (location: (41,0)-(41,2))
-        │   └── numeric:
-        │       @ IntegerNode (location: (41,0)-(41,1))
-        │       ├── flags: decimal
-        │       └── value: 1
+        │   ├── flags: decimal
+        │   ├── numerator: 1
+        │   └── denominator: 1
         ├── @ IntegerNode (location: (43,0)-(43,2))
         │   ├── flags: decimal
         │   └── value: -1
         ├── @ ImaginaryNode (location: (45,0)-(45,3))
         │   └── numeric:
         │       @ RationalNode (location: (45,0)-(45,2))
-        │       └── numeric:
-        │           @ IntegerNode (location: (45,0)-(45,1))
-        │           ├── flags: decimal
-        │           └── value: 1
+        │       ├── flags: decimal
+        │       ├── numerator: 1
+        │       └── denominator: 1
         ├── @ RationalNode (location: (47,0)-(47,4))
-        │   └── numeric:
-        │       @ FloatNode (location: (47,0)-(47,3))
-        │       └── value: 1.2
+        │   ├── flags: decimal
+        │   ├── numerator: 6
+        │   └── denominator: 5
         ├── @ ImaginaryNode (location: (49,0)-(49,5))
         │   └── numeric:
         │       @ RationalNode (location: (49,0)-(49,4))
-        │       └── numeric:
-        │           @ FloatNode (location: (49,0)-(49,3))
-        │           └── value: 1.2
+        │       ├── flags: decimal
+        │       ├── numerator: 6
+        │       └── denominator: 5
         ├── @ ImaginaryNode (location: (51,0)-(51,4))
         │   └── numeric:
         │       @ RationalNode (location: (51,0)-(51,3))
-        │       └── numeric:
-        │           @ IntegerNode (location: (51,0)-(51,2))
-        │           ├── flags: decimal
-        │           └── value: -1
+        │       ├── flags: decimal
+        │       ├── numerator: -1
+        │       └── denominator: 1
         ├── @ RationalNode (location: (53,0)-(53,5))
-        │   └── numeric:
-        │       @ FloatNode (location: (53,0)-(53,4))
-        │       └── value: -1.2
+        │   ├── flags: decimal
+        │   ├── numerator: -6
+        │   └── denominator: 5
         ├── @ ImaginaryNode (location: (55,0)-(55,6))
         │   └── numeric:
         │       @ RationalNode (location: (55,0)-(55,5))
-        │       └── numeric:
-        │           @ FloatNode (location: (55,0)-(55,4))
-        │           └── value: -1.2
+        │       ├── flags: decimal
+        │       ├── numerator: -6
+        │       └── denominator: 5
         ├── @ RationalNode (location: (57,0)-(57,4))
-        │   └── numeric:
-        │       @ IntegerNode (location: (57,0)-(57,3))
-        │       ├── flags: octal
-        │       └── value: 1
+        │   ├── flags: octal
+        │   ├── numerator: 1
+        │   └── denominator: 1
         ├── @ ImaginaryNode (location: (59,0)-(59,4))
         │   └── numeric:
         │       @ IntegerNode (location: (59,0)-(59,3))
@@ -119,15 +115,13 @@
         ├── @ ImaginaryNode (location: (61,0)-(61,5))
         │   └── numeric:
         │       @ RationalNode (location: (61,0)-(61,4))
-        │       └── numeric:
-        │           @ IntegerNode (location: (61,0)-(61,3))
-        │           ├── flags: octal
-        │           └── value: 1
+        │       ├── flags: octal
+        │       ├── numerator: 1
+        │       └── denominator: 1
         ├── @ RationalNode (location: (63,0)-(63,4))
-        │   └── numeric:
-        │       @ IntegerNode (location: (63,0)-(63,3))
-        │       ├── flags: decimal
-        │       └── value: 1
+        │   ├── flags: decimal
+        │   ├── numerator: 1
+        │   └── denominator: 1
         ├── @ ImaginaryNode (location: (65,0)-(65,4))
         │   └── numeric:
         │       @ IntegerNode (location: (65,0)-(65,3))
@@ -136,7 +130,6 @@
         └── @ ImaginaryNode (location: (67,0)-(67,5))
             └── numeric:
                 @ RationalNode (location: (67,0)-(67,4))
-                └── numeric:
-                    @ IntegerNode (location: (67,0)-(67,3))
-                    ├── flags: binary
-                    └── value: 1
+                ├── flags: binary
+                ├── numerator: 1
+                └── denominator: 1
diff --git a/test/prism/snapshots/patterns.txt b/test/prism/snapshots/patterns.txt
index 16298e79..17aa23b4 100644
--- a/test/prism/snapshots/patterns.txt
+++ b/test/prism/snapshots/patterns.txt
@@ -86,10 +86,9 @@
         │   │   └── block: ∅
         │   ├── pattern:
         │   │   @ RationalNode (location: (5,7)-(5,9))
-        │   │   └── numeric:
-        │   │       @ IntegerNode (location: (5,7)-(5,8))
-        │   │       ├── flags: decimal
-        │   │       └── value: 1
+        │   │   ├── flags: decimal
+        │   │   ├── numerator: 1
+        │   │   └── denominator: 1
         │   └── operator_loc: (5,4)-(5,6) = "=>"
         ├── @ MatchRequiredNode (location: (6,0)-(6,11))
         │   ├── value:
@@ -598,16 +597,14 @@
         │   │   ├── flags: ∅
         │   │   ├── left:
         │   │   │   @ RationalNode (location: (31,7)-(31,9))
-        │   │   │   └── numeric:
-        │   │   │       @ IntegerNode (location: (31,7)-(31,8))
-        │   │   │       ├── flags: decimal
-        │   │   │       └── value: 1
+        │   │   │   ├── flags: decimal
+        │   │   │   ├── numerator: 1
+        │   │   │   └── denominator: 1
         │   │   ├── right:
         │   │   │   @ RationalNode (location: (31,13)-(31,15))
-        │   │   │   └── numeric:
-        │   │   │       @ IntegerNode (location: (31,13)-(31,14))
-        │   │   │       ├── flags: decimal
-        │   │   │       └── value: 1
+        │   │   │   ├── flags: decimal
+        │   │   │   ├── numerator: 1
+        │   │   │   └── denominator: 1
         │   │   └── operator_loc: (31,10)-(31,12) = ".."
         │   └── operator_loc: (31,4)-(31,6) = "=>"
         ├── @ MatchRequiredNode (location: (32,0)-(32,19))
@@ -2461,10 +2458,9 @@
         │   │   └── block: ∅
         │   ├── pattern:
         │   │   @ RationalNode (location: (108,7)-(108,9))
-        │   │   └── numeric:
-        │   │       @ IntegerNode (location: (108,7)-(108,8))
-        │   │       ├── flags: decimal
-        │   │       └── value: 1
+        │   │   ├── flags: decimal
+        │   │   ├── numerator: 1
+        │   │   └── denominator: 1
         │   └── operator_loc: (108,4)-(108,6) = "in"
         ├── @ MatchPredicateNode (location: (109,0)-(109,11))
         │   ├── value:
@@ -3017,10 +3013,9 @@
         │   │   └── @ InNode (location: (139,10)-(139,20))
         │   │       ├── pattern:
         │   │       │   @ RationalNode (location: (139,13)-(139,15))
-        │   │       │   └── numeric:
-        │   │       │       @ IntegerNode (location: (139,13)-(139,14))
-        │   │       │       ├── flags: decimal
-        │   │       │       └── value: 1
+        │   │       │   ├── flags: decimal
+        │   │       │   ├── numerator: 1
+        │   │       │   └── denominator: 1
         │   │       ├── statements: ∅
         │   │       ├── in_loc: (139,10)-(139,12) = "in"
         │   │       └── then_loc: (139,16)-(139,20) = "then"
@@ -3758,10 +3753,9 @@
         │   │       │   │   @ StatementsNode (location: (166,13)-(166,15))
         │   │       │   │   └── body: (length: 1)
         │   │       │   │       └── @ RationalNode (location: (166,13)-(166,15))
-        │   │       │   │           └── numeric:
-        │   │       │   │               @ IntegerNode (location: (166,13)-(166,14))
-        │   │       │   │               ├── flags: decimal
-        │   │       │   │               └── value: 1
+        │   │       │   │           ├── flags: decimal
+        │   │       │   │           ├── numerator: 1
+        │   │       │   │           └── denominator: 1
         │   │       │   ├── consequent: ∅
         │   │       │   └── end_keyword_loc: ∅
         │   │       ├── statements: ∅
diff --git a/test/prism/snapshots/seattlerb/ruby21_numbers.txt b/test/prism/snapshots/seattlerb/ruby21_numbers.txt
index 34a3452d..e7eec943 100644
--- a/test/prism/snapshots/seattlerb/ruby21_numbers.txt
+++ b/test/prism/snapshots/seattlerb/ruby21_numbers.txt
@@ -12,16 +12,14 @@
             │   │       ├── flags: decimal
             │   │       └── value: 1
             │   ├── @ RationalNode (location: (1,5)-(1,7))
-            │   │   └── numeric:
-            │   │       @ IntegerNode (location: (1,5)-(1,6))
-            │   │       ├── flags: decimal
-            │   │       └── value: 2
+            │   │   ├── flags: decimal
+            │   │   ├── numerator: 2
+            │   │   └── denominator: 1
             │   └── @ ImaginaryNode (location: (1,9)-(1,12))
             │       └── numeric:
             │           @ RationalNode (location: (1,9)-(1,11))
-            │           └── numeric:
-            │               @ IntegerNode (location: (1,9)-(1,10))
-            │               ├── flags: decimal
-            │               └── value: 3
+            │           ├── flags: decimal
+            │           ├── numerator: 3
+            │           └── denominator: 1
             ├── opening_loc: (1,0)-(1,1) = "["
             └── closing_loc: (1,12)-(1,13) = "]"
diff --git a/test/prism/snapshots/symbols.txt b/test/prism/snapshots/symbols.txt
index dbd3a4d0..48ff0d63 100644
--- a/test/prism/snapshots/symbols.txt
+++ b/test/prism/snapshots/symbols.txt
@@ -146,10 +146,9 @@
         │   │   ├── @ FloatNode (location: (29,4)-(29,7))
         │   │   │   └── value: 1.0
         │   │   ├── @ RationalNode (location: (29,9)-(29,11))
-        │   │   │   └── numeric:
-        │   │   │       @ IntegerNode (location: (29,9)-(29,10))
-        │   │   │       ├── flags: decimal
-        │   │   │       └── value: 1
+        │   │   │   ├── flags: decimal
+        │   │   │   ├── numerator: 1
+        │   │   │   └── denominator: 1
         │   │   └── @ ImaginaryNode (location: (29,13)-(29,15))
         │   │       └── numeric:
         │   │           @ IntegerNode (location: (29,13)-(29,14))
diff --git a/test/prism/snapshots/unparser/corpus/literal/literal.txt b/test/prism/snapshots/unparser/corpus/literal/literal.txt
index 98b88e11..ddb10456 100644
--- a/test/prism/snapshots/unparser/corpus/literal/literal.txt
+++ b/test/prism/snapshots/unparser/corpus/literal/literal.txt
@@ -316,18 +316,17 @@
         │   ├── flags: decimal
         │   └── value: 1
         ├── @ RationalNode (location: (19,0)-(19,2))
-        │   └── numeric:
-        │       @ IntegerNode (location: (19,0)-(19,1))
-        │       ├── flags: decimal
-        │       └── value: 1
+        │   ├── flags: decimal
+        │   ├── numerator: 1
+        │   └── denominator: 1
         ├── @ RationalNode (location: (20,0)-(20,4))
-        │   └── numeric:
-        │       @ FloatNode (location: (20,0)-(20,3))
-        │       └── value: 1.5
+        │   ├── flags: decimal
+        │   ├── numerator: 3
+        │   └── denominator: 2
         ├── @ RationalNode (location: (21,0)-(21,4))
-        │   └── numeric:
-        │       @ FloatNode (location: (21,0)-(21,3))
-        │       └── value: 1.3
+        │   ├── flags: decimal
+        │   ├── numerator: 13
+        │   └── denominator: 10
         ├── @ ImaginaryNode (location: (22,0)-(22,2))
         │   └── numeric:
         │       @ IntegerNode (location: (22,0)-(22,1))
@@ -354,10 +353,9 @@
         ├── @ ImaginaryNode (location: (27,0)-(27,3))
         │   └── numeric:
         │       @ RationalNode (location: (27,0)-(27,2))
-        │       └── numeric:
-        │           @ IntegerNode (location: (27,0)-(27,1))
-        │           ├── flags: decimal
-        │           └── value: 1
+        │       ├── flags: decimal
+        │       ├── numerator: 1
+        │       └── denominator: 1
         ├── @ InterpolatedStringNode (location: (28,0)-(28,11))
         │   ├── flags: ∅
         │   ├── opening_loc: ∅
diff --git a/test/prism/snapshots/unparser/corpus/semantic/literal.txt b/test/prism/snapshots/unparser/corpus/semantic/literal.txt
index ef666890..448207f5 100644
--- a/test/prism/snapshots/unparser/corpus/semantic/literal.txt
+++ b/test/prism/snapshots/unparser/corpus/semantic/literal.txt
@@ -4,14 +4,13 @@
     @ StatementsNode (location: (1,0)-(14,10))
     └── body: (length: 14)
         ├── @ RationalNode (location: (1,0)-(1,4))
-        │   └── numeric:
-        │       @ FloatNode (location: (1,0)-(1,3))
-        │       └── value: 1.0
+        │   ├── flags: decimal
+        │   ├── numerator: 1
+        │   └── denominator: 1
         ├── @ RationalNode (location: (2,0)-(2,3))
-        │   └── numeric:
-        │       @ IntegerNode (location: (2,0)-(2,2))
-        │       ├── flags: decimal
-        │       └── value: 0
+        │   ├── flags: decimal
+        │   ├── numerator: 0
+        │   └── denominator: 1
         ├── @ IntegerNode (location: (3,0)-(3,3))
         │   ├── flags: hexadecimal
         │   └── value: 1
diff --git a/test/prism/snapshots/whitequark/complex.txt b/test/prism/snapshots/whitequark/complex.txt
index e688585a..bc748db0 100644
--- a/test/prism/snapshots/whitequark/complex.txt
+++ b/test/prism/snapshots/whitequark/complex.txt
@@ -10,9 +10,9 @@
         ├── @ ImaginaryNode (location: (3,0)-(3,6))
         │   └── numeric:
         │       @ RationalNode (location: (3,0)-(3,5))
-        │       └── numeric:
-        │           @ FloatNode (location: (3,0)-(3,4))
-        │           └── value: 42.1
+        │       ├── flags: decimal
+        │       ├── numerator: 421
+        │       └── denominator: 10
         ├── @ ImaginaryNode (location: (5,0)-(5,3))
         │   └── numeric:
         │       @ IntegerNode (location: (5,0)-(5,2))
@@ -21,7 +21,6 @@
         └── @ ImaginaryNode (location: (7,0)-(7,4))
             └── numeric:
                 @ RationalNode (location: (7,0)-(7,3))
-                └── numeric:
-                    @ IntegerNode (location: (7,0)-(7,2))
-                    ├── flags: decimal
-                    └── value: 42
+                ├── flags: decimal
+                ├── numerator: 42
+                └── denominator: 1
diff --git a/test/prism/snapshots/whitequark/rational.txt b/test/prism/snapshots/whitequark/rational.txt
index 90bbd179..e8c8eed5 100644
--- a/test/prism/snapshots/whitequark/rational.txt
+++ b/test/prism/snapshots/whitequark/rational.txt
@@ -4,11 +4,10 @@
     @ StatementsNode (location: (1,0)-(3,3))
     └── body: (length: 2)
         ├── @ RationalNode (location: (1,0)-(1,5))
-        │   └── numeric:
-        │       @ FloatNode (location: (1,0)-(1,4))
-        │       └── value: 42.1
+        │   ├── flags: decimal
+        │   ├── numerator: 421
+        │   └── denominator: 10
         └── @ RationalNode (location: (3,0)-(3,3))
-            └── numeric:
-                @ IntegerNode (location: (3,0)-(3,2))
-                ├── flags: decimal
-                └── value: 42
+            ├── flags: decimal
+            ├── numerator: 42
+            └── denominator: 1
diff --git a/test/prism/snapshots/whitequark/ruby_bug_11873_a.txt b/test/prism/snapshots/whitequark/ruby_bug_11873_a.txt
index 93418e64..831d57e3 100644
--- a/test/prism/snapshots/whitequark/ruby_bug_11873_a.txt
+++ b/test/prism/snapshots/whitequark/ruby_bug_11873_a.txt
@@ -225,9 +225,9 @@
         │   │       │   ├── closing_loc: (7,7)-(7,8) = ")"
         │   │       │   └── block: ∅
         │   │       └── @ RationalNode (location: (7,10)-(7,14))
-        │   │           └── numeric:
-        │   │               @ FloatNode (location: (7,10)-(7,13))
-        │   │               └── value: 1.0
+        │   │           ├── flags: decimal
+        │   │           ├── numerator: 1
+        │   │           └── denominator: 1
         │   ├── closing_loc: ∅
         │   └── block:
         │       @ BlockNode (location: (7,15)-(7,21))
@@ -519,9 +519,9 @@
         │   │       │   ├── closing_loc: (17,8)-(17,9) = ")"
         │   │       │   └── block: ∅
         │   │       └── @ RationalNode (location: (17,11)-(17,15))
-        │   │           └── numeric:
-        │   │               @ FloatNode (location: (17,11)-(17,14))
-        │   │               └── value: 1.0
+        │   │           ├── flags: decimal
+        │   │           ├── numerator: 1
+        │   │           └── denominator: 1
         │   ├── closing_loc: ∅
         │   └── block:
         │       @ BlockNode (location: (17,16)-(17,22))
@@ -833,9 +833,9 @@
         │   │       │       ├── opening_loc: (27,3)-(27,4) = "{"
         │   │       │       └── closing_loc: (27,7)-(27,8) = "}"
         │   │       └── @ RationalNode (location: (27,10)-(27,14))
-        │   │           └── numeric:
-        │   │               @ FloatNode (location: (27,10)-(27,13))
-        │   │               └── value: 1.0
+        │   │           ├── flags: decimal
+        │   │           ├── numerator: 1
+        │   │           └── denominator: 1
         │   ├── closing_loc: ∅
         │   └── block:
         │       @ BlockNode (location: (27,15)-(27,21))
@@ -1152,9 +1152,9 @@
         │   │       │       ├── opening_loc: (37,3)-(37,4) = "{"
         │   │       │       └── closing_loc: (37,8)-(37,9) = "}"
         │   │       └── @ RationalNode (location: (37,11)-(37,15))
-        │   │           └── numeric:
-        │   │               @ FloatNode (location: (37,11)-(37,14))
-        │   │               └── value: 1.0
+        │   │           ├── flags: decimal
+        │   │           ├── numerator: 1
+        │   │           └── denominator: 1
         │   ├── closing_loc: ∅
         │   └── block:
         │       @ BlockNode (location: (37,16)-(37,22))