ibex-runtime 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +337 -0
  4. data/lib/ibex/runtime/ast_data.rb +23 -0
  5. data/lib/ibex/runtime/cst/annotation.rb +16 -0
  6. data/lib/ibex/runtime/cst/cursor.rb +68 -0
  7. data/lib/ibex/runtime/cst/diff.rb +80 -0
  8. data/lib/ibex/runtime/cst/editing.rb +56 -0
  9. data/lib/ibex/runtime/cst/editor.rb +94 -0
  10. data/lib/ibex/runtime/cst/green/builder.rb +211 -0
  11. data/lib/ibex/runtime/cst/green/cache.rb +150 -0
  12. data/lib/ibex/runtime/cst/green/node.rb +78 -0
  13. data/lib/ibex/runtime/cst/green/token.rb +91 -0
  14. data/lib/ibex/runtime/cst/green/trivia.rb +36 -0
  15. data/lib/ibex/runtime/cst/incremental/blender.rb +233 -0
  16. data/lib/ibex/runtime/cst/incremental/lexed_syntax.rb +28 -0
  17. data/lib/ibex/runtime/cst/incremental/parse_memo.rb +95 -0
  18. data/lib/ibex/runtime/cst/incremental/relexer.rb +134 -0
  19. data/lib/ibex/runtime/cst/incremental/session.rb +229 -0
  20. data/lib/ibex/runtime/cst/incremental/token_memo.rb +66 -0
  21. data/lib/ibex/runtime/cst/kind.rb +133 -0
  22. data/lib/ibex/runtime/cst/parse_result.rb +41 -0
  23. data/lib/ibex/runtime/cst/rewriter.rb +75 -0
  24. data/lib/ibex/runtime/cst/serialize.rb +127 -0
  25. data/lib/ibex/runtime/cst/serialized_tree.rb +44 -0
  26. data/lib/ibex/runtime/cst/source_text.rb +112 -0
  27. data/lib/ibex/runtime/cst/syntax_node.rb +432 -0
  28. data/lib/ibex/runtime/cst/syntax_token.rb +160 -0
  29. data/lib/ibex/runtime/cst/text_edit.rb +77 -0
  30. data/lib/ibex/runtime/cst/typed_node.rb +87 -0
  31. data/lib/ibex/runtime/cst/validator.rb +428 -0
  32. data/lib/ibex/runtime/cst.rb +38 -0
  33. data/lib/ibex/runtime/event.rb +44 -0
  34. data/lib/ibex/runtime/event_jsonl_tracer.rb +37 -0
  35. data/lib/ibex/runtime/event_sanitizer.rb +273 -0
  36. data/lib/ibex/runtime/generated_lexer.rb +514 -0
  37. data/lib/ibex/runtime/jsonl_tracer.rb +75 -0
  38. data/lib/ibex/runtime/lexer_input.rb +107 -0
  39. data/lib/ibex/runtime/location_span.rb +114 -0
  40. data/lib/ibex/runtime/observation.rb +82 -0
  41. data/lib/ibex/runtime/parser.rb +3027 -0
  42. data/lib/ibex/runtime/parser_sync_recovery.rb +108 -0
  43. data/lib/ibex/runtime/repair.rb +126 -0
  44. data/lib/ibex/runtime/repair_priority_queue.rb +60 -0
  45. data/lib/ibex/runtime/repair_search.rb +318 -0
  46. data/lib/ibex/runtime/resource_limits.rb +64 -0
  47. data/lib/ibex/runtime/table_format.rb +11 -0
  48. data/lib/ibex/runtime/version.rb +7 -0
  49. data/lib/ibex/runtime.rb +27 -0
  50. data/lib/ibex/tables/compact.rb +186 -0
  51. data/lib/ibex/tables/compact_actions.rb +135 -0
  52. data/lib/ibex/tables/compact_productions.rb +167 -0
  53. data/sig/ibex/runtime/ast_data.rbs +12 -0
  54. data/sig/ibex/runtime/cst/annotation.rbs +13 -0
  55. data/sig/ibex/runtime/cst/cursor.rbs +34 -0
  56. data/sig/ibex/runtime/cst/diff.rbs +29 -0
  57. data/sig/ibex/runtime/cst/editing.rbs +23 -0
  58. data/sig/ibex/runtime/cst/editor.rbs +43 -0
  59. data/sig/ibex/runtime/cst/green/builder.rbs +84 -0
  60. data/sig/ibex/runtime/cst/green/cache.rbs +60 -0
  61. data/sig/ibex/runtime/cst/green/node.rbs +49 -0
  62. data/sig/ibex/runtime/cst/green/token.rbs +59 -0
  63. data/sig/ibex/runtime/cst/green/trivia.rbs +29 -0
  64. data/sig/ibex/runtime/cst/incremental/blender.rbs +114 -0
  65. data/sig/ibex/runtime/cst/incremental/lexed_syntax.rbs +20 -0
  66. data/sig/ibex/runtime/cst/incremental/parse_memo.rbs +59 -0
  67. data/sig/ibex/runtime/cst/incremental/relexer.rbs +54 -0
  68. data/sig/ibex/runtime/cst/incremental/session.rbs +89 -0
  69. data/sig/ibex/runtime/cst/incremental/token_memo.rbs +31 -0
  70. data/sig/ibex/runtime/cst/kind.rbs +93 -0
  71. data/sig/ibex/runtime/cst/parse_result.rbs +35 -0
  72. data/sig/ibex/runtime/cst/rewriter.rbs +31 -0
  73. data/sig/ibex/runtime/cst/serialize.rbs +39 -0
  74. data/sig/ibex/runtime/cst/serialized_tree.rbs +34 -0
  75. data/sig/ibex/runtime/cst/source_text.rbs +51 -0
  76. data/sig/ibex/runtime/cst/syntax_node.rbs +195 -0
  77. data/sig/ibex/runtime/cst/syntax_token.rbs +112 -0
  78. data/sig/ibex/runtime/cst/text_edit.rbs +33 -0
  79. data/sig/ibex/runtime/cst/typed_node.rbs +50 -0
  80. data/sig/ibex/runtime/cst/validator.rbs +108 -0
  81. data/sig/ibex/runtime/cst.rbs +9 -0
  82. data/sig/ibex/runtime/event.rbs +26 -0
  83. data/sig/ibex/runtime/event_jsonl_tracer.rbs +25 -0
  84. data/sig/ibex/runtime/event_sanitizer.rbs +92 -0
  85. data/sig/ibex/runtime/generated_lexer.rbs +169 -0
  86. data/sig/ibex/runtime/jsonl_tracer.rbs +42 -0
  87. data/sig/ibex/runtime/lexer_input.rbs +44 -0
  88. data/sig/ibex/runtime/location_span.rbs +64 -0
  89. data/sig/ibex/runtime/observation.rbs +35 -0
  90. data/sig/ibex/runtime/parser.rbs +926 -0
  91. data/sig/ibex/runtime/parser_sync_recovery.rbs +60 -0
  92. data/sig/ibex/runtime/repair.rbs +92 -0
  93. data/sig/ibex/runtime/repair_priority_queue.rbs +27 -0
  94. data/sig/ibex/runtime/repair_search.rbs +114 -0
  95. data/sig/ibex/runtime/resource_limits.rbs +39 -0
  96. data/sig/ibex/runtime/table_format.rbs +11 -0
  97. data/sig/ibex/runtime/version.rbs +7 -0
  98. data/sig/ibex/runtime.rbs +6 -0
  99. data/sig/ibex/tables/compact.rbs +70 -0
  100. data/sig/ibex/tables/compact_actions.rbs +57 -0
  101. data/sig/ibex/tables/compact_productions.rbs +68 -0
  102. metadata +144 -0
