@@ -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:
0 commit comments