Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions lib/prism/lex_compat.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -837,6 +843,8 @@ def post_process_tokens(tokens, source, data_loc, bom, eof_token)
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] == ""
Expand All @@ -851,8 +859,7 @@ def post_process_tokens(tokens, source, data_loc, bom, eof_token)

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("\\")
Expand Down
4 changes: 1 addition & 3 deletions lib/prism/parse_result.rb
Original file line number Diff line number Diff line change
Expand Up @@ -223,9 +223,7 @@ def deep_freeze
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
Expand Down
87 changes: 73 additions & 14 deletions lib/prism/translation/ripper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,64 @@ def self.sexp_raw(src, filename = "-", lineno = 1, raise_errors: false)
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.

Expand Down Expand Up @@ -489,6 +547,7 @@ def initialize(source, filename = "(ripper)", lineno = 1)
@lineno = lineno
@column = 0
@result = nil
@line_and_column_cache = nil
end

##########################################################################
Expand Down Expand Up @@ -948,7 +1007,7 @@ def visit_begin_node(node)
on_stmts_add(on_stmts_new, on_void_stmt)
else
body = node.statements.body
body.unshift(nil) if void_stmt?(location, node.statements.body[0].location, allow_newline)
body = [nil, *body] if void_stmt?(location, node.statements.body[0].location, allow_newline)

bounds(node.statements.location)
visit_statements_node_body(body)
Expand All @@ -965,7 +1024,7 @@ def visit_begin_node(node)
[nil]
else
body = else_clause_node.statements.body
body.unshift(nil) if void_stmt?(else_clause_node.else_keyword_loc, else_clause_node.statements.body[0].location, allow_newline)
body = [nil, *body] if void_stmt?(else_clause_node.else_keyword_loc, else_clause_node.statements.body[0].location, allow_newline)
body
end

Expand All @@ -987,7 +1046,7 @@ def visit_begin_node(node)
on_bodystmt(visit_statements_node_body([nil]), nil, nil, nil)
when StatementsNode
body = [*node.body]
body.unshift(nil) if void_stmt?(location, body[0].location, allow_newline)
body = [nil, *body] if void_stmt?(location, body[0].location, allow_newline)
stmts = visit_statements_node_body(body)

bounds(node.body.first.location)
Expand Down Expand Up @@ -1036,7 +1095,7 @@ def visit_block_node(node)
braces ? stmts : on_bodystmt(stmts, nil, nil, nil)
when StatementsNode
stmts = node.body.body
stmts.unshift(nil) if void_stmt?(node.parameters&.location || node.opening_loc, node.body.location, false)
stmts = [nil, *stmts] if void_stmt?(node.parameters&.location || node.opening_loc, node.body.location, false)
stmts = visit_statements_node_body(stmts)

bounds(node.body.location)
Expand Down Expand Up @@ -1963,7 +2022,7 @@ def visit_else_node(node)
[nil]
else
body = node.statements.body
body.unshift(nil) if void_stmt?(node.else_keyword_loc, node.statements.body[0].location, false)
body = [nil, *body] if void_stmt?(node.else_keyword_loc, node.statements.body[0].location, false)
body
end

Expand Down Expand Up @@ -2018,7 +2077,7 @@ def visit_ensure_node(node)
[nil]
else
body = node.statements.body
body.unshift(nil) if void_stmt?(node.ensure_keyword_loc, body[0].location, false)
body = [nil, *body] if void_stmt?(node.ensure_keyword_loc, body[0].location, false)
body
end

Expand Down Expand Up @@ -2801,7 +2860,7 @@ def visit_lambda_node(node)
braces ? stmts : on_bodystmt(stmts, nil, nil, nil)
when StatementsNode
stmts = node.body.body
stmts.unshift(nil) if void_stmt?(node.parameters&.location || node.opening_loc, node.body.location, false)
stmts = [nil, *stmts] if void_stmt?(node.parameters&.location || node.opening_loc, node.body.location, false)
stmts = visit_statements_node_body(stmts)

bounds(node.body.location)
Expand Down Expand Up @@ -3295,7 +3354,7 @@ def visit_pre_execution_node(node)
# The top-level program node.
def visit_program_node(node)
body = node.statements.body
body << nil if body.empty?
body = [nil] if body.empty?
statements = visit_statements_node_body(body)

bounds(node.location)
Expand Down Expand Up @@ -4011,7 +4070,11 @@ def visit_yield_node(node)

# Lazily initialize the parse result.
def result
@result ||= Prism.parse(source, partial_script: true, version: "current")
@result ||= Prism.parse(source, partial_script: true, version: "current", freeze: true)
end

def line_and_column_cache
@line_and_column_cache ||= LineAndColumnCache.new(result.source)
end

##########################################################################
Expand Down Expand Up @@ -4114,12 +4177,8 @@ def visit_write_value(node)

# 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:
Expand Down
8 changes: 8 additions & 0 deletions rbi/generated/prism/lex_compat.rbi

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions rbi/generated/prism/parse_result.rbi

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions sig/generated/prism/lex_compat.rbs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 1 addition & 3 deletions sig/generated/prism/parse_result.rbs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.