Skip to content

Commit b88fe7d

Browse files
authored
Merge pull request #4083 from Earlopain/ripper-bounds-perf
Optimize ripper bounds, and other small perf tweaks
2 parents 442bd90 + d611aa9 commit b88fe7d

7 files changed

Lines changed: 112 additions & 37 deletions

File tree

lib/prism/lex_compat.rb

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@ module Prism
2323
# def self.[]: (Integer value) -> State
2424
# end
2525
# end
26+
#
27+
# class LineAndColumnCache
28+
# def initialize: (Source source) -> void
29+
#
30+
# def line_and_column: (Integer byte_offset) -> [Integer, Integer]
31+
# end
2632
# end
2733
# end
2834

@@ -837,6 +843,8 @@ def post_process_tokens(tokens, source, data_loc, bom, eof_token)
837843
prev_token_state = Translation::Ripper::Lexer::State[Translation::Ripper::EXPR_BEG]
838844
prev_token_end = bom ? 3 : 0
839845

846+
cache = Translation::Ripper::LineAndColumnCache.new(source)
847+
840848
tokens.each do |token|
841849
# Skip missing heredoc ends.
842850
next if token[1] == :on_heredoc_end && token[2] == ""
@@ -851,8 +859,7 @@ def post_process_tokens(tokens, source, data_loc, bom, eof_token)
851859

852860
if start_offset > prev_token_end
853861
sp_value = source.slice(prev_token_end, start_offset - prev_token_end)
854-
sp_line = source.line(prev_token_end)
855-
sp_column = source.column(prev_token_end)
862+
sp_line, sp_column = cache.line_and_column(prev_token_end)
856863
# Ripper reports columns on line 1 without counting the BOM
857864
sp_column -= 3 if sp_line == 1 && bom
858865
continuation_index = sp_value.byteindex("\\")

lib/prism/parse_result.rb

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -225,9 +225,7 @@ def deep_freeze
225225
freeze
226226
end
227227

228-
private
229-
230-
# Binary search through the offsets to find the line number for the given
228+
# Binary search through the offsets to find the index for the given
231229
# byte offset.
232230
#--
233231
#: (Integer byte_offset) -> Integer

lib/prism/translation/ripper.rb

Lines changed: 85 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -346,7 +346,7 @@ def self.coerce_source(source) # :nodoc:
346346
"__ENCODING__",
347347
"__FILE__",
348348
"__LINE__"
349-
]
349+
].to_set
350350

351351
# A list of all of the Ruby binary operators.
352352
BINARY_OPERATORS = [
@@ -371,7 +371,7 @@ def self.coerce_source(source) # :nodoc:
371371
:/,
372372
:*,
373373
:**
374-
]
374+
].to_set
375375

376376
private_constant :KEYWORDS, :BINARY_OPERATORS
377377

@@ -445,6 +445,64 @@ def self.sexp_raw(src, filename = "-", lineno = 1, raise_errors: false)
445445
autoload :SexpBuilder, "prism/translation/ripper/sexp"
446446
autoload :SexpBuilderPP, "prism/translation/ripper/sexp"
447447

448+
# Provides optimized access to line and column information.
449+
# Ripper bounds are mostly accessed in a linear fashion, so
450+
# we can try a linear scan first and fall back to binary search.
451+
class LineAndColumnCache # :nodoc:
452+
# How many should it look ahead/behind before falling back to binary searching.
453+
WINDOW = 8
454+
private_constant :WINDOW
455+
456+
#: (Source source) -> void
457+
def initialize(source)
458+
@source = source
459+
@offsets = source.offsets
460+
@hint = 0
461+
end
462+
463+
#: (Integer byte_offset) -> [Integer, Integer]
464+
def line_and_column(byte_offset)
465+
@hint = new_hint(byte_offset) || @source.find_line(byte_offset)
466+
return [@hint + @source.start_line, byte_offset - @offsets[@hint]]
467+
end
468+
469+
private
470+
471+
def new_hint(byte_offset)
472+
if @offsets[@hint] <= byte_offset
473+
# Same line?
474+
if (@hint + 1 >= @offsets.size || @offsets[@hint + 1] > byte_offset)
475+
return @hint
476+
end
477+
478+
# Scan forwards
479+
limit = [@hint + WINDOW + 1, @offsets.size].min
480+
idx = @hint + 1
481+
while idx < limit
482+
if @offsets[idx] > byte_offset
483+
return idx - 1
484+
end
485+
if @offsets[idx] == byte_offset
486+
return idx
487+
end
488+
idx += 1
489+
end
490+
else
491+
# Scan backwards
492+
limit = @hint > WINDOW ? @hint - WINDOW : 0
493+
idx = @hint
494+
while idx >= limit + 1
495+
if @offsets[idx - 1] <= byte_offset
496+
return idx - 1
497+
end
498+
idx -= 1
499+
end
500+
end
501+
502+
nil
503+
end
504+
end
505+
448506
# :stopdoc:
449507
# This is not part of the public API but used by some gems.
450508

@@ -488,6 +546,7 @@ def initialize(source, filename = "(ripper)", lineno = 1)
488546
@lineno = lineno
489547
@column = 0
490548
@result = nil
549+
@line_and_column_cache = nil
491550
end
492551

493552
##########################################################################
@@ -961,7 +1020,7 @@ def visit_begin_node(node)
9611020
on_stmts_add(on_stmts_new, on_void_stmt)
9621021
else
9631022
body = node.statements.body
964-
body.unshift(nil) if void_stmt?(location, node.statements.body[0].location, allow_newline)
1023+
body = [nil, *body] if void_stmt?(location, node.statements.body[0].location, allow_newline)
9651024

9661025
bounds(node.statements.location)
9671026
visit_statements_node_body(body)
@@ -978,7 +1037,7 @@ def visit_begin_node(node)
9781037
[nil]
9791038
else
9801039
body = else_clause_node.statements.body
981-
body.unshift(nil) if void_stmt?(else_clause_node.else_keyword_loc, else_clause_node.statements.body[0].location, allow_newline)
1040+
body = [nil, *body] if void_stmt?(else_clause_node.else_keyword_loc, else_clause_node.statements.body[0].location, allow_newline)
9821041
body
9831042
end
9841043

@@ -1000,7 +1059,7 @@ def visit_begin_node(node)
10001059
on_bodystmt(visit_statements_node_body([nil]), nil, nil, nil)
10011060
when StatementsNode
10021061
body = [*node.body]
1003-
body.unshift(nil) if void_stmt?(location, body[0].location, allow_newline)
1062+
body = [nil, *body] if void_stmt?(location, body[0].location, allow_newline)
10041063
stmts = visit_statements_node_body(body)
10051064

10061065
bounds(node.body.first.location)
@@ -1049,7 +1108,7 @@ def visit_block_node(node)
10491108
braces ? stmts : on_bodystmt(stmts, nil, nil, nil)
10501109
when StatementsNode
10511110
stmts = node.body.body
1052-
stmts.unshift(nil) if void_stmt?(node.parameters&.location || node.opening_loc, node.body.location, false)
1111+
stmts = [nil, *stmts] if void_stmt?(node.parameters&.location || node.opening_loc, node.body.location, false)
10531112
stmts = visit_statements_node_body(stmts)
10541113

10551114
bounds(node.body.location)
@@ -1249,7 +1308,7 @@ def visit_call_node(node)
12491308
bounds(node.location)
12501309
on_unary(:!, receiver)
12511310
end
1252-
when *BINARY_OPERATORS
1311+
when BINARY_OPERATORS
12531312
receiver = visit(node.receiver)
12541313

12551314
bounds(node.message_loc)
@@ -1976,7 +2035,7 @@ def visit_else_node(node)
19762035
[nil]
19772036
else
19782037
body = node.statements.body
1979-
body.unshift(nil) if void_stmt?(node.else_keyword_loc, node.statements.body[0].location, false)
2038+
body = [nil, *body] if void_stmt?(node.else_keyword_loc, node.statements.body[0].location, false)
19802039
body
19812040
end
19822041

