[ruby/prism] Optimize ripper bounds

Basically a port of c45f781771 into ruby

It's quite effective at ~97% hit rate for me.
Speeds it up from ~6.77x slower to only 4.07x slower.

For the lexer `on_sp` it also gives a bit of an improvement:
1.04x slower to 1.10x faster

I guess the class may be universally useful but for now I just made it nodoc.

https://github.com/ruby/prism/commit/3ad9db38fe
This commit is contained in:
Earlopain 2026-04-20 19:16:56 +02:00 committed by git
parent b583dd6879
commit 0cbd210c3a
3 changed files with 74 additions and 10 deletions

View File

@ -23,6 +23,12 @@ module Prism
# def self.[]: (Integer value) -> State
# end
# end
#
# class LineAndColumnCache
# def initialize: (Source source) -> void
#
# def line_and_column: (Integer byte_offset) -> [Integer, Integer]
# end
# end
# end
@ -837,6 +843,8 @@ module Prism
prev_token_state = Translation::Ripper::Lexer::State[Translation::Ripper::EXPR_BEG]
prev_token_end = bom ? 3 : 0
cache = Translation::Ripper::LineAndColumnCache.new(source)
tokens.each do |token|
# Skip missing heredoc ends.
next if token[1] == :on_heredoc_end && token[2] == ""
@ -851,8 +859,7 @@ module Prism
if start_offset > prev_token_end
sp_value = source.slice(prev_token_end, start_offset - prev_token_end)
sp_line = source.line(prev_token_end)
sp_column = source.column(prev_token_end)
sp_line, sp_column = cache.line_and_column(prev_token_end)
# Ripper reports columns on line 1 without counting the BOM
sp_column -= 3 if sp_line == 1 && bom
continuation_index = sp_value.byteindex("\\")

View File

@ -225,9 +225,7 @@ module Prism
freeze
end
private
# Binary search through the offsets to find the line number for the given
# Binary search through the offsets to find the index for the given
# byte offset.
#--
#: (Integer byte_offset) -> Integer

View File

@ -445,6 +445,64 @@ module Prism
autoload :SexpBuilder, "prism/translation/ripper/sexp"
autoload :SexpBuilderPP, "prism/translation/ripper/sexp"
# Provides optimized access to line and column information.
# Ripper bounds are mostly accessed in a linear fashion, so
# we can try a linear scan first and fall back to binary search.
class LineAndColumnCache # :nodoc:
# How many should it look ahead/behind before falling back to binary searching.
WINDOW = 8
private_constant :WINDOW
#: (Source source) -> void
def initialize(source)
@source = source
@offsets = source.offsets
@hint = 0
end
#: (Integer byte_offset) -> [Integer, Integer]
def line_and_column(byte_offset)
@hint = new_hint(byte_offset) || @source.find_line(byte_offset)
return [@hint + @source.start_line, byte_offset - @offsets[@hint]]
end
private
def new_hint(byte_offset)
if @offsets[@hint] <= byte_offset
# Same line?
if (@hint + 1 >= @offsets.size || @offsets[@hint + 1] > byte_offset)
return @hint
end
# Scan forwards
limit = [@hint + WINDOW + 1, @offsets.size].min
idx = @hint + 1
while idx < limit
if @offsets[idx] > byte_offset
return idx - 1
end
if @offsets[idx] == byte_offset
return idx
end
idx += 1
end
else
# Scan backwards
limit = @hint > WINDOW ? @hint - WINDOW : 0
idx = @hint
while idx >= limit + 1
if @offsets[idx - 1] <= byte_offset
return idx - 1
end
idx -= 1
end
end
nil
end
end
# :stopdoc:
# This is not part of the public API but used by some gems.
@ -488,6 +546,7 @@ module Prism
@lineno = lineno
@column = 0
@result = nil
@line_and_column_cache = nil
end
##########################################################################
@ -4033,6 +4092,10 @@ module Prism
@result ||= Prism.parse(source, partial_script: true, version: "current")
end
def line_and_column_cache
@line_and_column_cache ||= LineAndColumnCache.new(result.source)
end
##########################################################################
# Helpers
##########################################################################
@ -4133,12 +4196,8 @@ module Prism
# This method is responsible for updating lineno and column information
# to reflect the current node.
#
# This method could be drastically improved with some caching on the start
# of every line, but for now it's good enough.
def bounds(location)
@lineno = location.start_line
@column = location.start_column
@lineno, @column = line_and_column_cache.line_and_column(location.start_offset)
end
# :startdoc: