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.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +337 -0
- data/lib/ibex/runtime/ast_data.rb +23 -0
- data/lib/ibex/runtime/cst/annotation.rb +16 -0
- data/lib/ibex/runtime/cst/cursor.rb +68 -0
- data/lib/ibex/runtime/cst/diff.rb +80 -0
- data/lib/ibex/runtime/cst/editing.rb +56 -0
- data/lib/ibex/runtime/cst/editor.rb +94 -0
- data/lib/ibex/runtime/cst/green/builder.rb +211 -0
- data/lib/ibex/runtime/cst/green/cache.rb +150 -0
- data/lib/ibex/runtime/cst/green/node.rb +78 -0
- data/lib/ibex/runtime/cst/green/token.rb +91 -0
- data/lib/ibex/runtime/cst/green/trivia.rb +36 -0
- data/lib/ibex/runtime/cst/incremental/blender.rb +233 -0
- data/lib/ibex/runtime/cst/incremental/lexed_syntax.rb +28 -0
- data/lib/ibex/runtime/cst/incremental/parse_memo.rb +95 -0
- data/lib/ibex/runtime/cst/incremental/relexer.rb +134 -0
- data/lib/ibex/runtime/cst/incremental/session.rb +229 -0
- data/lib/ibex/runtime/cst/incremental/token_memo.rb +66 -0
- data/lib/ibex/runtime/cst/kind.rb +133 -0
- data/lib/ibex/runtime/cst/parse_result.rb +41 -0
- data/lib/ibex/runtime/cst/rewriter.rb +75 -0
- data/lib/ibex/runtime/cst/serialize.rb +127 -0
- data/lib/ibex/runtime/cst/serialized_tree.rb +44 -0
- data/lib/ibex/runtime/cst/source_text.rb +112 -0
- data/lib/ibex/runtime/cst/syntax_node.rb +432 -0
- data/lib/ibex/runtime/cst/syntax_token.rb +160 -0
- data/lib/ibex/runtime/cst/text_edit.rb +77 -0
- data/lib/ibex/runtime/cst/typed_node.rb +87 -0
- data/lib/ibex/runtime/cst/validator.rb +428 -0
- data/lib/ibex/runtime/cst.rb +38 -0
- data/lib/ibex/runtime/event.rb +44 -0
- data/lib/ibex/runtime/event_jsonl_tracer.rb +37 -0
- data/lib/ibex/runtime/event_sanitizer.rb +273 -0
- data/lib/ibex/runtime/generated_lexer.rb +514 -0
- data/lib/ibex/runtime/jsonl_tracer.rb +75 -0
- data/lib/ibex/runtime/lexer_input.rb +107 -0
- data/lib/ibex/runtime/location_span.rb +114 -0
- data/lib/ibex/runtime/observation.rb +82 -0
- data/lib/ibex/runtime/parser.rb +3027 -0
- data/lib/ibex/runtime/parser_sync_recovery.rb +108 -0
- data/lib/ibex/runtime/repair.rb +126 -0
- data/lib/ibex/runtime/repair_priority_queue.rb +60 -0
- data/lib/ibex/runtime/repair_search.rb +318 -0
- data/lib/ibex/runtime/resource_limits.rb +64 -0
- data/lib/ibex/runtime/table_format.rb +11 -0
- data/lib/ibex/runtime/version.rb +7 -0
- data/lib/ibex/runtime.rb +27 -0
- data/lib/ibex/tables/compact.rb +186 -0
- data/lib/ibex/tables/compact_actions.rb +135 -0
- data/lib/ibex/tables/compact_productions.rb +167 -0
- data/sig/ibex/runtime/ast_data.rbs +12 -0
- data/sig/ibex/runtime/cst/annotation.rbs +13 -0
- data/sig/ibex/runtime/cst/cursor.rbs +34 -0
- data/sig/ibex/runtime/cst/diff.rbs +29 -0
- data/sig/ibex/runtime/cst/editing.rbs +23 -0
- data/sig/ibex/runtime/cst/editor.rbs +43 -0
- data/sig/ibex/runtime/cst/green/builder.rbs +84 -0
- data/sig/ibex/runtime/cst/green/cache.rbs +60 -0
- data/sig/ibex/runtime/cst/green/node.rbs +49 -0
- data/sig/ibex/runtime/cst/green/token.rbs +59 -0
- data/sig/ibex/runtime/cst/green/trivia.rbs +29 -0
- data/sig/ibex/runtime/cst/incremental/blender.rbs +114 -0
- data/sig/ibex/runtime/cst/incremental/lexed_syntax.rbs +20 -0
- data/sig/ibex/runtime/cst/incremental/parse_memo.rbs +59 -0
- data/sig/ibex/runtime/cst/incremental/relexer.rbs +54 -0
- data/sig/ibex/runtime/cst/incremental/session.rbs +89 -0
- data/sig/ibex/runtime/cst/incremental/token_memo.rbs +31 -0
- data/sig/ibex/runtime/cst/kind.rbs +93 -0
- data/sig/ibex/runtime/cst/parse_result.rbs +35 -0
- data/sig/ibex/runtime/cst/rewriter.rbs +31 -0
- data/sig/ibex/runtime/cst/serialize.rbs +39 -0
- data/sig/ibex/runtime/cst/serialized_tree.rbs +34 -0
- data/sig/ibex/runtime/cst/source_text.rbs +51 -0
- data/sig/ibex/runtime/cst/syntax_node.rbs +195 -0
- data/sig/ibex/runtime/cst/syntax_token.rbs +112 -0
- data/sig/ibex/runtime/cst/text_edit.rbs +33 -0
- data/sig/ibex/runtime/cst/typed_node.rbs +50 -0
- data/sig/ibex/runtime/cst/validator.rbs +108 -0
- data/sig/ibex/runtime/cst.rbs +9 -0
- data/sig/ibex/runtime/event.rbs +26 -0
- data/sig/ibex/runtime/event_jsonl_tracer.rbs +25 -0
- data/sig/ibex/runtime/event_sanitizer.rbs +92 -0
- data/sig/ibex/runtime/generated_lexer.rbs +169 -0
- data/sig/ibex/runtime/jsonl_tracer.rbs +42 -0
- data/sig/ibex/runtime/lexer_input.rbs +44 -0
- data/sig/ibex/runtime/location_span.rbs +64 -0
- data/sig/ibex/runtime/observation.rbs +35 -0
- data/sig/ibex/runtime/parser.rbs +926 -0
- data/sig/ibex/runtime/parser_sync_recovery.rbs +60 -0
- data/sig/ibex/runtime/repair.rbs +92 -0
- data/sig/ibex/runtime/repair_priority_queue.rbs +27 -0
- data/sig/ibex/runtime/repair_search.rbs +114 -0
- data/sig/ibex/runtime/resource_limits.rbs +39 -0
- data/sig/ibex/runtime/table_format.rbs +11 -0
- data/sig/ibex/runtime/version.rbs +7 -0
- data/sig/ibex/runtime.rbs +6 -0
- data/sig/ibex/tables/compact.rbs +70 -0
- data/sig/ibex/tables/compact_actions.rbs +57 -0
- data/sig/ibex/tables/compact_productions.rbs +68 -0
- 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
|