Ruby 4.1.0dev (2026-10-28 revision 1b06b950bb460d20a9dcecb8b97d363adbf4a358)
static_literals.c
1#include "prism/internal/static_literals.h"
2
5
6#include "prism/internal/allocator.h"
7#include "prism/internal/buffer.h"
8#include "prism/internal/integer.h"
9#include "prism/internal/isinf.h"
10#include "prism/internal/oom.h"
11#include "prism/internal/parser.h"
12#include "prism/internal/stringy.h"
13#include "prism/internal/xxh3.h"
14
15#include <assert.h>
16#include <inttypes.h>
17#include <math.h>
18#include <stdlib.h>
19#include <string.h>
20
28static PRISM_INLINE uint64_t
29key_hash(uint64_t type, uint64_t a, uint64_t b) {
30 const uint64_t key[3] = { type, a, b };
31 return pm_xxh3((const uint8_t *) key, sizeof(key));
32}
33
39static uint64_t
40integer_hash(const pm_integer_t *integer) {
41 uint64_t magnitude = integer->values ? pm_xxh3((const uint8_t *) integer->values, sizeof(uint32_t) * integer->length) : integer->value;
42 return key_hash(PM_INTEGER_NODE, integer->negative, magnitude);
43}
44
50static uint64_t
51node_hash(const pm_parser_t *parser, const pm_node_t *node) {
52 switch (PM_NODE_TYPE(node)) {
53 case PM_INTEGER_NODE:
54 return integer_hash(&((const pm_integer_node_t *) node)->value);
55 case PM_SOURCE_LINE_NODE: {
56 int64_t line = (int64_t) pm_line_offset_list_line_column(&parser->line_offsets, node->location.start, parser->start_line).line;
57 return key_hash(PM_INTEGER_NODE, line < 0, (uint64_t) (line < 0 ? -line : line));
58 }
59 case PM_FLOAT_NODE: {
60 const pm_float_node_t *cast = (const pm_float_node_t *) node;
61 double value = cast->value;
62 if (value == 0.0) value = 0.0; /* Normalize -0.0 to 0.0. */
63
64 uint64_t bits;
65 memcpy(&bits, &value, sizeof(bits));
66 return key_hash(PM_FLOAT_NODE, bits, 0);
67 }
68 case PM_RATIONAL_NODE: {
69 const pm_rational_node_t *cast = (const pm_rational_node_t *) node;
70 return key_hash(PM_RATIONAL_NODE, integer_hash(&cast->numerator), integer_hash(&cast->denominator));
71 }
72 case PM_IMAGINARY_NODE: {
73 const pm_imaginary_node_t *cast = (const pm_imaginary_node_t *) node;
74 return key_hash(PM_IMAGINARY_NODE, node_hash(parser, cast->numeric), 0);
75 }
76 case PM_STRING_NODE: {
77 const pm_string_node_t *cast = (const pm_string_node_t *) node;
78
79 pm_node_flags_t mask = PM_STRING_FLAGS_FORCED_BINARY_ENCODING;
80 if (parser->encoding != PM_ENCODING_UTF_8_ENTRY) mask |= PM_STRING_FLAGS_FORCED_UTF8_ENCODING;
81
82 return key_hash(PM_STRING_NODE, pm_xxh3(pm_string_source(&cast->unescaped), pm_string_length(&cast->unescaped)), node->flags & mask);
83 }
84 case PM_SOURCE_FILE_NODE: {
85 const pm_source_file_node_t *cast = (const pm_source_file_node_t *) node;
86 return key_hash(PM_STRING_NODE, pm_xxh3(pm_string_source(&cast->filepath), pm_string_length(&cast->filepath)), 0);
87 }
88 case PM_REGULAR_EXPRESSION_NODE: {
90 return key_hash(PM_REGULAR_EXPRESSION_NODE, pm_xxh3(pm_string_source(&cast->unescaped), pm_string_length(&cast->unescaped)), node->flags & (pm_node_flags_t) ~PM_REGULAR_EXPRESSION_FLAGS_ONCE);
91 }
92 case PM_SYMBOL_NODE: {
93 const pm_symbol_node_t *cast = (const pm_symbol_node_t *) node;
94
95 pm_node_flags_t mask = PM_SYMBOL_FLAGS_FORCED_BINARY_ENCODING | PM_SYMBOL_FLAGS_FORCED_US_ASCII_ENCODING;
96 if (parser->encoding != PM_ENCODING_UTF_8_ENTRY) mask |= PM_SYMBOL_FLAGS_FORCED_UTF8_ENCODING;
97
98 return key_hash(PM_SYMBOL_NODE, pm_xxh3(pm_string_source(&cast->unescaped), pm_string_length(&cast->unescaped)), node->flags & mask);
99 }
100 case PM_FALSE_NODE:
101 case PM_NIL_NODE:
102 case PM_SOURCE_ENCODING_NODE:
103 case PM_TRUE_NODE:
104 return key_hash(PM_NODE_TYPE(node), 0, 0);
105 default:
106 assert(false && "unreachable");
107 return 0;
108 }
109}
110
114#define COMPARE(left_, right_) (((left_) < (right_)) ? -1 : ((left_) > (right_)) ? 1 : 0)
115
119static int64_t
120int64_value(const pm_parser_t *parser, const pm_node_t *node) {
121 switch (PM_NODE_TYPE(node)) {
122 case PM_INTEGER_NODE: {
123 const pm_integer_t *integer = &((const pm_integer_node_t *) node)->value;
124 if (integer->values) return integer->negative ? INT64_MIN : INT64_MAX;
125
126 int64_t value = (int64_t) integer->value;
127 return integer->negative ? -value : value;
128 }
129 case PM_SOURCE_LINE_NODE:
130 return (int64_t) pm_line_offset_list_line_column(&parser->line_offsets, node->location.start, parser->start_line).line;
131 default:
132 assert(false && "unreachable");
133 return 0;
134 }
135}
136
141static int
142compare_integer_nodes(const pm_parser_t *parser, const pm_node_t *left, const pm_node_t *right) {
143 if (PM_NODE_TYPE_P(left, PM_SOURCE_LINE_NODE) || PM_NODE_TYPE_P(right, PM_SOURCE_LINE_NODE)) {
144 return COMPARE(int64_value(parser, left), int64_value(parser, right));
145 }
146
147 const pm_integer_t *left_integer = &((const pm_integer_node_t *) left)->value;
148 const pm_integer_t *right_integer = &((const pm_integer_node_t *) right)->value;
149 return pm_integer_compare(left_integer, right_integer);
150}
151
155static int
156compare_float_nodes(PRISM_UNUSED const pm_parser_t *parser, const pm_node_t *left, const pm_node_t *right) {
157 return COMPARE(((const pm_float_node_t *) left)->value, ((const pm_float_node_t *) right)->value);
158}
159
163static const pm_string_t *
164string_value(const pm_node_t *node) {
165 switch (PM_NODE_TYPE(node)) {
166 case PM_STRING_NODE:
167 return &((const pm_string_node_t *) node)->unescaped;
168 case PM_SOURCE_FILE_NODE:
169 return &((const pm_source_file_node_t *) node)->filepath;
170 case PM_SYMBOL_NODE:
171 return &((const pm_symbol_node_t *) node)->unescaped;
172 default:
173 assert(false && "unreachable");
174 return NULL;
175 }
176}
177
190static int
191compare_string_nodes(const pm_node_t *left, const pm_node_t *right, pm_node_flags_t mask) {
192 const pm_string_t *left_string = string_value(left);
193 const pm_string_t *right_string = string_value(right);
194
195 int result = pm_string_compare(left_string, right_string);
196 if (result != 0) return result;
197
198 /*
199 * Equal bytes are not enough. In a binary source file the two bytes written
200 * as `"\xC3\xA9"` stay BINARY while a `"\u00E9"` escape is forced to UTF-8,
201 * so those are distinct keys even though the bytes match.
202 */
203 return COMPARE(left->flags & mask, right->flags & mask);
204}
205
211static int
212compare_regular_expression_nodes(PRISM_UNUSED const pm_parser_t *parser, const pm_node_t *left, const pm_node_t *right) {
213 const pm_regular_expression_node_t *left_regexp = (const pm_regular_expression_node_t *) left;
214 const pm_regular_expression_node_t *right_regexp = (const pm_regular_expression_node_t *) right;
215
216 int result = pm_string_compare(&left_regexp->unescaped, &right_regexp->unescaped);
217 if (result != 0) return result;
218
219 pm_node_flags_t mask = (pm_node_flags_t) ~PM_REGULAR_EXPRESSION_FLAGS_ONCE;
220 return COMPARE(left->flags & mask, right->flags & mask);
221}
222
228static enum pm_node_type
229static_literal_type(const pm_node_t *node) {
230 switch (PM_NODE_TYPE(node)) {
231 case PM_SOURCE_LINE_NODE:
232 return PM_INTEGER_NODE;
233 case PM_SOURCE_FILE_NODE:
234 return PM_STRING_NODE;
235 default:
236 return PM_NODE_TYPE(node);
237 }
238}
239
244static int
245compare_nodes(const pm_parser_t *parser, const pm_node_t *left, const pm_node_t *right) {
246 enum pm_node_type left_type = static_literal_type(left);
247 enum pm_node_type right_type = static_literal_type(right);
248 if (left_type != right_type) return COMPARE(left_type, right_type);
249
250 switch (left_type) {
251 case PM_INTEGER_NODE:
252 return compare_integer_nodes(parser, left, right);
253 case PM_FLOAT_NODE:
254 return compare_float_nodes(parser, left, right);
255 case PM_IMAGINARY_NODE:
256 return compare_nodes(parser, ((const pm_imaginary_node_t *) left)->numeric, ((const pm_imaginary_node_t *) right)->numeric);
257 case PM_RATIONAL_NODE: {
258 const pm_rational_node_t *left_rational = (const pm_rational_node_t *) left;
259 const pm_rational_node_t *right_rational = (const pm_rational_node_t *) right;
260
261 int result = pm_integer_compare(&left_rational->denominator, &right_rational->denominator);
262 if (result != 0) return result;
263
264 return pm_integer_compare(&left_rational->numerator, &right_rational->numerator);
265 }
266 case PM_STRING_NODE: {
267 pm_node_flags_t mask = PM_STRING_FLAGS_FORCED_BINARY_ENCODING;
268 if (parser->encoding != PM_ENCODING_UTF_8_ENTRY) mask |= PM_STRING_FLAGS_FORCED_UTF8_ENCODING;
269 return compare_string_nodes(left, right, mask);
270 }
271 case PM_SYMBOL_NODE: {
272 pm_node_flags_t mask = PM_SYMBOL_FLAGS_FORCED_BINARY_ENCODING | PM_SYMBOL_FLAGS_FORCED_US_ASCII_ENCODING;
273 if (parser->encoding != PM_ENCODING_UTF_8_ENTRY) mask |= PM_SYMBOL_FLAGS_FORCED_UTF8_ENCODING;
274 return compare_string_nodes(left, right, mask);
275 }
276 case PM_REGULAR_EXPRESSION_NODE:
277 return compare_regular_expression_nodes(parser, left, right);
278 case PM_FALSE_NODE:
279 case PM_NIL_NODE:
280 case PM_SOURCE_ENCODING_NODE:
281 case PM_TRUE_NODE:
282 return 0;
283 default:
284 assert(false && "unreachable");
285 return 0;
286 }
287}
288
289#undef COMPARE
290
295static void
296pm_static_literals_place(pm_static_literals_entry_t *entries, uint32_t mask, pm_static_literals_entry_t entry) {
297 uint32_t index = (uint32_t) (entry.hash & mask);
298 while (entries[index].node != NULL) {
299 index = (index + 1) & mask;
300 }
301 entries[index] = entry;
302}
303
309static void
310pm_static_literals_resize(const pm_parser_t *parser, pm_static_literals_t *literals, uint32_t capacity) {
312 if (entries == NULL) PRISM_OOM();
313
314 /* It turns out to be more efficient to mask the hash value than to use the
315 * modulo operator. Because our capacities are always powers of two, we can
316 * use a bitwise AND to get the same result as the modulo operator. */
317 uint32_t mask = capacity - 1;
318
319 if (literals->capacity == 0) {
320 for (uint32_t index = 0; index < literals->size; index++) {
321 pm_node_t *node = literals->nodes[index];
322 pm_static_literals_place(entries, mask, (pm_static_literals_entry_t) { .node = node, .hash = node_hash(parser, node) });
323 }
324 } else {
325 for (uint32_t index = 0; index < literals->capacity; index++) {
326 if (literals->entries[index].node != NULL) {
327 pm_static_literals_place(entries, mask, literals->entries[index]);
328 }
329 }
330
331 xfree_sized(literals->entries, literals->capacity * sizeof(pm_static_literals_entry_t));
332 }
333
334 literals->entries = entries;
335 literals->capacity = capacity;
336}
337
344pm_node_t *
345pm_static_literals_add(const pm_parser_t *parser, pm_static_literals_t *literals, pm_node_t *node, bool replace) {
346 switch (PM_NODE_TYPE(node)) {
347 case PM_FALSE_NODE:
348 case PM_FLOAT_NODE:
349 case PM_IMAGINARY_NODE:
350 case PM_INTEGER_NODE:
351 case PM_NIL_NODE:
352 case PM_RATIONAL_NODE:
353 case PM_REGULAR_EXPRESSION_NODE:
354 case PM_SOURCE_ENCODING_NODE:
355 case PM_SOURCE_FILE_NODE:
356 case PM_SOURCE_LINE_NODE:
357 case PM_STRING_NODE:
358 case PM_SYMBOL_NODE:
359 case PM_TRUE_NODE:
360 break;
361 default:
362 return NULL;
363 }
364
365 if (literals->capacity == 0) {
366 /* A set without a hash table compares the new node against each of
367 * its nodes directly. Comparing nodes of different types returns
368 * early, so this costs about as much as hashing the new node. */
369 for (uint32_t index = 0; index < literals->size; index++) {
370 pm_node_t *existing = literals->nodes[index];
371
372 if (compare_nodes(parser, existing, node) == 0) {
373 if (replace) literals->nodes[index] = node;
374 return existing;
375 }
376 }
377
378 if (literals->size < PM_STATIC_LITERALS_INLINE_CAPACITY) {
379 literals->nodes[literals->size++] = node;
380 return NULL;
381 }
382
383 /* The set is full, so its nodes move into a hash table. The table is
384 * sized to stay at most half full after this node is added, with room
385 * to spare before its first resize. */
386 pm_static_literals_resize(parser, literals, PM_STATIC_LITERALS_INLINE_CAPACITY * 4);
387 } else if (literals->size * 2 >= literals->capacity) {
388 /* If we are out of space, we need to resize the hash. */
389 pm_static_literals_resize(parser, literals, literals->capacity * 2);
390 }
391
392 uint64_t hash = node_hash(parser, node);
393 uint32_t mask = literals->capacity - 1;
394 uint32_t index = (uint32_t) (hash & mask);
395
396 /* We use linear probing to resolve collisions. This means that if the
397 * current index is occupied, we will move to the next index and try again.
398 * We are guaranteed that this will eventually find an empty slot because we
399 * resize the hash when it gets too full. Entries whose hash differs cannot
400 * be equal, so only entries with a matching hash are compared. */
401 while (literals->entries[index].node != NULL) {
402 const pm_static_literals_entry_t *entry = &literals->entries[index];
403 if (entry->hash == hash && compare_nodes(parser, entry->node, node) == 0) break;
404 index = (index + 1) & mask;
405 }
406
407 /* If the current index is occupied, we need to return the node that was
408 * already in the hash. Otherwise, we can just increment the size and insert
409 * the new node. */
410 pm_node_t *result = literals->entries[index].node;
411
412 if (result == NULL) {
413 literals->size++;
414 literals->entries[index] = (pm_static_literals_entry_t) { .node = node, .hash = hash };
415 } else if (replace) {
416 literals->entries[index].node = node;
417 }
418
419 return result;
420}
421
425void
426pm_static_literals_free(pm_static_literals_t *literals) {
427 if (literals->capacity > 0) xfree_sized(literals->entries, literals->capacity * sizeof(pm_static_literals_entry_t));
428}
429
434static bool
435pm_static_literal_positive_p(const pm_node_t *node) {
436 switch (PM_NODE_TYPE(node)) {
437 case PM_FLOAT_NODE:
438 return ((const pm_float_node_t *) node)->value > 0;
439 case PM_INTEGER_NODE:
440 return !((const pm_integer_node_t *) node)->value.negative;
441 case PM_RATIONAL_NODE:
442 return !((const pm_rational_node_t *) node)->numerator.negative;
443 case PM_IMAGINARY_NODE:
444 return pm_static_literal_positive_p(((const pm_imaginary_node_t *) node)->numeric);
445 default:
446 assert(false && "unreachable");
447 return false;
448 }
449}
450
454void
455pm_static_literal_inspect(pm_buffer_t *buffer, const pm_parser_t *parser, const pm_node_t *node) {
456 switch (PM_NODE_TYPE(node)) {
457 case PM_FALSE_NODE:
458 pm_buffer_append_string(buffer, "false", 5);
459 break;
460 case PM_FLOAT_NODE: {
461 const double value = ((const pm_float_node_t *) node)->value;
462
463 if (PRISM_ISINF(value)) {
464 if (parser->start[node->location.start] == '-') {
465 pm_buffer_append_byte(buffer, '-');
466 }
467 pm_buffer_append_string(buffer, "Infinity", 8);
468 } else if (value == 0.0) {
469 if (parser->start[node->location.start] == '-') {
470 pm_buffer_append_byte(buffer, '-');
471 }
472 pm_buffer_append_string(buffer, "0.0", 3);
473 } else {
474 pm_buffer_append_format(buffer, "%g", value);
475
476 /* %g will not insert a .0 for 1e100 (we'll get back 1e+100). So
477 * we check for the decimal point and add it in here if it's not
478 * present. */
479 if (pm_buffer_index(buffer, '.') == SIZE_MAX) {
480 size_t exponent_index = pm_buffer_index(buffer, 'e');
481 size_t index = exponent_index == SIZE_MAX ? pm_buffer_length(buffer) : exponent_index;
482 pm_buffer_insert(buffer, index, ".0", 2);
483 }
484 }
485
486 break;
487 }
488 case PM_IMAGINARY_NODE: {
489 const pm_node_t *numeric = ((const pm_imaginary_node_t *) node)->numeric;
490 pm_buffer_append_string(buffer, "(0", 2);
491 if (pm_static_literal_positive_p(numeric)) pm_buffer_append_byte(buffer, '+');
492 pm_static_literal_inspect(buffer, parser, numeric);
493 if (PM_NODE_TYPE_P(numeric, PM_RATIONAL_NODE)) {
494 pm_buffer_append_byte(buffer, '*');
495 }
496 pm_buffer_append_string(buffer, "i)", 2);
497 break;
498 }
499 case PM_INTEGER_NODE:
500 pm_integer_string(buffer, &((const pm_integer_node_t *) node)->value);
501 break;
502 case PM_NIL_NODE:
503 pm_buffer_append_string(buffer, "nil", 3);
504 break;
505 case PM_RATIONAL_NODE: {
506 const pm_rational_node_t *rational = (const pm_rational_node_t *) node;
507 pm_buffer_append_byte(buffer, '(');
508 pm_integer_string(buffer, &rational->numerator);
509 pm_buffer_append_byte(buffer, '/');
510 pm_integer_string(buffer, &rational->denominator);
511 pm_buffer_append_byte(buffer, ')');
512 break;
513 }
514 case PM_REGULAR_EXPRESSION_NODE: {
515 const pm_string_t *unescaped = &((const pm_regular_expression_node_t *) node)->unescaped;
516 pm_buffer_append_byte(buffer, '/');
517 pm_buffer_append_source(buffer, pm_string_source(unescaped), pm_string_length(unescaped), PM_BUFFER_ESCAPING_RUBY);
518 pm_buffer_append_byte(buffer, '/');
519
520 if (PM_NODE_FLAG_P(node, PM_REGULAR_EXPRESSION_FLAGS_MULTI_LINE)) pm_buffer_append_string(buffer, "m", 1);
521 if (PM_NODE_FLAG_P(node, PM_REGULAR_EXPRESSION_FLAGS_IGNORE_CASE)) pm_buffer_append_string(buffer, "i", 1);
522 if (PM_NODE_FLAG_P(node, PM_REGULAR_EXPRESSION_FLAGS_EXTENDED)) pm_buffer_append_string(buffer, "x", 1);
523 if (PM_NODE_FLAG_P(node, PM_REGULAR_EXPRESSION_FLAGS_ASCII_8BIT)) pm_buffer_append_string(buffer, "n", 1);
524
525 break;
526 }
527 case PM_SOURCE_ENCODING_NODE:
528 pm_buffer_append_format(buffer, "#<Encoding:%s>", parser->encoding->name);
529 break;
530 case PM_SOURCE_FILE_NODE: {
531 const pm_string_t *filepath = &((const pm_source_file_node_t *) node)->filepath;
532 pm_buffer_append_byte(buffer, '"');
533 pm_buffer_append_source(buffer, pm_string_source(filepath), pm_string_length(filepath), PM_BUFFER_ESCAPING_RUBY);
534 pm_buffer_append_byte(buffer, '"');
535 break;
536 }
537 case PM_SOURCE_LINE_NODE:
538 pm_buffer_append_format(buffer, "%" PRIi32, pm_line_offset_list_line_column(&parser->line_offsets, node->location.start, parser->start_line).line);
539 break;
540 case PM_STRING_NODE: {
541 const pm_string_t *unescaped = &((const pm_string_node_t *) node)->unescaped;
542 pm_buffer_append_byte(buffer, '"');
543 pm_buffer_append_source(buffer, pm_string_source(unescaped), pm_string_length(unescaped), PM_BUFFER_ESCAPING_RUBY);
544 pm_buffer_append_byte(buffer, '"');
545 break;
546 }
547 case PM_SYMBOL_NODE: {
548 const pm_string_t *unescaped = &((const pm_symbol_node_t *) node)->unescaped;
549 pm_buffer_append_byte(buffer, ':');
550 pm_buffer_append_source(buffer, pm_string_source(unescaped), pm_string_length(unescaped), PM_BUFFER_ESCAPING_RUBY);
551 break;
552 }
553 case PM_TRUE_NODE:
554 pm_buffer_append_string(buffer, "true", 4);
555 break;
556 default:
557 assert(false && "unreachable");
558 break;
559 }
560}
#define xcalloc
Old name of ruby_xcalloc.
Definition xmalloc.h:55
#define PRISM_INLINE
Old Visual Studio versions do not support the inline keyword, so we need to define it to be __inline.
Definition inline.h:12
VALUE type(ANYARGS)
ANYARGS-ed function type.
T * cast(pm_node_t *node) noexcept
Cast the given node, which must not be null and must be of the given struct, to that struct.
Definition node.hpp:50
FloatNode.
Definition ast.h:3977
ImaginaryNode.
Definition ast.h:4646
IntegerNode.
Definition ast.h:5242
A structure represents an arbitrary-sized integer.
Definition integer.h:16
size_t length
The number of allocated values.
Definition integer.h:21
uint32_t value
Embedded value for small integer.
Definition integer.h:32
uint32_t * values
List of 32-bit integers.
Definition integer.h:26
bool negative
Whether or not the integer is negative.
Definition integer.h:38
int32_t line
The line number.
uint32_t start
The offset of the location from the start of the source.
Definition ast.h:581
This is the base structure that represents a node in the syntax tree.
Definition ast.h:1090
pm_node_flags_t flags
This represents any flags on the node.
Definition ast.h:1101
pm_location_t location
This is the location of the node in the source.
Definition ast.h:1115
RationalNode.
Definition ast.h:6938
pm_integer_t denominator
RationalNode::denominator.
Definition ast.h:6958
pm_integer_t numerator
RationalNode::numerator.
Definition ast.h:6949
RegularExpressionNode.
Definition ast.h:7003
pm_string_t unescaped
RegularExpressionNode::unescaped.
Definition ast.h:7025
SourceFileNode.
Definition ast.h:7383
StringNode.
Definition ast.h:7485
A view of a sequence of bytes.
Definition stringy.h:23
SymbolNode.
Definition ast.h:7579
#define PRISM_UNUSED
GCC will warn if you specify a function or parameter that is unused at runtime.
Definition unused.h:13