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,514 @@
1
+ # frozen_string_literal: true
2
+ # rbs_inline: enabled
3
+
4
+ require_relative "lexer_input" unless defined?(Ibex::Runtime::LexerInput)
5
+
6
+ module Ibex
7
+ module Runtime
8
+ # Runtime contract mixed into parser classes that declare a lexer.
9
+ # rubocop:disable Metrics/ModuleLength -- matching, actions, state, and positions share one session invariant.
10
+ # @rbs module-self Parser
11
+ module GeneratedLexer
12
+ NO_EMISSION = Object.new.freeze #: Object
13
+ empty_green_trivia = [] # @type var empty_green_trivia: Array[CST::GreenTrivia]
14
+ EMPTY_GREEN_TRIVIA = empty_green_trivia.freeze #: Array[CST::GreenTrivia]
15
+
16
+ # @rbs @lexer_input: LexerInput?
17
+ # @rbs @lexer_file: String
18
+ # @rbs @lexer_states: Array[String]
19
+ # @rbs @lexer_line: Integer
20
+ # @rbs @lexer_column: Integer
21
+ # @rbs @lexer_byte_column: Integer
22
+ # @rbs @lexer_byte_offset: Integer
23
+ # @rbs @lexer_lexeme: String?
24
+ # @rbs @lexer_emission: Object | Array[untyped]
25
+ # @rbs @lexer_skip_requested: bool
26
+ # @rbs @lexer_pending_green_trivia: Array[CST::GreenTrivia]
27
+ # @rbs @lexer_cst_trivia_policy: Symbol
28
+ # @rbs @lexer_cst_trivia_kinds: Hash[String, Integer]?
29
+ # @rbs @lexer_has_token: bool
30
+
31
+ # Reset the generated lexer to the beginning of an input.
32
+ # @rbs (String | IO | Fiber source, ?file: String) -> self
33
+ def lex(source, file: "(input)")
34
+ @lexer_input = LexerInput.new(source)
35
+ @lexer_file = file
36
+ @lexer_states = ["INITIAL"]
37
+ @lexer_line = 1
38
+ @lexer_column = 1
39
+ @lexer_byte_column = 1
40
+ @lexer_byte_offset = 0
41
+ @lexer_lexeme = nil
42
+ @lexer_emission = NO_EMISSION
43
+ @lexer_skip_requested = false
44
+ @lexer_pending_green_trivia = []
45
+ configure_lexer_cst
46
+ @lexer_has_token = false
47
+ self
48
+ end
49
+
50
+ # Lex and parse one String, IO, or Fiber source.
51
+ # @rbs (String | IO | Fiber source, ?file: String) -> untyped
52
+ def parse(source, file: "(input)")
53
+ lex(source, file: file)
54
+ parser = self #: Parser
55
+ parser.do_parse
56
+ rescue ParseError => e
57
+ raise unless parser_tables[:cst].is_a?(Hash)
58
+
59
+ parser = self #: Parser
60
+ parser.__send__(:cst_lexical_failure, e)
61
+ end
62
+
63
+ # Parse one generated-lexer source and return its semantic and syntax results.
64
+ # @rbs (String | IO | Fiber source, ?file: String) -> CST::ParseResult
65
+ def parse_with_syntax(source, file: "(input)")
66
+ lex(source, file: file)
67
+ parser = self #: Parser
68
+ value = parser.do_parse
69
+ parser.__send__(:syntax_parse_result, value)
70
+ rescue ParseError => e
71
+ raise unless parser_tables[:cst].is_a?(Hash)
72
+
73
+ parser = self #: Parser
74
+ value = parser.__send__(:cst_lexical_failure, e)
75
+ parser.__send__(:syntax_parse_result, value)
76
+ end
77
+
78
+ # Parse without executing parser production actions.
79
+ # Lexer actions still run because they define tokenization and lexer state.
80
+ # @rbs (String source, ?file: String) -> CST::SyntaxResult
81
+ def parse_syntax(source, file: "(input)")
82
+ parse_syntax_with_cache(CST::SourceText.new(source, file: file), CST::NodeCache.new)
83
+ end
84
+
85
+ # Return the current named lexer state.
86
+ # @rbs () -> Symbol
87
+ def lexer_state
88
+ (@lexer_states || ["INITIAL"]).last.to_sym
89
+ end
90
+
91
+ # Replace the current named lexer state.
92
+ # @rbs (Symbol | String state) -> Symbol
93
+ def lexer_state=(state)
94
+ name = validate_lexer_state(state)
95
+ states = @lexer_states ||= ["INITIAL"]
96
+ states[-1] = name
97
+ name.to_sym
98
+ end
99
+
100
+ # Pull one token using longest match and declaration-order tie breaking.
101
+ # @rbs () -> [untyped, untyped, Hash[Symbol, untyped]]
102
+ def next_token
103
+ input = @lexer_input
104
+ raise ParseError, "(lexer):1:1: call parse or lex before next_token" unless input
105
+
106
+ loop do
107
+ unless ensure_lexer_data?(input)
108
+ location = lexer_zero_width_location
109
+ location[:ibex_lexer_start_state] = lexer_state
110
+ location = attach_cst_trivia(location) if cst_enabled?
111
+ return [nil, nil, location.freeze]
112
+ end
113
+
114
+ rule, lexeme = select_lexer_match(input)
115
+ raise_lexer_no_match(input) unless rule && lexeme
116
+
117
+ location = consume_lexer_match(input, lexeme)
118
+ location[:ibex_lexer_start_state] = lexer_state
119
+ emitted = apply_lexer_rule(rule, lexeme, location)
120
+ return emitted if emitted
121
+ end
122
+ end
123
+
124
+ private
125
+
126
+ # @rbs (CST::SourceText source_text, CST::NodeCache cache) -> CST::SyntaxResult
127
+ def parse_syntax_with_cache(source_text, cache)
128
+ parser = self #: Parser
129
+ parser.__send__(:with_syntax_only, cache) do
130
+ lex(source_text.text, file: source_text.file || "(input)")
131
+ value = begin
132
+ parser.do_parse
133
+ rescue ParseError => e
134
+ raise unless parser_tables[:cst].is_a?(Hash)
135
+
136
+ parser.__send__(:cst_lexical_failure, e)
137
+ end
138
+ parsed = parser.__send__(:syntax_parse_result, value)
139
+ CST::SyntaxResult.new(
140
+ syntax_root: parsed.syntax_root,
141
+ diagnostics: parsed.diagnostics,
142
+ reused_ratio: 0.0
143
+ )
144
+ end
145
+ end
146
+
147
+ # Run the generated lexer once and materialize its exact Green token stream.
148
+ # @rbs (CST::SourceText source_text, CST::NodeCache cache) -> CST::LexedSyntax
149
+ def scan_syntax_with_cache(source_text, cache)
150
+ lex(source_text.text, file: source_text.file || "(input)")
151
+ raw_tokens = [] #: Array[Array[untyped]]
152
+ green_tokens = [] #: Array[CST::GreenToken]
153
+ states = [] #: Array[Symbol]
154
+ loop do
155
+ external, value, location = next_token
156
+ replace_previous_scanned_trailing(green_tokens, location, cache)
157
+ token_id = external.nil? || external == false ? Parser::EOF_TOKEN : __send__(:internal_token_id, external)
158
+ green = scanned_green_token(token_id, value, location, cache)
159
+ raw_tokens << [external, value, location]
160
+ green_tokens << green
161
+ state = location[:ibex_lexer_start_state] if location.is_a?(Hash)
162
+ states << (state ? state.to_sym : :INITIAL)
163
+ break if token_id == Parser::EOF_TOKEN
164
+ end
165
+ offsets = [] #: Array[Integer]
166
+ offset = 0
167
+ green_tokens.each do |token|
168
+ offsets << offset
169
+ offset += token.full_width
170
+ end
171
+ memo = CST::TokenMemo.new(tokens: green_tokens, offsets: offsets, states: states)
172
+ CST::LexedSyntax.new(raw_tokens: raw_tokens, memo: memo)
173
+ end
174
+
175
+ # @rbs (Integer token_id, untyped value, untyped location, CST::NodeCache cache) -> CST::GreenToken
176
+ def scanned_green_token(token_id, value, location, cache)
177
+ leading = scanned_green_trivia(location, :leading_trivia)
178
+ text = if location.is_a?(Hash) && location[:ibex_cst_text].is_a?(String)
179
+ location.fetch(:ibex_cst_text)
180
+ elsif value.is_a?(String)
181
+ value
182
+ else
183
+ ""
184
+ end
185
+ text = "" if token_id == Parser::EOF_TOKEN
186
+ cache.intern_token_fields(kind: token_id, text: text, leading: leading)
187
+ end
188
+
189
+ # @rbs (Array[CST::GreenToken] tokens, untyped location, CST::NodeCache cache) -> void
190
+ def replace_previous_scanned_trailing(tokens, location, cache)
191
+ trailing = scanned_green_trivia(location, :cst_previous_trailing)
192
+ previous = tokens.last
193
+ return if trailing.empty? || !previous
194
+
195
+ tokens[-1] = cache.intern_token_fields(
196
+ kind: previous.kind,
197
+ text: previous.text,
198
+ leading: previous.leading,
199
+ trailing: previous.trailing + trailing,
200
+ flags: previous.flags,
201
+ expected_kind: previous.expected_kind
202
+ )
203
+ end
204
+
205
+ # @rbs (untyped location, Symbol key) -> Array[CST::GreenTrivia]
206
+ def scanned_green_trivia(location, key)
207
+ return [] unless location.is_a?(Hash)
208
+
209
+ value = location[key]
210
+ value.is_a?(Array) ? value.grep(CST::GreenTrivia) : []
211
+ end
212
+
213
+ # @rbs (LexerInput input) -> bool
214
+ def ensure_lexer_data?(input)
215
+ input.read_more? while input.buffer.empty? && !input.eof?
216
+ !input.buffer.empty?
217
+ end
218
+
219
+ # @rbs (LexerInput input) -> [Hash[Symbol, untyped]?, String?]
220
+ # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
221
+ def select_lexer_match(input)
222
+ loop do
223
+ matches = lexer_rules.filter_map do |rule|
224
+ match = rule.fetch(:regexp).match(input.buffer)
225
+ [rule, match[0]] if match&.begin(0)&.zero?
226
+ end #: Array[[Hash[Symbol, untyped], String]]
227
+ boundary_match = matches.any? { |_rule, lexeme| lexeme.length == input.buffer.length }
228
+ if !input.eof? && (matches.empty? || boundary_match)
229
+ input.read_more?
230
+ next
231
+ end
232
+
233
+ selected = matches.max_by { |rule, lexeme| [lexeme.bytesize, -rule.fetch(:id)] }
234
+ return [nil, nil] unless selected
235
+
236
+ return selected
237
+ end
238
+ end
239
+ # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
240
+
241
+ # @rbs () -> Array[Hash[Symbol, untyped]]
242
+ def lexer_rules
243
+ tables = self.class.const_get(:LEXER_RULES_BY_STATE)
244
+ rules = tables[lexer_state]
245
+ return rules if rules
246
+
247
+ raise ParseError, "(lexer):1:1: missing rules for lexer state #{lexer_state}"
248
+ end
249
+
250
+ # @rbs (LexerInput input) -> bot
251
+ def raise_lexer_no_match(input)
252
+ location = lexer_zero_width_location
253
+ location[:ibex_cst_unmatched_text] = input.buffer.dup.freeze
254
+ location.freeze
255
+ excerpt = input.buffer.slice(0, 16)
256
+ raise ParseError.new(
257
+ token_name: "lexer input", token_value: excerpt, location: location,
258
+ detail: "no lexer rule matches #{excerpt.inspect} in state #{lexer_state}"
259
+ )
260
+ end
261
+
262
+ # @rbs (LexerInput input, String lexeme) -> Hash[Symbol, untyped]
263
+ def consume_lexer_match(input, lexeme)
264
+ start = lexer_position
265
+ input.consume(lexeme)
266
+ advance_lexer_position(lexeme)
267
+ finish = lexer_position
268
+ {
269
+ file: @lexer_file, line: start.fetch(:line), column: start.fetch(:column),
270
+ grapheme_column: start.fetch(:column), byte_column: start.fetch(:byte_column),
271
+ end_line: finish.fetch(:line), end_column: finish.fetch(:column),
272
+ end_grapheme_column: finish.fetch(:column), end_byte_column: finish.fetch(:byte_column),
273
+ start_byte: start.fetch(:byte), end_byte: finish.fetch(:byte),
274
+ source_line: input.source_line(start.fetch(:line))
275
+ }
276
+ end
277
+
278
+ # @rbs (Hash[Symbol, untyped] rule, String lexeme, Hash[Symbol, untyped] location) ->
279
+ # [untyped, untyped, Hash[Symbol, untyped]]?
280
+ def apply_lexer_rule(rule, lexeme, location)
281
+ @lexer_lexeme = lexeme
282
+ @lexer_emission = NO_EMISSION
283
+ @lexer_skip_requested = rule.fetch(:kind) == :skip
284
+ action = rule[:action]
285
+ value = action ? __send__(action, lexeme) : lexeme
286
+ emission = @lexer_emission
287
+ if emission.is_a?(Array)
288
+ location[:ibex_cst_text] = lexeme
289
+ attached = attach_cst_trivia(location)
290
+ @lexer_has_token = true
291
+ return [emission.fetch(0), emission.fetch(1), attached.freeze]
292
+ end
293
+
294
+ if @lexer_skip_requested
295
+ retain_cst_trivia(lexeme, location.freeze)
296
+ return nil
297
+ end
298
+ if rule.fetch(:kind) == :token
299
+ location[:ibex_cst_text] = lexeme
300
+ attached = attach_cst_trivia(location)
301
+ @lexer_has_token = true
302
+ return [rule.fetch(:token), value, attached.freeze]
303
+ end
304
+
305
+ location.freeze
306
+ raise ParseError, "#{location.fetch(:file)}:#{location.fetch(:line)}:" \
307
+ "#{location.fetch(:column)}: on lexer rule did not emit or skip"
308
+ ensure
309
+ @lexer_lexeme = nil
310
+ end
311
+
312
+ # Emit a token from a lexer action.
313
+ # @rbs (untyped token, ?untyped value) -> untyped
314
+ def emit(token, value = NO_EMISSION)
315
+ actual = value.equal?(NO_EMISSION) ? @lexer_lexeme : value
316
+ @lexer_emission = [token, actual]
317
+ actual
318
+ end
319
+
320
+ # Suppress the current match from a lexer action.
321
+ # @rbs () -> nil
322
+ def skip
323
+ @lexer_skip_requested = true
324
+ nil
325
+ end
326
+
327
+ # Return the text consumed by the active lexer action.
328
+ # @rbs () -> String?
329
+ def lexeme
330
+ @lexer_lexeme
331
+ end
332
+
333
+ # @rbs (String text, Hash[Symbol, untyped] location) -> void
334
+ def retain_cst_trivia(text, _location)
335
+ return if cst_trivia_policy == :drop
336
+
337
+ kind = cst_trivia_kind(text)
338
+ cache = @green_cache
339
+ value = if cache
340
+ cache.intern_trivia_fields(kind: kind, text: text)
341
+ else
342
+ CST::GreenTrivia.new(kind: kind, text: text)
343
+ end
344
+ @lexer_pending_green_trivia << value
345
+ end
346
+
347
+ # @rbs (Hash[Symbol, untyped] location) -> Hash[Symbol, untyped]
348
+ def attach_cst_trivia(location)
349
+ policy = cst_trivia_policy
350
+ trivia = @lexer_pending_green_trivia
351
+ return location if trivia.empty? || policy == :drop
352
+
353
+ @lexer_pending_green_trivia = []
354
+ if policy == :balanced && @lexer_has_token
355
+ trailing, leading = split_balanced_trivia(trivia)
356
+ else
357
+ leading = trivia.freeze
358
+ trailing = EMPTY_GREEN_TRIVIA
359
+ end
360
+ location[:leading_trivia] = leading.freeze
361
+ location[:cst_previous_trailing] = trailing.freeze
362
+ location
363
+ end
364
+
365
+ # @rbs () -> Symbol
366
+ def cst_trivia_policy
367
+ configure_lexer_cst unless defined?(@lexer_cst_trivia_policy)
368
+ @lexer_cst_trivia_policy
369
+ end
370
+
371
+ # @rbs (String text) -> Integer
372
+ def cst_trivia_kind(text)
373
+ kinds = @lexer_cst_trivia_kinds || raise(ParseError, "(lexer):1:1: CST trivia kinds are unavailable")
374
+ name = if text.include?("\n")
375
+ "newline"
376
+ elsif text.match?(/\A[[:space:]]+\z/)
377
+ "whitespace"
378
+ elsif text.start_with?("//", "#")
379
+ "line_comment"
380
+ elsif text.start_with?("/*")
381
+ "block_comment"
382
+ else
383
+ "custom_skip"
384
+ end
385
+ kinds.fetch(name)
386
+ end
387
+
388
+ # @rbs () -> void
389
+ def configure_lexer_cst
390
+ tables = parser_tables
391
+ config = tables[:cst]
392
+ if config.is_a?(Hash)
393
+ @lexer_cst_trivia_policy = config.fetch(:trivia_policy)
394
+ @lexer_cst_trivia_kinds = config.fetch(:kinds).fetch(:trivia)
395
+ return
396
+ end
397
+
398
+ @lexer_cst_trivia_policy = :drop
399
+ @lexer_cst_trivia_kinds = nil
400
+ end
401
+
402
+ # @rbs (Array[CST::GreenTrivia] trivia) -> [Array[CST::GreenTrivia], Array[CST::GreenTrivia]]
403
+ def split_balanced_trivia(trivia)
404
+ return [trivia.freeze, EMPTY_GREEN_TRIVIA] unless trivia.any? { |item| item.text.include?("\n".b) }
405
+
406
+ before = [] #: Array[CST::GreenTrivia]
407
+ after = [] #: Array[CST::GreenTrivia]
408
+ found = false
409
+ trivia.each do |item|
410
+ if found
411
+ after << item
412
+ next
413
+ end
414
+ newline = item.text.index("\n".b)
415
+ unless newline
416
+ before << item
417
+ next
418
+ end
419
+
420
+ boundary = newline + 1
421
+ before_text = item.text.byteslice(0, boundary) || "".b
422
+ after_text = item.text.byteslice(boundary, item.text.bytesize - boundary) || "".b
423
+ before << CST::GreenTrivia.new(kind: cst_trivia_kind(before_text), text: before_text)
424
+ after << CST::GreenTrivia.new(kind: cst_trivia_kind(after_text), text: after_text) unless after_text.empty?
425
+ found = true
426
+ end
427
+ [before, after]
428
+ end
429
+
430
+ # @rbs () -> CST::SourceText
431
+ def cst_source_text
432
+ input = @lexer_input || raise(ParseError, "(lexer):1:1: lexer input is unavailable")
433
+ CST::SourceText.new(input.source_bytes.byteslice(0, @lexer_byte_offset || 0) || "".b, file: @lexer_file)
434
+ end
435
+
436
+ # @rbs () -> Array[CST::GreenTrivia]
437
+ def take_cst_pending_green_trivia
438
+ values = @lexer_pending_green_trivia
439
+ @lexer_pending_green_trivia = []
440
+ values.freeze
441
+ end
442
+
443
+ # @rbs (Array[CST::GreenTrivia] values) -> Array[CST::GreenTrivia]
444
+ def green_trivia_values(values)
445
+ result = [] #: Array[CST::GreenTrivia]
446
+ values.each { |item| result << item if item.is_a?(CST::GreenTrivia) }
447
+ result
448
+ end
449
+
450
+ # Push a named lexer state.
451
+ # @rbs (Symbol | String state) -> Symbol
452
+ def push_state(state)
453
+ name = validate_lexer_state(state)
454
+ (@lexer_states ||= ["INITIAL"]) << name
455
+ name.to_sym
456
+ end
457
+
458
+ # Pop the current named lexer state.
459
+ # @rbs () -> Symbol
460
+ def pop_state
461
+ states = @lexer_states ||= ["INITIAL"]
462
+ raise ParseError, "(lexer):1:1: cannot pop the INITIAL lexer state" if states.one?
463
+
464
+ (states.pop || "INITIAL").to_sym
465
+ end
466
+
467
+ # @rbs (Symbol | String state) -> String
468
+ def validate_lexer_state(state)
469
+ name = state.to_s
470
+ known = self.class.const_get(:LEXER_STATES)
471
+ raise ArgumentError, "unknown lexer state #{state.inspect}" unless known.include?(name)
472
+
473
+ name
474
+ end
475
+
476
+ # @rbs () -> Hash[Symbol, Integer]
477
+ def lexer_position
478
+ {
479
+ line: @lexer_line || 1, column: @lexer_column || 1,
480
+ byte_column: @lexer_byte_column || 1, byte: @lexer_byte_offset || 0
481
+ }
482
+ end
483
+
484
+ # @rbs () -> Hash[Symbol, untyped]
485
+ def lexer_zero_width_location
486
+ position = lexer_position
487
+ {
488
+ file: @lexer_file || "(input)", line: position.fetch(:line), column: position.fetch(:column),
489
+ grapheme_column: position.fetch(:column), byte_column: position.fetch(:byte_column),
490
+ end_line: position.fetch(:line), end_column: position.fetch(:column),
491
+ end_grapheme_column: position.fetch(:column), end_byte_column: position.fetch(:byte_column),
492
+ start_byte: position.fetch(:byte), end_byte: position.fetch(:byte)
493
+ }
494
+ end
495
+
496
+ # @rbs (String lexeme) -> void
497
+ def advance_lexer_position(lexeme)
498
+ lexeme.scan(/\X/).each do |value|
499
+ grapheme = value.is_a?(String) ? value : value.join
500
+ @lexer_byte_offset += grapheme.bytesize
501
+ if grapheme.include?("\n")
502
+ @lexer_line += 1
503
+ @lexer_column = 1
504
+ @lexer_byte_column = 1
505
+ else
506
+ @lexer_column += 1
507
+ @lexer_byte_column += grapheme.bytesize
508
+ end
509
+ end
510
+ end
511
+ end
512
+ # rubocop:enable Metrics/ModuleLength
513
+ end
514
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+ # rbs_inline: enabled
3
+
4
+ require "json"
5
+
6
+ module Ibex
7
+ module Runtime
8
+ # Emits committed parser observer events as JSON Lines.
9
+ module JSONLTracer
10
+ # @rbs!
11
+ # interface _TraceOutput
12
+ # def puts: (String) -> untyped
13
+ # end
14
+
15
+ # Hook layer installed on an individual parser's singleton class.
16
+ module Hooks
17
+ # @rbs!
18
+ # def token_to_str: (Integer) -> String
19
+
20
+ # @rbs @ibex_jsonl_trace_output: _TraceOutput?
21
+
22
+ # @rbs (Integer token_id, untyped value, Integer state) -> void
23
+ def on_shift(token_id, value, state)
24
+ write_trace(event: "shift", token_id: token_id, token: token_to_str(token_id),
25
+ value: safe_inspect(value), state: state)
26
+ super
27
+ end
28
+
29
+ # @rbs (Integer production_id, Array[untyped] values, untyped result) -> void
30
+ def on_reduce(production_id, values, result)
31
+ write_trace(event: "reduce", production_id: production_id, values: values.map { |value| safe_inspect(value) },
32
+ result: safe_inspect(result))
33
+ super
34
+ end
35
+
36
+ # @rbs (Integer token_id, untyped value, Array[untyped] value_stack) -> void
37
+ def on_error_recover(token_id, value, value_stack)
38
+ write_trace(event: "error_recover", token_id: token_id, token: token_to_str(token_id),
39
+ value: safe_inspect(value), value_stack: value_stack.map { |item| safe_inspect(item) })
40
+ super
41
+ end
42
+
43
+ private
44
+
45
+ # @rbs (_TraceOutput output) -> void
46
+ def ibex_jsonl_trace_output=(output)
47
+ @ibex_jsonl_trace_output = output
48
+ end
49
+
50
+ # @rbs (**untyped event) -> void
51
+ def write_trace(**event)
52
+ @ibex_jsonl_trace_output&.puts(JSON.generate(event))
53
+ rescue StandardError
54
+ nil
55
+ end
56
+
57
+ # @rbs (untyped value) -> String
58
+ def safe_inspect(value)
59
+ value.inspect.encode(Encoding::UTF_8, invalid: :replace, undef: :replace)
60
+ rescue StandardError
61
+ "<inspect failed>"
62
+ end
63
+ end
64
+
65
+ # @rbs [P < Parser] (P parser, io: _TraceOutput) -> P
66
+ def self.attach(parser, io:)
67
+ singleton = parser.singleton_class
68
+ singleton.prepend(Hooks) unless singleton.ancestors.include?(Hooks)
69
+ Object.instance_method(:instance_variable_set).bind_call(parser, :@runtime_fast_path, false)
70
+ parser.__send__(:ibex_jsonl_trace_output=, io)
71
+ parser
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+ # rbs_inline: enabled
3
+
4
+ module Ibex
5
+ module Runtime
6
+ # Incremental String, IO, or Fiber input used by generated lexers.
7
+ class LexerInput
8
+ DEFAULT_CHUNK_SIZE = 16 * 1024 #: Integer
9
+
10
+ attr_reader :buffer #: String
11
+
12
+ # @rbs (String | IO | Fiber source, ?chunk_size: Integer) -> void
13
+ def initialize(source, chunk_size: DEFAULT_CHUNK_SIZE)
14
+ raise ArgumentError, "lexer chunk_size must be positive" unless chunk_size.positive?
15
+ unless source.is_a?(String) || source.respond_to?(:read) || source.is_a?(Fiber)
16
+ raise ArgumentError, "lexer input must be a String, IO, or Fiber"
17
+ end
18
+
19
+ @source = source
20
+ @chunk_size = chunk_size
21
+ @buffer = +""
22
+ @archive = +""
23
+ @eof = false
24
+ return unless source.is_a?(String)
25
+
26
+ append(source)
27
+ @eof = true
28
+ end
29
+
30
+ # @rbs () -> bool
31
+ def eof? = @eof
32
+
33
+ # Return every source byte read so far, including the unconsumed buffer.
34
+ # @rbs () -> String
35
+ def source_bytes = @archive.dup
36
+
37
+ # Read until at least one byte is added or EOF is observed.
38
+ # @rbs () -> bool
39
+ def read_more?
40
+ before = @buffer.bytesize
41
+ read_chunk until @eof || @buffer.bytesize > before
42
+ @buffer.bytesize > before
43
+ end
44
+
45
+ # @rbs (String prefix) -> void
46
+ def consume(prefix)
47
+ raise ArgumentError, "lexer input prefix does not match the buffer" unless @buffer.start_with?(prefix)
48
+
49
+ @buffer.slice!(0, prefix.length)
50
+ end
51
+
52
+ # Return one source line, reading ahead only to its line boundary.
53
+ # @rbs (Integer line) -> String
54
+ def source_line(line)
55
+ raise ArgumentError, "source line must be positive" unless line.positive?
56
+
57
+ read_more? while !@eof && @archive.count("\n") < line
58
+ (@archive.lines[line - 1] || "").delete_suffix("\n").delete_suffix("\r")
59
+ end
60
+
61
+ private
62
+
63
+ # @rbs () -> void
64
+ def read_chunk
65
+ source = @source
66
+ chunk = if source.is_a?(Fiber)
67
+ resume_fiber(source)
68
+ elsif source.respond_to?(:read)
69
+ source.read(@chunk_size)
70
+ end
71
+ if chunk.nil? || chunk == ""
72
+ @eof = true
73
+ return
74
+ end
75
+ raise TypeError, "lexer input chunks must be Strings" unless chunk.is_a?(String)
76
+
77
+ text = chunk #: String
78
+ append(text)
79
+ end
80
+
81
+ # @rbs (Fiber source) -> untyped
82
+ def resume_fiber(source)
83
+ supports_alive = source.respond_to?(:alive?)
84
+ if supports_alive && !source.alive?
85
+ @eof = true
86
+ return
87
+ end
88
+
89
+ value = source.resume
90
+ @eof = true if supports_alive && !source.alive?
91
+ value
92
+ rescue FiberError => e
93
+ terminated = /\A(?:dead fiber called|attempt to resume a terminated fiber)\z/.match?(e.message)
94
+ raise unless !supports_alive && terminated
95
+
96
+ @eof = true
97
+ nil
98
+ end
99
+
100
+ # @rbs (String chunk) -> void
101
+ def append(chunk)
102
+ @buffer << chunk
103
+ @archive << chunk
104
+ end
105
+ end
106
+ end
107
+ end