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,44 @@
1
+ # frozen_string_literal: true
2
+ # rbs_inline: enabled
3
+
4
+ module Ibex
5
+ module Runtime
6
+ module CST
7
+ # Validated CST serialization document and its reconstructed Red root.
8
+ class SerializedTree
9
+ attr_reader :grammar_digest #: String
10
+ attr_reader :table_format #: Integer
11
+ attr_reader :state_count #: Integer
12
+ attr_reader :production_count #: Integer
13
+ attr_reader :trivia_policy #: Symbol
14
+ attr_reader :kinds #: Kind
15
+ attr_reader :green_root #: GreenNode
16
+ attr_reader :memo #: ParseMemo?
17
+
18
+ # @rbs (grammar_digest: String, table_format: Integer, state_count: Integer,
19
+ # production_count: Integer, trivia_policy: Symbol, kinds: Kind, green_root: GreenNode,
20
+ # ?memo: ParseMemo?) -> void
21
+ def initialize(grammar_digest:, table_format:, state_count:, production_count:, trivia_policy:, kinds:,
22
+ green_root:, memo: nil)
23
+ @grammar_digest = grammar_digest.dup.freeze
24
+ @table_format = table_format
25
+ @state_count = state_count
26
+ @production_count = production_count
27
+ @trivia_policy = trivia_policy
28
+ @kinds = kinds
29
+ @green_root = green_root
30
+ @memo = memo
31
+ freeze
32
+ end
33
+
34
+ # @rbs () -> SyntaxNode
35
+ def syntax_root
36
+ source = SourceText.new(@green_root.to_source)
37
+ SyntaxNode.new(
38
+ green: @green_root, kinds: @kinds, trivia_policy: @trivia_policy, source_text: source
39
+ )
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+ # rbs_inline: enabled
3
+
4
+ module Ibex
5
+ module Runtime
6
+ module CST
7
+ # Immutable byte-oriented source with lazy line/column conversion.
8
+ class SourceText
9
+ # @rbs! type position = [Integer, Integer]
10
+
11
+ attr_reader :text #: String
12
+ attr_reader :file #: String?
13
+
14
+ # @rbs (String text, ?file: String?) -> void
15
+ def initialize(text, file: nil)
16
+ @text = text.encoding == Encoding::BINARY && text.frozen? ? text : text.b.freeze
17
+ @file = file&.then { |value| value.frozen? ? value : value.dup.freeze }
18
+ @line_starts = build_line_starts.freeze #: Array[Integer]
19
+ freeze
20
+ end
21
+
22
+ # @rbs () -> Integer
23
+ def bytesize = @text.bytesize
24
+
25
+ # Apply non-overlapping byte edits expressed against this source.
26
+ # @rbs (Array[TextEdit] edits) -> SourceText
27
+ def apply(edits)
28
+ ordered = TextEdit.normalize(edits)
29
+ output = String.new(encoding: Encoding::BINARY)
30
+ cursor = 0
31
+ ordered.each do |edit|
32
+ raise ArgumentError, "text edits overlap" if edit.start < cursor
33
+ raise RangeError, "text edit exceeds source" if edit.range.end > @text.bytesize
34
+
35
+ output << (@text.byteslice(cursor, edit.start - cursor) || "".b)
36
+ output << edit.insert_text
37
+ cursor = edit.range.end
38
+ end
39
+ output << (@text.byteslice(cursor, @text.bytesize - cursor) || "".b)
40
+ SourceText.new(output, file: @file)
41
+ end
42
+
43
+ # Return one-based line and Unicode-scalar column for a byte offset.
44
+ # Invalid UTF-8 bytes count as one replacement scalar each.
45
+ # @rbs (Integer offset) -> position
46
+ def position(offset)
47
+ validate_offset(offset)
48
+ line_index = @line_starts.bsearch_index { |start| start > offset }
49
+ line_index = line_index ? line_index - 1 : @line_starts.length - 1
50
+ line_start = @line_starts.fetch(line_index)
51
+ prefix = @text.byteslice(line_start, offset - line_start) || "".b
52
+ value = [line_index + 1, unicode_scalar_length(prefix) + 1] #: position
53
+ value.freeze
54
+ end
55
+
56
+ # @rbs (Range[Integer] range) -> Ibex::Location
57
+ def location(range)
58
+ start_offset, end_offset = normalized_range(range)
59
+ line, column = position(start_offset)
60
+ end_line, end_column = position(end_offset)
61
+ Ibex::Location.new(
62
+ file: @file, line: line, column: column, end_line: end_line, end_column: end_column,
63
+ start_byte: start_offset, end_byte: end_offset, source_line: line_text(line)
64
+ )
65
+ end
66
+
67
+ # @rbs (Integer line) -> String
68
+ def line_text(line)
69
+ raise ArgumentError, "line must be positive" unless line.positive?
70
+
71
+ start_offset = @line_starts.fetch(line - 1)
72
+ end_offset = @line_starts.fetch(line, @text.bytesize)
73
+ value = @text.byteslice(start_offset, end_offset - start_offset) || "".b
74
+ value.delete_suffix("\n".b).delete_suffix("\r".b)
75
+ end
76
+
77
+ private
78
+
79
+ # @rbs () -> Array[Integer]
80
+ def build_line_starts
81
+ starts = [0]
82
+ @text.bytes.each_with_index { |byte, index| starts << (index + 1) if byte == 10 }
83
+ starts
84
+ end
85
+
86
+ # @rbs (Integer offset) -> void
87
+ def validate_offset(offset)
88
+ return if offset.between?(0, @text.bytesize)
89
+
90
+ raise RangeError, "source offset #{offset} is outside 0..#{@text.bytesize}"
91
+ end
92
+
93
+ # @rbs (Range[Integer] range) -> position
94
+ def normalized_range(range)
95
+ start_offset = range.begin
96
+ end_offset = range.end + (range.exclude_end? ? 0 : 1)
97
+ validate_offset(start_offset)
98
+ validate_offset(end_offset)
99
+ raise RangeError, "range end precedes start" if end_offset < start_offset
100
+
101
+ value = [start_offset, end_offset] #: position
102
+ value.freeze
103
+ end
104
+
105
+ # @rbs (String bytes) -> Integer
106
+ def unicode_scalar_length(bytes)
107
+ bytes.dup.force_encoding(Encoding::UTF_8).scrub.length
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,432 @@
1
+ # frozen_string_literal: true
2
+ # rbs_inline: enabled
3
+
4
+ require_relative "source_text" unless defined?(Ibex::Runtime::CST::SourceText)
5
+ require_relative "syntax_token" unless defined?(Ibex::Runtime::CST::SyntaxToken)
6
+
7
+ module Ibex
8
+ module Runtime
9
+ module CST
10
+ # Lazy Red navigation facade for one Green node occurrence.
11
+ class SyntaxNode # rubocop:disable Metrics/ClassLength -- navigation and compatibility form one facade.
12
+ # @rbs! type element = SyntaxNode | SyntaxToken
13
+ # @rbs! include Enumerable[element]
14
+ # @rbs skip
15
+ include Enumerable
16
+
17
+ attr_reader :green #: GreenNode
18
+ attr_reader :parent #: SyntaxNode?
19
+ attr_reader :index #: Integer
20
+ attr_reader :offset #: Integer
21
+ attr_reader :kinds #: Kind
22
+ attr_reader :trivia_policy #: Symbol
23
+ attr_reader :source_text #: SourceText
24
+
25
+ # @rbs (green: GreenNode, kinds: Kind, ?parent: SyntaxNode?, ?index: Integer, ?offset: Integer,
26
+ # ?trivia_policy: Symbol, ?source_text: SourceText?) -> void
27
+ def initialize(green:, kinds:, parent: nil, index: 0, offset: 0, trivia_policy: :leading, source_text: nil)
28
+ @green = green
29
+ @kinds = kinds
30
+ @parent = parent
31
+ @index = index
32
+ @offset = offset
33
+ @trivia_policy = trivia_policy
34
+ @source_text = source_text || SourceText.new(root_source)
35
+ @children = Array.new(@green.children.length) #: Array[element?]
36
+ end
37
+
38
+ # @rbs () -> Integer
39
+ def kind = @green.kind
40
+
41
+ # @rbs () -> String
42
+ def kind_name = @kinds.name(kind)
43
+
44
+ # Compatibility name for the physical grammar symbol.
45
+ # @rbs () -> String
46
+ def symbol = @kinds.name(@kinds.nonterminal_of(kind))
47
+
48
+ # Green kinds replace occurrence-local production ids. Keep the legacy
49
+ # key and reader with the historical synthetic sentinel.
50
+ # @rbs () -> Integer
51
+ def production_id = -1
52
+
53
+ # @rbs () -> Array[element]
54
+ def children
55
+ @green.children.each_index.map { |child_index| child_at(child_index) }.freeze
56
+ end
57
+
58
+ # @rbs!
59
+ # def each: () -> Enumerator[element, self]
60
+ # | () { (element) -> void } -> self
61
+ # @rbs skip
62
+ def each(&block)
63
+ return enum_for(:each) unless block
64
+
65
+ children.each(&block)
66
+ self
67
+ end
68
+
69
+ # @rbs (Integer child_index) -> element
70
+ def child_at(child_index)
71
+ cached = @children.fetch(child_index)
72
+ return cached if cached
73
+
74
+ green_child = @green.children.fetch(child_index)
75
+ child_offset = offset_for(child_index)
76
+ value = if green_child.is_a?(GreenNode)
77
+ self.class.new(
78
+ green: green_child, kinds: @kinds, parent: self, index: child_index,
79
+ offset: child_offset, trivia_policy: @trivia_policy, source_text: @source_text
80
+ )
81
+ else
82
+ SyntaxToken.new(green: green_child, parent: self, index: child_index, offset: child_offset)
83
+ end
84
+ @children[child_index] = value
85
+ end
86
+
87
+ # @rbs () -> Array[SyntaxNode]
88
+ def child_nodes
89
+ nodes = [] #: Array[SyntaxNode]
90
+ children.each { |child| nodes << child if child.is_a?(SyntaxNode) }
91
+ nodes.freeze
92
+ end
93
+
94
+ # @rbs () -> Array[SyntaxToken]
95
+ def tokens
96
+ values = [] #: Array[SyntaxToken]
97
+ descendants.each { |element| values << element if element.is_a?(SyntaxToken) }
98
+ values.freeze
99
+ end
100
+
101
+ # @rbs () -> SyntaxToken?
102
+ def first_token = tokens.first
103
+
104
+ # @rbs () -> SyntaxToken?
105
+ def last_token = tokens.last
106
+
107
+ # @rbs () -> element?
108
+ def next_sibling
109
+ parent = @parent
110
+ return unless parent
111
+
112
+ parent.children[@index + 1]
113
+ end
114
+
115
+ # @rbs () -> element?
116
+ def prev_sibling
117
+ parent = @parent
118
+ return unless parent
119
+ return if @index.zero?
120
+
121
+ parent.children[@index - 1]
122
+ end
123
+
124
+ # @rbs () -> Enumerator[SyntaxNode, void]
125
+ def ancestors
126
+ Enumerator.new do |yielder|
127
+ ancestor = @parent
128
+ while ancestor
129
+ yielder << ancestor
130
+ ancestor = ancestor.parent
131
+ end
132
+ end
133
+ end
134
+
135
+ # @rbs () -> Enumerator[element, void]
136
+ def descendants
137
+ Enumerator.new do |yielder|
138
+ visit = lambda do |node|
139
+ node.children.each do |child|
140
+ yielder << child
141
+ visit.call(child) if child.is_a?(SyntaxNode)
142
+ end
143
+ end
144
+ visit.call(self)
145
+ end
146
+ end
147
+
148
+ # @rbs () -> SyntaxNode
149
+ def root
150
+ parent = @parent
151
+ parent ? parent.root : self
152
+ end
153
+
154
+ # @rbs () -> Range[Integer]
155
+ def full_span
156
+ ensure_coordinates!
157
+ @offset...(@offset + @green.full_width)
158
+ end
159
+
160
+ # @rbs () -> Range[Integer]
161
+ def span
162
+ ensure_coordinates!
163
+ (@offset + @green.leading_width)...(@offset + @green.full_width - @green.trailing_width)
164
+ end
165
+
166
+ # @rbs () -> Ibex::Location
167
+ def location = @source_text.location(span)
168
+
169
+ # @rbs () -> String
170
+ def full_text = @green.to_source
171
+ alias to_source full_text
172
+
173
+ # @rbs () -> String
174
+ def text
175
+ start_offset = @green.leading_width
176
+ width = @green.full_width - @green.leading_width - @green.trailing_width
177
+ full_text.byteslice(start_offset, width) || "".b
178
+ end
179
+
180
+ # Compatibility view of file-tail trivia. The Red/Green layout owns it
181
+ # on EOF rather than on the start node.
182
+ # @rbs () -> Array[GreenTrivia]
183
+ def trailing_trivia
184
+ eof = compatibility_eof
185
+ return (eof.green.leading + eof.green.trailing).freeze if eof
186
+
187
+ token = last_token
188
+ token ? token.green.trailing : []
189
+ end
190
+
191
+ # @rbs () -> bool
192
+ def error? = @kinds.error?(kind)
193
+
194
+ # @rbs () -> bool
195
+ def missing? = false
196
+
197
+ # @rbs () -> bool
198
+ def contains_error? = @green.flags.anybits?(Flags::CONTAINS_ERROR)
199
+
200
+ # @rbs () -> bool
201
+ def incomplete_input? = @green.flags.anybits?(Flags::INCOMPLETE_INPUT)
202
+
203
+ # @rbs (GreenNode | GreenToken | SyntaxNode | SyntaxToken replacement) -> SyntaxNode
204
+ def replace_with(replacement) = Editing.replace(self, replacement)
205
+
206
+ # @rbs (Integer child_index, GreenNode | GreenToken | SyntaxNode | SyntaxToken child) -> SyntaxNode
207
+ def with_child(child_index, child)
208
+ children = @green.children.dup
209
+ children.fetch(child_index)
210
+ children[child_index] = Editing.green_element(child)
211
+ replace_with(
212
+ GreenNode.new(
213
+ kind: @green.kind, children: children,
214
+ flags: @green.intrinsic_flags, annotations: @green.annotations
215
+ )
216
+ )
217
+ end
218
+
219
+ # @rbs (Integer child_index, GreenNode | GreenToken | SyntaxNode | SyntaxToken child) -> SyntaxNode
220
+ def insert_child(child_index, child)
221
+ raise IndexError, "child index #{child_index} is outside 0..#{@green.children.length}" unless
222
+ child_index.between?(0, @green.children.length)
223
+
224
+ children = @green.children.dup
225
+ children.insert(child_index, Editing.green_element(child))
226
+ replace_with(
227
+ GreenNode.new(
228
+ kind: @green.kind, children: children,
229
+ flags: @green.intrinsic_flags, annotations: @green.annotations
230
+ )
231
+ )
232
+ end
233
+
234
+ # @rbs (Integer child_index) -> SyntaxNode
235
+ def remove_child(child_index)
236
+ children = @green.children.dup
237
+ children.fetch(child_index)
238
+ children.delete_at(child_index)
239
+ replace_with(
240
+ GreenNode.new(
241
+ kind: @green.kind, children: children,
242
+ flags: @green.intrinsic_flags, annotations: @green.annotations
243
+ )
244
+ )
245
+ end
246
+
247
+ # @rbs (SyntaxAnnotation annotation) -> SyntaxNode
248
+ def annotate(annotation)
249
+ raise TypeError, "annotation must be a SyntaxAnnotation" unless annotation.is_a?(SyntaxAnnotation)
250
+ return root if @green.annotations.include?(annotation)
251
+
252
+ replace_with(
253
+ GreenNode.new(
254
+ kind: @green.kind, children: @green.children,
255
+ flags: @green.intrinsic_flags, annotations: @green.annotations + [annotation]
256
+ )
257
+ )
258
+ end
259
+
260
+ # @rbs (SyntaxAnnotation annotation) -> Enumerator[SyntaxNode, void]
261
+ def annotated(annotation)
262
+ Enumerator.new do |yielder|
263
+ visit = lambda do |node|
264
+ yielder << node if node.green.annotations.include?(annotation)
265
+ node.child_nodes.each { |child| visit.call(child) }
266
+ end
267
+ visit.call(self)
268
+ end
269
+ end
270
+
271
+ # Find the full-span-owning token for a byte offset.
272
+ # @rbs (Integer source_offset) -> SyntaxToken?
273
+ def token_at(source_offset)
274
+ ensure_coordinates!
275
+ return unless source_offset >= @offset && source_offset < @offset + @green.full_width
276
+
277
+ node = self
278
+ loop do
279
+ child = node.child_covering_offset(source_offset)
280
+ return unless child
281
+ return child if child.is_a?(SyntaxToken)
282
+
283
+ node = child
284
+ end
285
+ end
286
+
287
+ # Return the smallest syntax element whose full span covers a range.
288
+ # @rbs (Range[Integer] range) -> element?
289
+ def covering(range)
290
+ ensure_coordinates!
291
+ start_offset, end_offset = normalize_range(range)
292
+ return unless covers_offsets?(start_offset, end_offset)
293
+
294
+ node = self
295
+ loop do
296
+ child = node.children.find do |candidate|
297
+ candidate_start = candidate.offset
298
+ candidate_end = candidate.offset + candidate.green.full_width
299
+ candidate_start <= start_offset && candidate_end >= end_offset
300
+ end
301
+ return node unless child
302
+ return child if child.is_a?(SyntaxToken)
303
+
304
+ node = child
305
+ end
306
+ end
307
+
308
+ # @rbs (kind: Integer | String | Symbol) -> Enumerator[element, void]
309
+ def find(kind:)
310
+ expected = kind.is_a?(Integer) ? kind : @kinds.fetch(kind)
311
+ Enumerator.new do |yielder|
312
+ descendants.each { |element| yielder << element if element.kind == expected }
313
+ end
314
+ end
315
+
316
+ # @rbs () -> Enumerator[element, void]
317
+ # @rbs () { (element) -> void } -> self
318
+ def each_error(&block)
319
+ errors = Enumerator.new do |yielder|
320
+ descendants.each do |element|
321
+ yielder << element if element.error? || element.missing?
322
+ end
323
+ end #: Enumerator[element, void]
324
+ return errors unless block
325
+
326
+ errors.each(&block)
327
+ self
328
+ end
329
+
330
+ # @rbs () { (Symbol, element) -> void } -> self
331
+ def walk(&block)
332
+ raise ArgumentError, "walk requires a block" unless block
333
+
334
+ visit = lambda do |element|
335
+ block.call(:enter, element)
336
+ element.children.each { |child| visit.call(child) } if element.is_a?(SyntaxNode)
337
+ block.call(:leave, element)
338
+ end
339
+ visit.call(self)
340
+ self
341
+ end
342
+
343
+ # @rbs () -> Cursor
344
+ def cursor = Cursor.new(self)
345
+
346
+ # @rbs (SyntaxNode other) -> bool
347
+ def same_node?(other)
348
+ root.equal?(other.root) && @green.equal?(other.green) && @offset == other.offset
349
+ end
350
+
351
+ # @rbs (untyped other) -> bool
352
+ def ==(other)
353
+ other.is_a?(SyntaxNode) && @green == other.green
354
+ end
355
+
356
+ # @rbs () -> Array[element]
357
+ def deconstruct = children
358
+
359
+ # @rbs (Array[Symbol]?) -> Hash[Symbol, untyped]
360
+ def deconstruct_keys(_keys)
361
+ values = {
362
+ kind: :node, symbol: symbol, production_id: production_id, children: children,
363
+ location: location, trailing_trivia: trailing_trivia
364
+ } #: Hash[Symbol, untyped]
365
+ @kinds.fields(kind).each do |name, slot|
366
+ index = slot.is_a?(Hash) ? slot.fetch(:index) : slot
367
+ values[name.to_sym] = child_at(index)
368
+ end
369
+ values.freeze
370
+ end
371
+
372
+ # @rbs () -> Hash[Symbol, untyped]
373
+ def to_h = deconstruct_keys(nil)
374
+
375
+ protected
376
+
377
+ # @rbs (Integer source_offset) -> element?
378
+ def child_covering_offset(source_offset)
379
+ children.find do |child|
380
+ source_offset >= child.offset && source_offset < child.offset + child.green.full_width
381
+ end
382
+ end
383
+
384
+ private
385
+
386
+ # @rbs () -> SyntaxToken?
387
+ def compatibility_eof
388
+ if kind_name == "source_file"
389
+ token = last_token
390
+ return token if token&.kind_name == "$eof"
391
+ end
392
+ container = @parent
393
+ return unless container
394
+ return unless container.kind_name == "source_file"
395
+ return unless @index.zero?
396
+
397
+ token = container.last_token
398
+ token if token&.kind_name == "$eof"
399
+ end
400
+
401
+ # @rbs () -> String
402
+ def root_source
403
+ parent = @parent
404
+ parent ? parent.source_text.text : @green.to_source
405
+ end
406
+
407
+ # @rbs (Integer child_index) -> Integer
408
+ def offset_for(child_index)
409
+ @offset + @green.children.first(child_index).sum(&:full_width)
410
+ end
411
+
412
+ # @rbs () -> void
413
+ def ensure_coordinates!
414
+ return unless @trivia_policy == :drop
415
+
416
+ raise TriviaDroppedError, "source coordinates are unavailable when CST trivia is dropped"
417
+ end
418
+
419
+ # @rbs (Range[Integer] range) -> [Integer, Integer]
420
+ def normalize_range(range)
421
+ value = [range.begin, range.end + (range.exclude_end? ? 0 : 1)] #: [Integer, Integer]
422
+ value.freeze
423
+ end
424
+
425
+ # @rbs (Integer start_offset, Integer end_offset) -> bool
426
+ def covers_offsets?(start_offset, end_offset)
427
+ start_offset >= @offset && end_offset <= @offset + @green.full_width && end_offset >= start_offset
428
+ end
429
+ end
430
+ end
431
+ end
432
+ end