From 0728ece031b10e0096bd2feae6930b7ae80faed2 Mon Sep 17 00:00:00 2001 From: Shugo Maeda Date: Tue, 28 Jul 2026 13:52:45 +0900 Subject: [PATCH 1/5] [ruby/prism] Avoid a quadratic walk when parsing long call chains parse_expression_terminator runs after every infix operator and called pm_block_call_p, which walks the whole receiver chain, even when the binding power alone already decided the result. Check the binding power first; pm_block_call_p is pure, so the condition is unchanged. Parsing "a" followed by 8000 ".b" links drops from 44.8ms to 0.46ms. https://github.com/ruby/prism/commit/8a40a928a8 Co-Authored-By: Claude Opus 5 --- prism/prism.c | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/prism/prism.c b/prism/prism.c index 3b6b6f64fb9f15..1f62ea5addc9fd 100644 --- a/prism/prism.c +++ b/prism/prism.c @@ -22403,11 +22403,7 @@ parse_expression_terminator(pm_parser_t *parser, pm_node_t *node) { // A block call (command with do-block, or any call chained // from one) can only be followed by call chaining (., ::, // &.), composition (and/or), and modifier operators. - if (pm_block_call_p(node)) { - return left > PM_BINDING_POWER_COMPOSITION && left < PM_BINDING_POWER_CALL; - } - - return false; + return left > PM_BINDING_POWER_COMPOSITION && left < PM_BINDING_POWER_CALL && pm_block_call_p(node); } case PM_SUPER_NODE: case PM_YIELD_NODE: @@ -22419,10 +22415,7 @@ parse_expression_terminator(pm_parser_t *parser, pm_node_t *node) { /* A super carrying a do-block is a block call, so it may also be * followed by call chaining (`.`, `::`, `&.`). */ - if (pm_block_call_p(node)) { - return left > PM_BINDING_POWER_COMPOSITION && left < PM_BINDING_POWER_CALL; - } - return false; + return left > PM_BINDING_POWER_COMPOSITION && left < PM_BINDING_POWER_CALL && pm_block_call_p(node); case PM_DEF_NODE: // An endless method whose body is a command-style call (e.g., // `def f = foo bar`) is a command assignment and can only be From 6a0b6ef6335fe9af1de5643b47daef6dec94e6b7 Mon Sep 17 00:00:00 2001 From: Earlopain <14981592+Earlopain@users.noreply.github.com> Date: Tue, 28 Jul 2026 12:14:12 +0200 Subject: [PATCH 2/5] [ruby/prism] Better compatibility for hash string-like symbol keys in parser translator There's some special handling in regards to empty symbols and those containing newlines that should apply here. Makes it produce the correct amount of sym/dsym nodes https://github.com/ruby/prism/commit/8b567cc403 --- lib/prism/translation/parser/compiler.rb | 8 +++++++- test/prism/fixtures/hashes.txt | 6 ++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/lib/prism/translation/parser/compiler.rb b/lib/prism/translation/parser/compiler.rb index 8e17b5b61e951f..87ae24d7d8aab3 100644 --- a/lib/prism/translation/parser/compiler.rb +++ b/lib/prism/translation/parser/compiler.rb @@ -165,7 +165,13 @@ def visit_assoc_node(node) else parts = if key.is_a?(SymbolNode) - [builder.string_internal([key.unescaped, srange(key.value_loc)])] + if key.value.nil? + [] + elsif key.value.include?("\n") + string_nodes_from_line_continuations(key.unescaped, key.value, key.value_loc.start_offset, key.opening) + else + [builder.string_internal([key.unescaped, srange(key.value_loc)])] + end else visit_all(key.parts) end diff --git a/test/prism/fixtures/hashes.txt b/test/prism/fixtures/hashes.txt index 0afb8db496d1f2..c6abbf086c0d08 100644 --- a/test/prism/fixtures/hashes.txt +++ b/test/prism/fixtures/hashes.txt @@ -26,3 +26,9 @@ tap do end { a: -1 } + +{ "": nil} + +{ "foo +bar": nil} + From f61f8f041a7b31d13aa861c6af599528e9e9a3aa Mon Sep 17 00:00:00 2001 From: Earlopain <14981592+Earlopain@users.noreply.github.com> Date: Tue, 28 Jul 2026 12:33:56 +0200 Subject: [PATCH 3/5] [ruby/prism] Appease typecheck https://github.com/ruby/prism/commit/e25f51b19c --- lib/prism/translation/parser/compiler.rb | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/prism/translation/parser/compiler.rb b/lib/prism/translation/parser/compiler.rb index 87ae24d7d8aab3..1ef30dab1beac8 100644 --- a/lib/prism/translation/parser/compiler.rb +++ b/lib/prism/translation/parser/compiler.rb @@ -165,12 +165,13 @@ def visit_assoc_node(node) else parts = if key.is_a?(SymbolNode) - if key.value.nil? + value_loc = key.value_loc + if value_loc.nil? [] - elsif key.value.include?("\n") - string_nodes_from_line_continuations(key.unescaped, key.value, key.value_loc.start_offset, key.opening) + elsif value_loc.slice.include?("\n") + string_nodes_from_line_continuations(key.unescaped, value_loc.slice, value_loc.start_offset, key.opening) else - [builder.string_internal([key.unescaped, srange(key.value_loc)])] + [builder.string_internal([key.unescaped, srange(value_loc)])] end else visit_all(key.parts) From 624075c83874fa331c7d1a276a0333e18a64e327 Mon Sep 17 00:00:00 2001 From: Shizuo Fujita Date: Tue, 28 Jul 2026 20:11:46 +0900 Subject: [PATCH 4/5] [DOC] Fix the buffer size in the IO::Buffer#& example (#18087) The example shows the result as `+4`, but the operation returns a buffer of the same size as the source, which is 10 bytes here: $ ruby -e 'p((IO::Buffer.for("1234567890") & IO::Buffer.for("\xFF\x00\x00\xFF")).size)' 10 The rest of the example is already correct. The hex dump and the ASCII column show all 10 bytes, and the description right above says "Generate a new buffer the same size as the source", so the example contradicts both. The size of the mask is 4, which is likely where the number came from. The neighbouring examples for #|, #^ and #~ all show `+10` and need no change. --- io_buffer.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/io_buffer.c b/io_buffer.c index 9599cc6a28c0e7..8e8910915da2ce 100644 --- a/io_buffer.c +++ b/io_buffer.c @@ -3540,7 +3540,7 @@ memory_and(unsigned char * restrict output, const unsigned char * restrict base, * * IO::Buffer.for("1234567890") & IO::Buffer.for("\xFF\x00\x00\xFF") * # => - * # # + * # # * # 0x00000000 31 00 00 34 35 00 00 38 39 00 1..45..89. */ static VALUE From 9b76442b629fc58d8f9afc11a2898509bc4c26cc Mon Sep 17 00:00:00 2001 From: Shizuo Fujita Date: Tue, 28 Jul 2026 21:03:59 +0900 Subject: [PATCH 5/5] [DOC] Fix return value documentation of IO::Buffer#<=> and #hexdump (#18089) The call-seq of IO::Buffer#<=> says it returns "true or false", but rb_io_buffer_compare always returns an integer: -1 or 1 when the sizes differ, and the result of memcmp as is when the sizes are equal. Since the value comes straight from memcmp it is not normalized to -1, 0 or 1, so only its sign is meaningful. p(IO::Buffer.for("abc") <=> IO::Buffer.for("abc")) # => 0 p(IO::Buffer.for("abc") <=> IO::Buffer.for("ab")) # => 1 p(IO::Buffer.for("abc") <=> IO::Buffer.for("abz")) # => -23 IO::Buffer#hexdump returns nil when the buffer does not reference any memory, that is, when #null? returns true, but this was not documented. buffer = IO::Buffer.new(4) buffer.free p buffer.hexdump # => nil --- io_buffer.c | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/io_buffer.c b/io_buffer.c index 8e8910915da2ce..e7a03fcb786b14 100644 --- a/io_buffer.c +++ b/io_buffer.c @@ -1684,11 +1684,14 @@ io_buffer_validate_range(struct rb_io_buffer *buffer, size_t offset, size_t leng } /* - * call-seq: hexdump([offset, [length, [width]]]) -> string + * call-seq: hexdump([offset, [length, [width]]]) -> string or nil * * Returns a human-readable string representation of the buffer. The exact * format is subject to change. * + * Returns +nil+ if the buffer does not reference any memory, that is, if + * #null? returns +true+ (for example after #free or #transfer). + * * buffer = IO::Buffer.for("Hello World") * puts buffer.hexdump * # 0x00000000 48 65 6c 6c 6f 20 57 6f 72 6c 64 Hello World @@ -1961,10 +1964,18 @@ io_buffer_resize(VALUE self, VALUE size) } /* - * call-seq: <=>(other) -> true or false + * call-seq: <=>(other) -> integer + * + * Returns a negative integer, zero, or a positive integer if the receiver is + * less than, equal to, or greater than +other+, respectively. + * + * Buffers are compared by size first, and if the sizes are equal, by the exact + * contents of the memory they are referencing using +memcmp+. Only the sign of + * the returned integer is meaningful; the result of +memcmp+ is returned as is. * - * Buffers are compared by size and exact contents of the memory they are - * referencing using +memcmp+. + * IO::Buffer.for("abc") <=> IO::Buffer.for("abc") # => 0 + * IO::Buffer.for("abc") <=> IO::Buffer.for("ab") # => 1 + * IO::Buffer.for("abc") <=> IO::Buffer.for("abd") # => -1 */ static VALUE rb_io_buffer_compare(VALUE self, VALUE other)