@@ -2031,7 +2090,7 @@ def visit_ensure_node(node)
20312090
[nil]
20322091
else
20332092
body = node.statements.body
2034-
body.unshift(nil) if void_stmt?(node.ensure_keyword_loc, body[0].location, false)
2093+
body = [nil, *body] if void_stmt?(node.ensure_keyword_loc, body[0].location, false)
20352094
body
20362095
end
20372096

@@ -2814,7 +2873,7 @@ def visit_lambda_node(node)
28142873
braces ? stmts : on_bodystmt(stmts, nil, nil, nil)
28152874
when StatementsNode
28162875
stmts = node.body.body
2817-
stmts.unshift(nil) if void_stmt?(node.parameters&.location || node.opening_loc, node.body.location, false)
2876+
stmts = [nil, *stmts] if void_stmt?(node.parameters&.location || node.opening_loc, node.body.location, false)
28182877
stmts = visit_statements_node_body(stmts)
28192878

28202879
bounds(node.body.location)
@@ -3308,7 +3367,7 @@ def visit_pre_execution_node(node)
33083367
# The top-level program node.
33093368
def visit_program_node(node)
33103369
body = node.statements.body
3311-
body << nil if body.empty?
3370+
body = [nil] if body.empty?
33123371
statements = visit_statements_node_body(body)
33133372

33143373
bounds(node.location)
@@ -4030,7 +4089,11 @@ def visit_yield_node(node)
40304089

40314090
# Lazily initialize the parse result.
40324091
def result
4033-
@result ||= Prism.parse(source, partial_script: true, version: "current")
4092+
@result ||= Prism.parse(source, partial_script: true, version: "current", freeze: true)
4093+
end
4094+
4095+
def line_and_column_cache
4096+
@line_and_column_cache ||= LineAndColumnCache.new(result.source)
40344097
end
40354098

40364099
##########################################################################
@@ -4051,24 +4114,23 @@ def void_stmt?(left, right, allow_newline)
40514114
# Visit the string content of a particular node. This method is used to
40524115
# split into the various token types.
40534116
def visit_token(token, allow_keywords = true)
4054-
case token
4055-
when "."
4117+
if token == "."
40564118
on_period(token)
4057-
when "`"
4119+
elsif token == "`"
40584120
on_backtick(token)
4059-
when *(allow_keywords ? KEYWORDS : [])
4121+
elsif allow_keywords && KEYWORDS.include?(token)
40604122
on_kw(token)
4061-
when /^_/
4123+
elsif token.start_with?("_")
40624124
on_ident(token)
4063-
when /^[[:upper:]]\w*$/
4125+
elsif token.match?(/^[[:upper:]]\w*$/)
40644126
on_const(token)
4065-
when /^@@/
4127+
elsif token.start_with?("@@")
40664128
on_cvar(token)
4067-
when /^@/
4129+
elsif token.start_with?("@")
40684130
on_ivar(token)
4069-
when /^\$/
4131+
elsif token.start_with?("$")
40704132
on_gvar(token)
4071-
when /^[[:punct:]]/
4133+
elsif token.match?(/^[[:punct:]]/)
40724134
on_op(token)
40734135
else
40744136
on_ident(token)
@@ -4133,12 +4195,8 @@ def visit_write_value(node)
41334195

41344196
# This method is responsible for updating lineno and column information
41354197
# to reflect the current node.
4136-
#
4137-
# This method could be drastically improved with some caching on the start
4138-
# of every line, but for now it's good enough.
41394198
def bounds(location)
4140-
@lineno = location.start_line
4141-
@column = location.start_column
4199+
@lineno, @column = line_and_column_cache.line_and_column(location.start_offset)
41424200
end
41434201

41444202
# :startdoc:

rbi/generated/prism/lex_compat.rbi

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

rbi/generated/prism/parse_result.rbi

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

sig/generated/prism/lex_compat.rbs

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

sig/generated/prism/parse_result.rbs

Lines changed: 1 addition & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)