@@ -0,0 +1,233 @@
1
+ # frozen_string_literal: true
2
+ # rbs_inline: enabled
3
+
4
+ module Ibex
5
+ module Runtime
6
+ module CST
7
+ # A Green nonterminal that can replace its old LR shift/reduce sequence.
8
+ class ReusableSubtree
9
+ attr_reader :green #: GreenNode
10
+ attr_reader :lhs #: Integer
11
+ attr_reader :left_state #: Integer
12
+ attr_reader :left_states #: Array[Integer?]
13
+ attr_reader :previous_trailing #: Array[GreenTrivia]
14
+
15
+ # @rbs (green: GreenNode, lhs: Integer, left_state: Integer, left_states: Array[Integer?],
16
+ # previous_trailing: Array[GreenTrivia]) -> void
17
+ def initialize(green:, lhs:, left_state:, left_states:, previous_trailing:)
18
+ @green = green
19
+ @lhs = lhs
20
+ @left_state = left_state
21
+ @left_states = left_states.dup.freeze
22
+ @previous_trailing = previous_trailing.dup.freeze
23
+ freeze
24
+ end
25
+ end
26
+
27
+ # A state-aware token source that substitutes validated old subtrees.
28
+ class Blender
29
+ class Candidate
30
+ attr_reader :entry #: ReusableSubtree
31
+ attr_reader :token_count #: Integer
32
+
33
+ # @rbs (entry: ReusableSubtree, token_count: Integer) -> void
34
+ def initialize(entry:, token_count:)
35
+ @entry = entry
36
+ @token_count = token_count
37
+ freeze
38
+ end
39
+ end
40
+
41
+ UNSAFE_FLAGS = Flags::CONTAINS_ERROR | Flags::CONTAINS_MISSING | Flags::CONTAINS_SKIPPED #: Integer
42
+
43
+ attr_reader :reused_descendants #: Integer
44
+ attr_reader :decomposed_nodes #: Integer
45
+ attr_reader :fallback_reason #: Symbol?
46
+
47
+ # @rbs @old_root: SyntaxNode
48
+ # @rbs @parse_memo: ParseMemo
49
+ # @rbs @lexed: LexedSyntax
50
+ # @rbs @edits: Array[TextEdit]
51
+ # @rbs @max_decomposed_nodes: Integer
52
+ # @rbs @enabled: bool
53
+ # @rbs @candidates: Hash[Integer, Array[Candidate]]
54
+ # @rbs @new_offsets: Hash[Integer, Integer]
55
+ # @rbs @old_tokens: Array[GreenToken]
56
+ # @rbs @token_index: Integer
57
+
58
+ # @rbs (old_root: SyntaxNode, parse_memo: ParseMemo, lexed: LexedSyntax, edits: Array[TextEdit],
59
+ # max_decomposed_nodes: Integer, ?enabled: bool) -> void
60
+ def initialize(old_root:, parse_memo:, lexed:, edits:, max_decomposed_nodes:, enabled: true)
61
+ @old_root = old_root
62
+ @parse_memo = parse_memo
63
+ @lexed = lexed
64
+ @edits = TextEdit.normalize(edits)
65
+ @max_decomposed_nodes = max_decomposed_nodes
66
+ @enabled = enabled
67
+ @candidates = {} #: Hash[Integer, Array[Candidate]]
68
+ @new_offsets = offset_index(@lexed.memo.offsets)
69
+ @old_tokens = @old_root.tokens.map(&:green)
70
+ @token_index = 0
71
+ @reused_descendants = 0
72
+ @decomposed_nodes = 0
73
+ @fallback_reason = nil #: Symbol?
74
+ build_candidates if @enabled && reusable_root?
75
+ end
76
+
77
+ # Return the next token or a whole nonterminal valid in the current LR state.
78
+ # @rbs (Integer state) -> (Array[untyped] | ReusableSubtree | false)
79
+ def next_for_state(state)
80
+ candidates = @candidates[@token_index]
81
+ candidate = candidates&.find { |item| item.entry.left_state == state }
82
+ if candidate
83
+ @token_index += candidate.token_count
84
+ @reused_descendants += candidate.entry.green.descendant_count
85
+ return candidate.entry
86
+ end
87
+
88
+ token = @lexed.raw_tokens[@token_index]
89
+ return false unless token
90
+
91
+ @token_index += 1
92
+ token
93
+ end
94
+
95
+ # @rbs () -> Integer
96
+ def token_count = @lexed.memo.tokens.length
97
+
98
+ private
99
+
100
+ # @rbs () -> bool
101
+ def reusable_root?
102
+ @old_root.green.flags.nobits?(UNSAFE_FLAGS) &&
103
+ @parse_memo.left_states.length == @old_root.green.descendant_count
104
+ end
105
+
106
+ # @rbs () -> void
107
+ def build_candidates
108
+ walk(@old_root.green, offset: 0, preorder: 0, token_index: 0)
109
+ @candidates.each_value do |values|
110
+ values.sort_by! { |candidate| -candidate.entry.green.descendant_count }
111
+ end
112
+ rescue ResourceLimitError
113
+ @candidates.clear
114
+ @fallback_reason = :decomposition_budget
115
+ end
116
+
117
+ # @rbs (GreenNode | GreenToken element, offset: Integer, preorder: Integer, token_index: Integer) ->
118
+ # [Integer, Integer, Integer]
119
+ def walk(element, offset:, preorder:, token_index:)
120
+ next_preorder = preorder + 1
121
+ return [next_preorder, offset + element.full_width, token_index + 1] if element.is_a?(GreenToken)
122
+
123
+ consume_decomposition_budget!
124
+ start_token = token_index
125
+ element.children.each do |child|
126
+ next_preorder, offset, token_index = walk(
127
+ child, offset: offset, preorder: next_preorder, token_index: token_index
128
+ )
129
+ end
130
+ add_candidate(element, preorder, offset - element.full_width, start_token, token_index)
131
+ [next_preorder, offset, token_index]
132
+ end
133
+
134
+ # @rbs (GreenNode node, Integer preorder, Integer old_offset, Integer start_token, Integer end_token) -> void
135
+ def add_candidate(node, preorder, old_offset, start_token, end_token)
136
+ return unless reusable_node?(node, old_offset)
137
+
138
+ new_offset = translated_offset(old_offset)
139
+ new_index = @new_offsets[new_offset]
140
+ token_count = end_token - start_token
141
+ return unless new_index && token_count.positive?
142
+ return unless matching_tokens?(start_token, new_index, token_count)
143
+ return unless matching_follow_token?(end_token, new_index + token_count)
144
+
145
+ left_state = @parse_memo.left_state(preorder)
146
+ return unless left_state
147
+
148
+ entry = ReusableSubtree.new(
149
+ green: node,
150
+ lhs: @old_root.kinds.nonterminal_of(node.kind),
151
+ left_state: left_state,
152
+ left_states: @parse_memo.slice(preorder, node),
153
+ previous_trailing: previous_trailing(new_index)
154
+ )
155
+ empty = [] #: Array[Candidate]
156
+ (@candidates[new_index] ||= empty) << Candidate.new(entry: entry, token_count: token_count)
157
+ end
158
+
159
+ # @rbs (GreenNode node, Integer old_offset) -> bool
160
+ def reusable_node?(node, old_offset)
161
+ node.full_width.positive? &&
162
+ node.flags.nobits?(UNSAFE_FLAGS) &&
163
+ @old_root.kinds.nonterminal?(node.kind) &&
164
+ outside_damage?(old_offset, old_offset + node.full_width)
165
+ end
166
+
167
+ # @rbs (Integer start_offset, Integer finish_offset) -> bool
168
+ def outside_damage?(start_offset, finish_offset)
169
+ @edits.none? do |edit|
170
+ edit_finish = edit.start + edit.delete_length
171
+ if edit.delete_length.zero?
172
+ edit.start >= start_offset && edit.start < finish_offset
173
+ else
174
+ edit.start < finish_offset && edit_finish > start_offset
175
+ end
176
+ end
177
+ end
178
+
179
+ # @rbs (Integer old_offset) -> Integer
180
+ def translated_offset(old_offset)
181
+ delta = @edits.sum do |edit|
182
+ edit.start + edit.delete_length <= old_offset ? edit.insert_text.bytesize - edit.delete_length : 0
183
+ end
184
+ old_offset + delta
185
+ end
186
+
187
+ # @rbs (Integer old_index, Integer new_index, Integer count) -> bool
188
+ def matching_tokens?(old_index, new_index, count)
189
+ count.times.all? do |relative|
190
+ @old_tokens.fetch(old_index + relative).equal?(@lexed.memo.tokens[new_index + relative])
191
+ end
192
+ end
193
+
194
+ # @rbs (Integer old_index, Integer new_index) -> bool
195
+ def matching_follow_token?(old_index, new_index)
196
+ old_follow = @old_tokens[old_index]
197
+ new_follow = @lexed.memo.tokens[new_index]
198
+ old_follow && new_follow ? old_follow.equal?(new_follow) : false
199
+ end
200
+
201
+ # @rbs (Integer token_index) -> Array[GreenTrivia]
202
+ def previous_trailing(token_index)
203
+ location = @lexed.raw_tokens.fetch(token_index).fetch(2)
204
+ return [] unless location.is_a?(Hash)
205
+
206
+ value = location[:cst_previous_trailing]
207
+ value.is_a?(Array) ? value.grep(GreenTrivia) : []
208
+ end
209
+
210
+ # @rbs (Array[Integer] offsets) -> Hash[Integer, Integer]
211
+ def offset_index(offsets)
212
+ result = {} #: Hash[Integer, Integer]
213
+ offsets.each_with_index { |offset, index| result[offset] ||= index }
214
+ result
215
+ end
216
+
217
+ # @rbs () -> void
218
+ def consume_decomposition_budget!
219
+ @decomposed_nodes += 1
220
+ return if @decomposed_nodes <= @max_decomposed_nodes
221
+
222
+ raise ResourceLimitError.new(
223
+ resource: :incremental_decomposed_nodes,
224
+ limit: @max_decomposed_nodes,
225
+ observed: @decomposed_nodes,
226
+ state: nil,
227
+ location: nil
228
+ )
229
+ end
230
+ end
231
+ end
232
+ end
233
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+ # rbs_inline: enabled
3
+
4
+ module Ibex
5
+ module Runtime
6
+ module CST
7
+ # One complete generated-lexer pass, including the explicit EOF token.
8
+ class LexedSyntax
9
+ attr_reader :raw_tokens #: Array[Array[untyped]]
10
+ attr_reader :memo #: TokenMemo
11
+
12
+ # @rbs (raw_tokens: Array[Array[untyped]], memo: TokenMemo) -> void
13
+ def initialize(raw_tokens:, memo:)
14
+ raise ArgumentError, "raw and Green token counts differ" unless raw_tokens.length == memo.tokens.length
15
+
16
+ @raw_tokens = raw_tokens.map { |token| token.dup.freeze }.freeze
17
+ @memo = memo
18
+ freeze
19
+ end
20
+
21
+ # @rbs (TokenMemo replacement) -> LexedSyntax
22
+ def with_memo(replacement)
23
+ LexedSyntax.new(raw_tokens: @raw_tokens, memo: replacement)
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+ # rbs_inline: enabled
3
+
4
+ module Ibex
5
+ module Runtime
6
+ module CST
7
+ # Parser-state metadata parallel to Green preorder occurrences.
8
+ class ParseMemo
9
+ VERSION = 1 #: Integer
10
+ ENTRY_BYTES = 8 #: Integer
11
+
12
+ attr_reader :left_states #: Array[Integer?]
13
+ attr_reader :grammar_digest #: String
14
+ attr_reader :state_count #: Integer
15
+ attr_reader :production_count #: Integer
16
+
17
+ # @rbs (left_states: Array[Integer?], grammar_digest: String, state_count: Integer,
18
+ # production_count: Integer) -> void
19
+ def initialize(left_states:, grammar_digest:, state_count:, production_count:)
20
+ unless left_states.all? { |state| state.nil? || state.between?(0, state_count - 1) }
21
+ raise ArgumentError, "parse memo contains an invalid parser state"
22
+ end
23
+
24
+ @left_states = left_states.dup.freeze
25
+ @grammar_digest = grammar_digest.dup.freeze
26
+ @state_count = state_count
27
+ @production_count = production_count
28
+ freeze
29
+ end
30
+
31
+ # @rbs (Integer preorder_index) -> Integer?
32
+ def left_state(preorder_index) = @left_states.fetch(preorder_index)
33
+
34
+ # @rbs (Integer preorder_index, GreenNode | GreenToken element) -> Array[Integer?]
35
+ def slice(preorder_index, element)
36
+ value = @left_states.slice(preorder_index, element.descendant_count)
37
+ return value if value && value.length == element.descendant_count
38
+
39
+ raise IndexError, "parse memo subtree range is outside the preorder state array"
40
+ end
41
+
42
+ # @rbs (Hash[Symbol, untyped] tables) -> bool
43
+ def compatible?(tables)
44
+ @grammar_digest == tables[:grammar_digest] &&
45
+ @state_count == tables[:state_count] &&
46
+ @production_count == tables[:production_count]
47
+ end
48
+
49
+ # @rbs () -> Integer
50
+ def estimated_bytes = @left_states.length * ENTRY_BYTES
51
+
52
+ # @rbs () -> Hash[String, untyped]
53
+ def to_h
54
+ { "version" => VERSION, "left_states" => @left_states }.freeze
55
+ end
56
+
57
+ # Lightweight construction tree flattened once when a parse completes.
58
+ class Entry
59
+ empty_children = [] # @type var empty_children: Array[Entry]
60
+ EMPTY_CHILDREN = empty_children.freeze #: Array[Entry]
61
+
62
+ # @rbs @state: Integer?
63
+ # @rbs @children: Array[Entry]
64
+ # @rbs @segment: Array[Integer?]?
65
+
66
+ # @rbs (Integer? state, ?children: Array[Entry], ?segment: Array[Integer?]?) -> void
67
+ def initialize(state, children: EMPTY_CHILDREN, segment: nil)
68
+ @state = state
69
+ @children = if children.empty?
70
+ EMPTY_CHILDREN
71
+ elsif children.frozen?
72
+ children
73
+ else
74
+ children.dup.freeze
75
+ end
76
+ @segment = segment&.then { |values| values.frozen? ? values : values.dup.freeze }
77
+ freeze
78
+ end
79
+
80
+ # @rbs (Array[Integer?] output) -> void
81
+ def append_to(output)
82
+ segment = @segment
83
+ if segment
84
+ output.concat(segment)
85
+ return
86
+ end
87
+
88
+ output << @state
89
+ @children.each { |child| child.append_to(output) }
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,134 @@
1
+ # frozen_string_literal: true
2
+ # rbs_inline: enabled
3
+
4
+ module Ibex
5
+ module Runtime
6
+ module CST
7
+ # Result of comparing one fresh lexical pass with the prior token memo.
8
+ class RelexResult
9
+ attr_reader :memo #: TokenMemo
10
+ attr_reader :reused_count #: Integer
11
+ attr_reader :scanned_count #: Integer
12
+ attr_reader :resynchronized_at #: Integer?
13
+
14
+ # @rbs (memo: TokenMemo, reused_count: Integer, scanned_count: Integer,
15
+ # resynchronized_at: Integer?) -> void
16
+ def initialize(memo:, reused_count:, scanned_count:, resynchronized_at:)
17
+ @memo = memo
18
+ @reused_count = reused_count
19
+ @scanned_count = scanned_count
20
+ @resynchronized_at = resynchronized_at
21
+ freeze
22
+ end
23
+
24
+ # @rbs () -> Float
25
+ def reused_ratio
26
+ return 0.0 if @memo.tokens.empty?
27
+
28
+ @reused_count.fdiv(@memo.tokens.length)
29
+ end
30
+ end
31
+
32
+ # Sound Stage-A comparison: scan from the beginning and reuse only exact
33
+ # token/state/boundary matches, then recognize a shifted suffix boundary.
34
+ class Relexer
35
+ # @rbs (TokenMemo previous, TokenMemo current, Array[TextEdit] edits) -> RelexResult
36
+ def self.reconcile(previous, current, edits)
37
+ normalized = TextEdit.normalize(edits)
38
+ return RelexResult.new(memo: current, reused_count: 0, scanned_count: 0, resynchronized_at: nil) if
39
+ normalized.empty?
40
+
41
+ damage, delta, resync_boundary = edit_window(normalized)
42
+ old_by_offset = offset_index(previous)
43
+ tokens = current.tokens.dup
44
+ reused, scanned, resynchronized_at = reuse_tokens(
45
+ previous, current, tokens, old_by_offset, damage, delta, resync_boundary
46
+ )
47
+ memo = TokenMemo.new(tokens: tokens, offsets: current.offsets, states: current.states)
48
+ RelexResult.new(
49
+ memo: memo,
50
+ reused_count: reused,
51
+ scanned_count: scanned,
52
+ resynchronized_at: resynchronized_at
53
+ )
54
+ end
55
+
56
+ # @rbs (Array[TextEdit] edits) -> [Integer, Integer, Integer]
57
+ def self.edit_window(edits)
58
+ delta = edits.sum { |edit| edit.insert_text.bytesize - edit.delete_length }
59
+ prefix_delta = edits.take(edits.length - 1).sum do |edit|
60
+ edit.insert_text.bytesize - edit.delete_length
61
+ end
62
+ last_edit = edits.last
63
+ [edits.first.start, delta, last_edit.start + prefix_delta + last_edit.insert_text.bytesize]
64
+ end
65
+
66
+ # @rbs (TokenMemo memo) -> Hash[Integer, Integer]
67
+ def self.offset_index(memo)
68
+ result = {} #: Hash[Integer, Integer]
69
+ memo.offsets.each_with_index { |offset, index| result[offset] = index }
70
+ result
71
+ end
72
+
73
+ # @rbs (TokenMemo previous, TokenMemo current, Array[GreenToken] tokens,
74
+ # Hash[Integer, Integer] old_by_offset, Integer damage, Integer delta, Integer resync_boundary) ->
75
+ # [Integer, Integer, Integer?]
76
+ def self.reuse_tokens(previous, current, tokens, old_by_offset, damage, delta, resync_boundary)
77
+ reused = 0
78
+ scanned = 0
79
+ current.tokens.each_index do |index|
80
+ new_offset = current.offsets.fetch(index)
81
+ suffix = reusable_suffix(
82
+ previous, current, index, new_offset, delta, resync_boundary, old_by_offset
83
+ )
84
+ if suffix
85
+ old_index, length = suffix
86
+ copy_suffix(tokens, previous.tokens, index, old_index, length)
87
+ return [reused + length, scanned, index]
88
+ end
89
+
90
+ old_index = index if new_offset < damage
91
+ scanned += 1
92
+ next unless old_index
93
+ next unless reusable_token?(previous, old_index, current, index)
94
+
95
+ tokens[index] = previous.tokens.fetch(old_index)
96
+ reused += 1
97
+ end
98
+ [reused, scanned, nil]
99
+ end
100
+
101
+ # @rbs (TokenMemo previous, TokenMemo current, Integer index, Integer new_offset, Integer delta,
102
+ # Integer boundary, Hash[Integer, Integer] old_by_offset) -> [Integer, Integer]?
103
+ def self.reusable_suffix(previous, current, index, new_offset, delta, boundary, old_by_offset)
104
+ return if new_offset < boundary
105
+
106
+ old_index = old_by_offset[new_offset - delta]
107
+ return unless old_index
108
+
109
+ length = current.tokens.length - index
110
+ return unless length == previous.tokens.length - old_index
111
+ return unless previous.states.fetch(old_index) == current.states.fetch(index)
112
+
113
+ [old_index, length]
114
+ end
115
+
116
+ # @rbs (Array[GreenToken] target, Array[GreenToken] source, Integer target_index,
117
+ # Integer source_index, Integer length) -> void
118
+ def self.copy_suffix(target, source, target_index, source_index, length)
119
+ length.times do |relative|
120
+ target[target_index + relative] = source.fetch(source_index + relative)
121
+ end
122
+ end
123
+
124
+ # @rbs (TokenMemo previous, Integer old_index, TokenMemo current, Integer new_index) -> bool
125
+ def self.reusable_token?(previous, old_index, current, new_index)
126
+ previous.states.fetch(old_index) == current.states.fetch(new_index) &&
127
+ previous.tokens.fetch(old_index) == current.tokens.fetch(new_index)
128
+ end
129
+ private_class_method :edit_window, :offset_index, :reuse_tokens, :reusable_suffix, :copy_suffix,
130
+ :reusable_token?
131
+ end
132
+ end
133
+ end
134
+ end