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,3027 @@
1
+ # frozen_string_literal: true
2
+ # rbs_inline: enabled
3
+
4
+ require_relative "location_span" unless defined?(Ibex::Runtime::LocationSpan)
5
+ require_relative "observation" unless defined?(Ibex::Runtime::Observation)
6
+ require_relative "resource_limits" unless defined?(Ibex::Runtime::ResourceLimits)
7
+ require_relative "repair" unless defined?(Ibex::Runtime::RepairPolicy)
8
+ require_relative "repair_search" unless defined?(Ibex::Runtime::RepairSearch)
9
+ require_relative "parser_sync_recovery" unless defined?(Ibex::Runtime::ParserSyncRecovery)
10
+ require_relative "table_format" unless defined?(Ibex::Runtime::PARSER_TABLE_FORMAT_VERSION)
11
+
12
+ module Ibex
13
+ module Runtime
14
+ # Raised by the default parser error handler.
15
+ class ParseError < StandardError
16
+ attr_reader :token_id #: Integer?
17
+ attr_reader :token_name #: String?
18
+ attr_reader :token_value #: untyped
19
+ attr_reader :expected_tokens #: Array[String]
20
+ attr_reader :location #: untyped
21
+ attr_reader :state #: Integer?
22
+ attr_reader :suggestions #: Array[String]
23
+ attr_reader :error_id #: String?
24
+
25
+ # rubocop:disable Layout/LineLength
26
+ # @rbs (?String? message, ?token_id: Integer?, ?token_name: String?, ?token_value: untyped, ?expected_tokens: Array[String], ?location: untyped, ?state: Integer?, ?suggestions: Array[String], ?error_id: String?, ?detail: String?) -> void
27
+ # rubocop:enable Layout/LineLength
28
+ def initialize(
29
+ message = nil,
30
+ token_id: nil,
31
+ token_name: nil,
32
+ token_value: nil,
33
+ expected_tokens: [],
34
+ location: nil,
35
+ state: nil,
36
+ suggestions: [],
37
+ error_id: nil,
38
+ detail: nil
39
+ )
40
+ @token_id = token_id
41
+ @token_name = token_name
42
+ @token_value = token_value
43
+ @expected_tokens = expected_tokens.dup.freeze
44
+ @location = location
45
+ @state = state
46
+ @suggestions = suggestions.dup.freeze
47
+ @error_id = error_id&.dup&.freeze
48
+ @detail = detail
49
+ super(message || diagnostic_message)
50
+ end
51
+
52
+ # @rbs () -> String
53
+ def location_label
54
+ file = location_value(:file) || "(input)"
55
+ line = location_value(:line) || 1
56
+ column = location_value(:column) || 1
57
+ "#{file}:#{line}:#{column}"
58
+ end
59
+
60
+ private
61
+
62
+ # @rbs () -> String
63
+ def diagnostic_message
64
+ expected = @expected_tokens.empty? ? "" : "; expected #{@expected_tokens.join(', ')}"
65
+ default = "unexpected #{@token_name || @token_id}#{expected} (#{@token_value.inspect})"
66
+ identifier = @error_id ? "[#{@error_id}] " : ""
67
+ message = "#{location_label}: #{identifier}#{@detail || default}"
68
+ source_line = location_value(:source_line)
69
+ column = location_value(:column)
70
+ message += "\n#{source_line}\n#{' ' * [column.to_i - 1, 0].max}^" if source_line
71
+ message += "\ndid you mean #{@suggestions.join(' or ')}?" unless @suggestions.empty?
72
+ message
73
+ end
74
+
75
+ # @rbs (Symbol key) -> untyped
76
+ def location_value(key)
77
+ return nil unless @location
78
+ return @location.public_send(key) if @location.respond_to?(key)
79
+ return @location[key] || @location[key.to_s] if @location.is_a?(Hash)
80
+
81
+ nil
82
+ end
83
+ end
84
+
85
+ # Raised when a configured parser-session resource budget is exhausted.
86
+ class ResourceLimitError < ParseError
87
+ attr_reader :resource #: Symbol
88
+ attr_reader :limit #: Integer
89
+ attr_reader :observed #: Integer
90
+
91
+ # @rbs (resource: Symbol, limit: Integer, observed: Integer, state: Integer?, location: untyped) -> void
92
+ def initialize(resource:, limit:, observed:, state:, location:)
93
+ @resource = resource
94
+ @limit = limit
95
+ @observed = observed
96
+ super(
97
+ "parser resource limit exceeded: #{resource} is #{observed}, configured maximum is #{limit}",
98
+ state: state, location: location
99
+ )
100
+ end
101
+
102
+ # @rbs () -> Hash[Symbol, untyped]
103
+ def to_h
104
+ {
105
+ type: :resource_limit, resource: @resource, limit: @limit, observed: @observed,
106
+ state: state, location: location
107
+ }.freeze
108
+ end
109
+ end
110
+
111
+ # rubocop:disable Metrics/ClassLength
112
+
113
+ # Drives a table-defined LR parser without native extensions.
114
+ #
115
+ # Subclasses provide `.parser_tables`, returning `:tokens`, `:token_names`,
116
+ # `:actions`, `:gotos`, and `:productions`, with optional
117
+ # `:default_actions`, `:error_messages`, and `:recovery_sync_tokens`.
118
+ # Actions are represented by
119
+ # `[:shift, state]`, `[:reduce, production]`, `[:accept]`, or `[:error]`.
120
+ # Format-v2 and newer generated production entries mark their five-argument
121
+ # semantic methods with `location_action: true`. Format-v3 and newer
122
+ # composed actions additionally use `composition_action: true` for the
123
+ # six-argument contract carrying the lookahead location. Format-v4
124
+ # location-free generated methods use `values_action: true` for a
125
+ # one-argument values contract. Format-v5 additionally marks proven-safe
126
+ # zero-to-four-value methods with `positional_action: true`. V1 and
127
+ # unmarked application actions retain the historical two-argument
128
+ # contract. Markers are honored only for the generated `_ibex_action_N`
129
+ # Symbol shape, never for callables.
130
+ class Parser
131
+ # Keep direct singleton-hook mutation visible even when an application
132
+ # replaces Ruby's mutation callbacks without calling super.
133
+ module FastPathMutationTracker
134
+ # @rbs (Ibex::Runtime::Parser parser, UnboundMethod lookup) -> bool
135
+ def self.effective_for?(parser, lookup)
136
+ added = lookup.bind_call(parser, :singleton_method_added) # @type var added: Method
137
+ removed = lookup.bind_call(parser, :singleton_method_removed) # @type var removed: Method
138
+ undefined = lookup.bind_call(parser, :singleton_method_undefined) # @type var undefined: Method
139
+ added.owner.equal?(self) && removed.owner.equal?(self) && undefined.owner.equal?(self)
140
+ rescue NameError
141
+ false
142
+ end
143
+
144
+ private
145
+
146
+ # @rbs (Symbol name) -> void
147
+ def singleton_method_added(name)
148
+ case name
149
+ when :on_shift, :on_shift_location, :on_reduce, :on_reduce_location, :token_to_str
150
+ @runtime_fast_path = false
151
+ @runtime_fast_path_hooks_mutated = true
152
+ end
153
+ super
154
+ end
155
+
156
+ # @rbs (Symbol name) -> void
157
+ def singleton_method_removed(name)
158
+ case name
159
+ when :on_shift, :on_shift_location, :on_reduce, :on_reduce_location, :token_to_str
160
+ @runtime_fast_path = false
161
+ @runtime_fast_path_hooks_mutated = true
162
+ end
163
+ super
164
+ end
165
+
166
+ # @rbs (Symbol name) -> void
167
+ def singleton_method_undefined(name)
168
+ case name
169
+ when :on_shift, :on_shift_location, :on_reduce, :on_reduce_location, :token_to_str
170
+ @runtime_fast_path = false
171
+ @runtime_fast_path_hooks_mutated = true
172
+ end
173
+ super
174
+ end
175
+ end
176
+
177
+ # Invalidates the generated-class hook cache when the parser class
178
+ # changes its own effective hook surface.
179
+ module FastPathClassMutationTracker
180
+ # @rbs (Symbol name) -> void
181
+ def method_added(name)
182
+ __ibex_invalidate_fast_path_hooks(name)
183
+ super
184
+ end
185
+
186
+ # @rbs (Symbol name) -> void
187
+ def method_removed(name)
188
+ __ibex_invalidate_fast_path_hooks(name)
189
+ super
190
+ end
191
+
192
+ # @rbs (Symbol name) -> void
193
+ def method_undefined(name)
194
+ __ibex_invalidate_fast_path_hooks(name)
195
+ super
196
+ end
197
+
198
+ # @rbs (*Module modules) -> self
199
+ def include(*modules)
200
+ __ibex_bump_fast_path_hook_version
201
+ super
202
+ end
203
+
204
+ # @rbs (*Module modules) -> self
205
+ def prepend(*modules)
206
+ __ibex_bump_fast_path_hook_version
207
+ super
208
+ end
209
+
210
+ private
211
+
212
+ # @rbs (Symbol name) -> void
213
+ def __ibex_invalidate_fast_path_hooks(name)
214
+ __ibex_bump_fast_path_hook_version if FAST_PATH_HOOK_NAMES.include?(name)
215
+ end
216
+
217
+ # @rbs () -> void
218
+ def __ibex_bump_fast_path_hook_version
219
+ current = instance_variable_get(:@__ibex_fast_path_hook_version) || 0
220
+ instance_variable_set(:@__ibex_fast_path_hook_version, current + 1)
221
+ remove_instance_variable(:@__ibex_fast_path_hook_cache) if
222
+ instance_variable_defined?(:@__ibex_fast_path_hook_cache)
223
+ end
224
+
225
+ private :method_added, :method_removed, :method_undefined
226
+ end
227
+
228
+ include Observation
229
+ include ParserSyncRecovery
230
+
231
+ ParseError = Ibex::Runtime::ParseError #: singleton(Ibex::Runtime::ParseError)
232
+ EOF_TOKEN = 0 #: Integer
233
+ ERROR_TOKEN = 1 #: Integer
234
+ GENERATED_ACTION_NAME = /\A_ibex_action_\d+\z/ #: Regexp
235
+ NO_LOOKAHEAD = Object.new.freeze #: Object
236
+ RECOVERY_SHIFTS = 3 #: Integer
237
+ FAST_PATH_HOOK_NAMES = %i[
238
+ on_shift on_shift_location on_reduce on_reduce_location token_to_str
239
+ ].freeze #: Array[Symbol]
240
+ FAST_PATH_HOOK_REFERENCES = {
241
+ on_shift: :__ibex_fast_path_on_shift,
242
+ on_shift_location: :__ibex_fast_path_on_shift_location,
243
+ on_reduce: :__ibex_fast_path_on_reduce,
244
+ on_reduce_location: :__ibex_fast_path_on_reduce_location,
245
+ token_to_str: :__ibex_fast_path_token_to_str
246
+ }.freeze #: Hash[Symbol, Symbol]
247
+ ERROR_ACTION = [:error].freeze #: [:error]
248
+ SYNC_RECOVER_ACTION = [:sync_recover].freeze #: [:sync_recover]
249
+ CONTINUE_OUTCOME = [:continue].freeze #: [:continue]
250
+ REPAIR_PENDING_OUTCOME = [:repair_pending].freeze #: [:repair_pending]
251
+ TERMINAL_OUTCOMES = %i[accepted done].freeze #: Array[Symbol]
252
+ COMPACT_ACCEPTED = Object.new.freeze #: Object
253
+ empty_row = {} # @type var empty_row: Hash[Integer, untyped]
254
+ empty_location_names = {} # @type var empty_location_names: Hash[Symbol, Integer]
255
+ empty_locations = [] # @type var empty_locations: Array[untyped]
256
+ empty_green_trivia = [] # @type var empty_green_trivia: Array[CST::GreenTrivia]
257
+
258
+ EMPTY_ROW = empty_row.freeze #: Hash[Integer, untyped]
259
+ EMPTY_LOCATION_NAMES = empty_location_names.freeze #: Hash[Symbol, Integer]
260
+ EMPTY_LOCATIONS = empty_locations.freeze #: Array[untyped]
261
+ EMPTY_GREEN_TRIVIA = empty_green_trivia.freeze #: Array[CST::GreenTrivia]
262
+ private_constant :ERROR_ACTION, :SYNC_RECOVER_ACTION, :CONTINUE_OUTCOME, :REPAIR_PENDING_OUTCOME,
263
+ :TERMINAL_OUTCOMES, :COMPACT_ACCEPTED, :FAST_PATH_HOOK_NAMES, :FAST_PATH_HOOK_REFERENCES,
264
+ :FastPathMutationTracker, :FastPathClassMutationTracker
265
+
266
+ # @rbs @yydebug: bool
267
+ # @rbs @yydebug_output: IO
268
+ # @rbs @source: untyped
269
+ # @rbs @state_stack: Array[Integer]
270
+ # @rbs @value_stack: Array[untyped]
271
+ # @rbs @vstack: Array[untyped]
272
+ # @rbs @racc_vstack: Array[untyped]
273
+ # @rbs @location_stack: Array[untyped]?
274
+ # @rbs @lookahead: untyped
275
+ # @rbs @lookahead_value: untyped
276
+ # @rbs @lookahead_location: untyped
277
+ # @rbs @recovery_shifts: Integer
278
+ # @rbs @semantic_error: bool
279
+ # @rbs @accept_requested: bool
280
+ # @rbs @unknown_token_id: Integer?
281
+ # @rbs @unknown_token_name: String?
282
+ # @rbs @push_status: :idle | :active | :finished
283
+ # @rbs @driver_status: :idle | :pull | :push
284
+ # @rbs @runtime_driver_thread: Thread?
285
+ # @rbs @runtime_observers: Hash[Observation::Subscription, Proc]?
286
+ # @rbs @runtime_event_sequence: Integer
287
+ # @rbs @runtime_lookahead_token_display: untyped
288
+ # @rbs @runtime_observation_mutex: Mutex
289
+ # @rbs @repair_policy: RepairPolicy?
290
+ # @rbs @repair_input_buffer: Array[RepairInput]?
291
+ # @rbs @repair_selected: bool
292
+ # @rbs @semantic_locations: Array[untyped]?
293
+ # @rbs @semantic_location_names: Hash[Symbol, Integer]?
294
+ # @rbs @semantic_result_location: untyped
295
+ # @rbs @trace_value_printer: (^(untyped) -> untyped)?
296
+ # @rbs @sync_recovery_context: Hash[Symbol, untyped]?
297
+ # @rbs @sync_recovery_token_data: Hash[String, untyped]?
298
+ # @rbs @sync_recovery_observers: Array[Proc]?
299
+ # @rbs @green_builder: CST::GreenBuilder?
300
+ # @rbs @green_kinds: CST::Kind?
301
+ # @rbs @green_cache: CST::NodeCache?
302
+ # @rbs @syntax_root: CST::SyntaxNode?
303
+ # @rbs @syntax_diagnostics: Array[untyped]
304
+ # @rbs @green_pending_skipped: Array[CST::GreenTrivia]
305
+ # @rbs @resource_limits: ResourceLimits
306
+ # @rbs @recovery_attempts: Integer
307
+ # @rbs @runtime_parser_tables: Hash[Symbol, untyped]?
308
+ # @rbs @runtime_fast_path: bool
309
+ # @rbs @runtime_fast_path_tracker_installed: bool
310
+ # @rbs @runtime_fast_path_hooks_mutated: bool
311
+ # @rbs @runtime_fast_path_singleton_ancestors: Array[Module]?
312
+ # @rbs @syntax_only: bool
313
+ # @rbs @green_cache_override: CST::NodeCache?
314
+ # @rbs @green_token_states: Array[Symbol]
315
+ # @rbs @green_memo_stack: Array[CST::ParseMemo::Entry]
316
+ # @rbs @green_parse_memo_valid: bool
317
+ # @rbs @green_initial_state: Integer
318
+ # @rbs @syntax_parse_memo: CST::ParseMemo?
319
+ # @rbs @green_reused_right_edge: bool
320
+ # @rbs @incremental_reused_descendants: Integer
321
+
322
+ attr_reader :syntax_parse_memo #: CST::ParseMemo?
323
+ attr_reader :incremental_reused_descendants #: Integer
324
+
325
+ # Start a syntax-only incremental session backed by a generated lexer.
326
+ # @rbs (CST::SourceText source_text, ?resource_limits: ResourceLimits?, ?blender: bool) ->
327
+ # CST::IncrementalParseSession
328
+ def self.incremental_session(source_text, resource_limits: nil, blender: true)
329
+ CST::IncrementalParseSession.new(
330
+ self, source_text, resource_limits: resource_limits, blender: blender
331
+ )
332
+ end
333
+
334
+ # @rbs (?resource_limits: ResourceLimits) -> void
335
+ def initialize(resource_limits: ResourceLimits.new)
336
+ validate_resource_limits!(resource_limits)
337
+ initialize_runtime_state(resource_limits, preserve_existing: false)
338
+ end
339
+
340
+ # @rbs () -> bool
341
+ def yydebug
342
+ ensure_runtime_initialized!
343
+ @yydebug
344
+ end
345
+
346
+ # @rbs (bool enabled) -> bool
347
+ def yydebug=(enabled)
348
+ ensure_runtime_initialized!
349
+ @runtime_fast_path = false
350
+ @yydebug = enabled
351
+ end
352
+
353
+ # @rbs (IO output) -> IO
354
+ def yydebug_output=(output)
355
+ ensure_runtime_initialized!
356
+ @yydebug_output = output
357
+ end
358
+
359
+ # @rbs () -> RepairPolicy?
360
+ def repair_policy
361
+ ensure_runtime_initialized!
362
+ @repair_policy
363
+ end
364
+
365
+ # @rbs () -> ResourceLimits
366
+ def resource_limits
367
+ ensure_runtime_initialized!
368
+ @resource_limits
369
+ end
370
+
371
+ # Enable bounded automatic repair for the next parser session.
372
+ # Assign nil to restore the compatible yacc-only behavior.
373
+ # @rbs (RepairPolicy? policy) -> RepairPolicy?
374
+ def repair_policy=(policy)
375
+ ensure_runtime_initialized!
376
+ unless policy.nil? || policy.is_a?(RepairPolicy)
377
+ raise ArgumentError, "repair_policy must be an Ibex::Runtime::RepairPolicy or nil"
378
+ end
379
+
380
+ @runtime_observation_mutex.synchronize do
381
+ ensure_driver_available_without_lock!
382
+ if @push_status == :active
383
+ raise ParseError, "(repair):1:1: repair_policy cannot change during an active push session"
384
+ end
385
+
386
+ @repair_policy = policy
387
+ end
388
+ end
389
+
390
+ # Replace the limits used by future sessions.
391
+ # @rbs (ResourceLimits limits) -> ResourceLimits
392
+ def resource_limits=(limits)
393
+ ensure_runtime_initialized!
394
+ validate_resource_limits!(limits)
395
+
396
+ @runtime_observation_mutex.synchronize do
397
+ ensure_driver_available_without_lock!
398
+ if @push_status == :active
399
+ raise ParseError, "(resource):1:1: resource_limits cannot change during an active push session"
400
+ end
401
+
402
+ @resource_limits = limits
403
+ end
404
+ end
405
+
406
+ # Pull tokens from `next_token` and parse them.
407
+ # @rbs () -> untyped
408
+ def do_parse
409
+ drive_parser(nil)
410
+ end
411
+
412
+ # Parse through `next_token` and return both the semantic value and Red root.
413
+ # @rbs () -> CST::ParseResult
414
+ def parse_with_syntax
415
+ syntax_parse_result(do_parse)
416
+ end
417
+
418
+ # Return the Red source-file root built by the most recent CST parse.
419
+ # @rbs () -> CST::SyntaxNode?
420
+ def syntax_root
421
+ ensure_runtime_initialized!
422
+ @syntax_root
423
+ end
424
+
425
+ # Parse tokens yielded by `receiver.method_id`.
426
+ # @rbs (untyped receiver, Symbol method_id) -> untyped
427
+ def yyparse(receiver, method_id)
428
+ stream = Enumerator.new do |tokens|
429
+ receiver.__send__(method_id) { |token| tokens << token }
430
+ end
431
+ drive_parser(-> { stream.next })
432
+ end
433
+
434
+ # Supply one token to a caller-driven parser session.
435
+ # Returns `:need_more` after consuming it, `[:accepted, result]` after
436
+ # acceptance, or `[:rejected, result]` after recovery terminates.
437
+ # rubocop:disable Layout/LineLength
438
+ # @rbs (untyped token, ?untyped value, ?untyped location) -> (:need_more | [:accepted, untyped] | [:rejected, untyped])
439
+ # rubocop:enable Layout/LineLength
440
+ def push(token, value = nil, location = nil)
441
+ raise ParseError, "(input):1:1: push requires a token; call finish for EOF" if token.nil? || token == false
442
+
443
+ run_push_driver do
444
+ start_push_session
445
+ refresh_runtime_fast_path_after_user_code!
446
+ if @repair_policy
447
+ enqueue_or_assign_repair_input(repair_input(token, value, location))
448
+ else
449
+ @lookahead = internal_token_id(token)
450
+ @lookahead_value = value
451
+ @lookahead_location = location
452
+ @runtime_fast_path = false unless nil.equal?(location)
453
+ materialize_compatible_lookahead
454
+ end
455
+ run_push_lookahead
456
+ end
457
+ end
458
+
459
+ # Supply EOF to a caller-driven parser session and return its result.
460
+ # @rbs (?location: untyped) -> untyped
461
+ def finish(location: nil)
462
+ run_push_driver do
463
+ start_push_session
464
+ refresh_runtime_fast_path_after_user_code!
465
+ if @repair_policy
466
+ enqueue_or_assign_repair_input(
467
+ RepairInput.new(token_id: EOF_TOKEN, token_name: token_to_str(EOF_TOKEN), value: nil, location: location)
468
+ )
469
+ else
470
+ @lookahead = EOF_TOKEN
471
+ @lookahead_value = nil
472
+ @lookahead_location = location
473
+ @runtime_fast_path = false unless nil.equal?(location)
474
+ materialize_compatible_lookahead
475
+ end
476
+ outcome = run_push_lookahead
477
+ return outcome.fetch(1) if outcome.is_a?(Array)
478
+
479
+ raise ParseError, "(input):1:1: parser requested input after EOF"
480
+ end
481
+ end
482
+
483
+ # Discard a caller-driven session so this parser can accept a new one.
484
+ # @rbs () -> nil
485
+ def reset_push
486
+ ensure_runtime_initialized!
487
+ @runtime_observation_mutex.synchronize do
488
+ ensure_driver_available_without_lock!
489
+ @push_status = :idle
490
+ @source = nil
491
+ @state_stack = []
492
+ install_value_stack([])
493
+ @location_stack = nil
494
+ @lookahead = NO_LOOKAHEAD
495
+ @lookahead_value = nil
496
+ @lookahead_location = nil
497
+ @runtime_lookahead_token_display = nil
498
+ @repair_input_buffer = nil
499
+ @repair_selected = false
500
+ @runtime_parser_tables = nil
501
+ @runtime_fast_path = false
502
+ end
503
+ nil
504
+ end
505
+
506
+ # Override in pull parsers. Return `[token, value]`,
507
+ # `[token, value, location]`, `false`, or `nil`.
508
+ # @rbs () -> ([untyped, untyped] | [untyped, untyped, untyped] | false | nil)
509
+ def next_token
510
+ raise NotImplementedError, "(input):1:1: next_token must be implemented"
511
+ end
512
+
513
+ # Override to recover from syntax errors. The default raises unless a
514
+ # bounded automatic repair has already been selected.
515
+ # @rbs (Integer token_id, untyped value, Array[untyped] value_stack) -> untyped
516
+ def on_error(token_id, value, _value_stack)
517
+ return if @repair_selected
518
+ return if cst_enabled?
519
+
520
+ expected = expected_tokens
521
+ token_name = token_to_str(token_id)
522
+ state = @state_stack.last
523
+ configured = parser_tables.fetch(:error_messages, EMPTY_ROW)[state]
524
+ error_id, detail = configured_error_message(configured)
525
+ raise ParseError.new(
526
+ token_id: token_id,
527
+ token_name: token_name,
528
+ token_value: value,
529
+ expected_tokens: expected,
530
+ location: @lookahead_location,
531
+ state: state,
532
+ suggestions: token_suggestions(token_name, expected),
533
+ error_id: error_id,
534
+ detail: detail
535
+ )
536
+ end
537
+
538
+ # Called after an ordinary input token is shifted. Override to observe
539
+ # the internal token id, semantic value, and destination state.
540
+ # @rbs (Integer token_id, untyped value, Integer state) -> void
541
+ def on_shift(_token_id, _value, _state); end
542
+
543
+ # Location-aware shift observer. The compatible hook above retains its
544
+ # original signature and runs first.
545
+ # @rbs (Integer token_id, untyped value, Integer state, untyped location) -> void
546
+ def on_shift_location(_token_id, _value, _state, _location); end
547
+
548
+ # Called after a production's semantic action and goto are committed.
549
+ # Override to observe its id, RHS values, and semantic result.
550
+ # @rbs (Integer production_id, Array[untyped] values, untyped result) -> void
551
+ def on_reduce(_production_id, _values, _result); end
552
+
553
+ # Location-aware reduction observer.
554
+ # @rbs (Integer production_id, Array[untyped] values, untyped result,
555
+ # Array[untyped] locations, untyped result_location) -> void
556
+ def on_reduce_location(_production_id, _values, _result, _locations, _result_location); end
557
+
558
+ # Called after the synthetic error token enters a recovery state.
559
+ # The payload describes the original error before recovery popped stacks.
560
+ # @rbs (Integer token_id, untyped value, Array[untyped] value_stack) -> void
561
+ def on_error_recover(_token_id, _value, _value_stack); end
562
+
563
+ # Location-aware recovery observer.
564
+ # @rbs (Integer token_id, untyped value, Array[untyped] value_stack,
565
+ # untyped location, Integer state) -> void
566
+ def on_error_recover_location(_token_id, _value, _value_stack, _location, _state); end
567
+
568
+ # Called when yacc recovery discards an application token.
569
+ # @rbs (Integer token_id, untyped value, untyped location, Symbol reason) -> void
570
+ def on_discard(_token_id, _value, _location, _reason); end
571
+
572
+ # Install an opt-in value formatter for human-readable yydebug traces.
573
+ # @rbs ((^(untyped) -> untyped)? printer) -> (^(untyped) -> untyped)?
574
+ def trace_value_printer=(printer)
575
+ ensure_runtime_initialized!
576
+ unless printer.nil? || printer.respond_to?(:call)
577
+ raise ArgumentError, "trace value printer must respond to call or be nil"
578
+ end
579
+
580
+ @trace_value_printer = printer
581
+ end
582
+
583
+ # Called once after a repair is selected and before its edited token
584
+ # prefix is replayed through normal parser actions.
585
+ # @rbs (RepairPlan plan) -> void
586
+ def on_repair(_plan); end
587
+
588
+ # Return a human-readable name for an internal token id.
589
+ # @rbs (Integer token_id) -> String
590
+ def token_to_str(token_id)
591
+ return @unknown_token_name || token_id.to_s if token_id == @unknown_token_id
592
+
593
+ parser_tables.fetch(:token_names).fetch(token_id, token_id.to_s)
594
+ end
595
+
596
+ # Enter error recovery from a semantic action without calling `on_error`.
597
+ # @rbs () -> nil
598
+ def yyerror
599
+ @runtime_fast_path = false
600
+ @semantic_error = true
601
+ nil
602
+ end
603
+
604
+ # Leave error recovery immediately.
605
+ # @rbs () -> nil
606
+ def yyerrok
607
+ @recovery_shifts = 0
608
+ nil
609
+ end
610
+
611
+ # Accept immediately after the current semantic action completes.
612
+ # @rbs () -> nil
613
+ def yyaccept
614
+ @runtime_fast_path = false
615
+ @accept_requested = true
616
+ nil
617
+ end
618
+
619
+ # Return token names accepted in the current parser state.
620
+ # @rbs () -> Array[String]
621
+ def expected_tokens
622
+ ensure_runtime_initialized!
623
+ return expected_tokens_exact if parser_tables[:exact_expected_tokens]
624
+ return [] if @state_stack.empty?
625
+
626
+ state = @state_stack.last
627
+ parser_tables.fetch(:token_names).keys.filter_map do |token_id|
628
+ action = table_lookup(parser_tables.fetch(:actions), state, token_id) || default_action(state) || ERROR_ACTION
629
+ token_to_str(token_id) unless error_action?(action) || token_id == ERROR_TOKEN
630
+ end
631
+ end
632
+
633
+ # Return token names that survive all required default reductions.
634
+ # Semantic actions are not evaluated during this lookahead correction.
635
+ # @rbs () -> Array[String]
636
+ def expected_tokens_exact
637
+ ensure_runtime_initialized!
638
+ return [] if @state_stack.empty?
639
+
640
+ parser_tables.fetch(:token_names).keys.filter_map do |token_id|
641
+ next if token_id == ERROR_TOKEN
642
+
643
+ token_to_str(token_id) if exact_lookahead_accepted?(token_id)
644
+ end
645
+ end
646
+
647
+ # Return the location of a one-based RHS position or named reference
648
+ # while a semantic action is running.
649
+ # @rbs (Integer | Symbol | String reference) -> untyped
650
+ def loc(reference)
651
+ locations = @semantic_locations
652
+ raise ParseError, "(runtime):1:1: loc is only available inside a semantic action" unless locations
653
+
654
+ index = if reference.is_a?(Integer)
655
+ raise ArgumentError, "location index must be positive" unless reference.positive?
656
+
657
+ reference - 1
658
+ else
659
+ names = @semantic_location_names || EMPTY_LOCATION_NAMES
660
+ names.fetch(reference.to_sym) do
661
+ raise ArgumentError, "unknown named location #{reference.inspect}"
662
+ end
663
+ end
664
+ locations.fetch(index) { raise ArgumentError, "location index #{reference.inspect} is outside the RHS" }
665
+ end
666
+
667
+ # Return the synthesized span of the reduction being evaluated.
668
+ # @rbs () -> untyped
669
+ def result_loc
670
+ unless @semantic_locations
671
+ raise ParseError, "(runtime):1:1: result_loc is only available inside a semantic action"
672
+ end
673
+
674
+ @semantic_result_location
675
+ end
676
+
677
+ private
678
+
679
+ alias __ibex_fast_path_on_shift on_shift
680
+ alias __ibex_fast_path_on_shift_location on_shift_location
681
+ alias __ibex_fast_path_on_reduce on_reduce
682
+ alias __ibex_fast_path_on_reduce_location on_reduce_location
683
+ alias __ibex_fast_path_token_to_str token_to_str
684
+
685
+ # Racc-generated parsers commonly define an application initializer
686
+ # without calling super. Complete only missing runtime state so those
687
+ # initializers retain values they deliberately configured.
688
+ # @rbs () -> void
689
+ def ensure_runtime_initialized!
690
+ return if defined?(@runtime_observation_mutex) && @runtime_observation_mutex
691
+
692
+ limits = if defined?(@resource_limits) && @resource_limits.is_a?(ResourceLimits)
693
+ @resource_limits
694
+ else
695
+ ResourceLimits.new
696
+ end
697
+ initialize_runtime_state(limits, preserve_existing: true)
698
+ end
699
+
700
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
701
+ # The explicit branches keep every runtime ivar typed while preserving application-owned values.
702
+ # @rbs (ResourceLimits resource_limits, preserve_existing: bool) -> void
703
+ def initialize_runtime_state(resource_limits, preserve_existing:)
704
+ @resource_limits = resource_limits unless preserve_existing && defined?(@resource_limits)
705
+ @yydebug = false unless preserve_existing && defined?(@yydebug)
706
+ @yydebug_output = $stderr unless preserve_existing && defined?(@yydebug_output)
707
+ @source = nil unless preserve_existing && defined?(@source)
708
+ @state_stack = [] unless preserve_existing && defined?(@state_stack)
709
+ install_value_stack([]) unless preserve_existing && defined?(@value_stack)
710
+ @location_stack = nil unless preserve_existing && defined?(@location_stack)
711
+ @lookahead = NO_LOOKAHEAD unless preserve_existing && defined?(@lookahead)
712
+ @lookahead_value = nil unless preserve_existing && defined?(@lookahead_value)
713
+ @lookahead_location = nil unless preserve_existing && defined?(@lookahead_location)
714
+ @recovery_shifts = 0 unless preserve_existing && defined?(@recovery_shifts)
715
+ @semantic_error = false unless preserve_existing && defined?(@semantic_error)
716
+ @accept_requested = false unless preserve_existing && defined?(@accept_requested)
717
+ @unknown_token_id = nil unless preserve_existing && defined?(@unknown_token_id)
718
+ @push_status = :idle unless preserve_existing && defined?(@push_status)
719
+ @driver_status = :idle unless preserve_existing && defined?(@driver_status)
720
+ @runtime_driver_thread = nil unless preserve_existing && defined?(@runtime_driver_thread)
721
+ @runtime_observers = nil unless preserve_existing && defined?(@runtime_observers)
722
+ @runtime_event_sequence = 0 unless preserve_existing && defined?(@runtime_event_sequence)
723
+ @runtime_lookahead_token_display = nil unless preserve_existing && defined?(@runtime_lookahead_token_display)
724
+ @runtime_observation_mutex = Mutex.new unless preserve_existing && defined?(@runtime_observation_mutex)
725
+ @repair_policy = nil unless preserve_existing && defined?(@repair_policy)
726
+ @repair_input_buffer = nil unless preserve_existing && defined?(@repair_input_buffer)
727
+ @repair_selected = false unless preserve_existing && defined?(@repair_selected)
728
+ @semantic_locations = nil unless preserve_existing && defined?(@semantic_locations)
729
+ @semantic_location_names = nil unless preserve_existing && defined?(@semantic_location_names)
730
+ @semantic_result_location = nil unless preserve_existing && defined?(@semantic_result_location)
731
+ @trace_value_printer = nil unless preserve_existing && defined?(@trace_value_printer)
732
+ @sync_recovery_context = nil unless preserve_existing && defined?(@sync_recovery_context)
733
+ @sync_recovery_token_data = nil unless preserve_existing && defined?(@sync_recovery_token_data)
734
+ @sync_recovery_observers = nil unless preserve_existing && defined?(@sync_recovery_observers)
735
+ @green_builder = nil unless preserve_existing && defined?(@green_builder)
736
+ @green_kinds = nil unless preserve_existing && defined?(@green_kinds)
737
+ @green_cache = nil unless preserve_existing && defined?(@green_cache)
738
+ @syntax_root = nil unless preserve_existing && defined?(@syntax_root)
739
+ @syntax_diagnostics = [] unless preserve_existing && defined?(@syntax_diagnostics)
740
+ @green_pending_skipped = [] unless preserve_existing && defined?(@green_pending_skipped)
741
+ @recovery_attempts = 0 unless preserve_existing && defined?(@recovery_attempts)
742
+ @runtime_parser_tables = nil unless preserve_existing && defined?(@runtime_parser_tables)
743
+ @runtime_fast_path = false unless preserve_existing && defined?(@runtime_fast_path)
744
+ @runtime_fast_path_tracker_installed = false unless
745
+ preserve_existing && defined?(@runtime_fast_path_tracker_installed)
746
+ @runtime_fast_path_hooks_mutated = false unless
747
+ preserve_existing && defined?(@runtime_fast_path_hooks_mutated)
748
+ @runtime_fast_path_singleton_ancestors = nil unless
749
+ preserve_existing && defined?(@runtime_fast_path_singleton_ancestors)
750
+ @syntax_only = false unless preserve_existing && defined?(@syntax_only)
751
+ @green_cache_override = nil unless preserve_existing && defined?(@green_cache_override)
752
+ @green_token_states = [] unless preserve_existing && defined?(@green_token_states)
753
+ @green_memo_stack = [] unless preserve_existing && defined?(@green_memo_stack)
754
+ @green_parse_memo_valid = false unless preserve_existing && defined?(@green_parse_memo_valid)
755
+ @green_initial_state = 0 unless preserve_existing && defined?(@green_initial_state)
756
+ @syntax_parse_memo = nil unless preserve_existing && defined?(@syntax_parse_memo)
757
+ @green_reused_right_edge = false unless preserve_existing && defined?(@green_reused_right_edge)
758
+ @incremental_reused_descendants = 0 unless
759
+ preserve_existing && defined?(@incremental_reused_descendants)
760
+ end
761
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
762
+
763
+ # @rbs (ResourceLimits resource_limits) -> void
764
+ def validate_resource_limits!(resource_limits)
765
+ return if resource_limits.is_a?(ResourceLimits)
766
+
767
+ raise ArgumentError, "resource_limits must be an Ibex::Runtime::ResourceLimits"
768
+ end
769
+
770
+ # Keep the two historical value-stack names as read-compatible aliases.
771
+ # Applications must not mutate or replace these internal arrays.
772
+ # @rbs (Array[untyped] values) -> void
773
+ def install_value_stack(values)
774
+ @value_stack = values
775
+ @vstack = values
776
+ @racc_vstack = values
777
+ end
778
+
779
+ # @rbs (Integer token_id) -> bool
780
+ def exact_lookahead_accepted?(token_id)
781
+ stack = @state_stack.dup
782
+ seen = {} #: Hash[Array[Integer], bool]
783
+ remaining = exact_lookahead_step_budget(stack.length)
784
+ loop do
785
+ return false unless remaining.positive?
786
+ return false if seen.key?(stack)
787
+
788
+ remaining -= 1
789
+ seen[stack.dup.freeze] = true
790
+ state = stack.last
791
+ action = table_lookup(parser_tables.fetch(:actions), state, token_id) || default_action(state) || ERROR_ACTION
792
+ case action.first
793
+ when :shift, :accept then return true
794
+ when :error then return false
795
+ end
796
+ return false unless action.first == :reduce
797
+ return false unless exact_reduction_applied?(stack, action.fetch(1))
798
+ end
799
+ end
800
+
801
+ # @rbs (Integer stack_depth) -> Integer
802
+ def exact_lookahead_step_budget(stack_depth)
803
+ tables = parser_tables
804
+ production_count = tables.fetch(:productions).length
805
+ unless production_count.is_a?(Integer)
806
+ raise ParseError, "(tables):1:1: parser table production count must be an Integer"
807
+ end
808
+
809
+ (stack_depth + parser_state_count(tables) + 1) * (production_count + 1)
810
+ end
811
+
812
+ # @rbs (Hash[Symbol, untyped] tables) -> Integer
813
+ def parser_state_count(tables)
814
+ state_count = tables[:state_count]
815
+ return state_count if state_count.is_a?(Integer)
816
+
817
+ actions = tables.fetch(:actions)
818
+ state_count = actions.respond_to?(:row_count) ? actions.row_count : actions.length
819
+ return state_count if state_count.is_a?(Integer)
820
+
821
+ raise ParseError, "(tables):1:1: parser table state count must be an Integer"
822
+ end
823
+
824
+ # @rbs (Array[Integer] stack, Integer production_id) -> bool
825
+ def exact_reduction_applied?(stack, production_id)
826
+ production = parser_tables.fetch(:productions).fetch(production_id)
827
+ length = production.fetch(:length)
828
+ return false if length >= stack.length
829
+
830
+ if length.is_a?(Integer)
831
+ if length.negative?
832
+ stack.pop(length)
833
+ return false
834
+ end
835
+
836
+ remaining = length
837
+ while remaining.positive?
838
+ stack.pop
839
+ remaining -= 1
840
+ end
841
+ else
842
+ stack.pop(length)
843
+ end
844
+ target = table_lookup(parser_tables.fetch(:gotos), stack.last, production.fetch(:lhs))
845
+ return false unless target
846
+
847
+ stack << target
848
+ true
849
+ end
850
+
851
+ # @rbs (untyped source, ?initial_state: Integer?) -> untyped
852
+ def drive_parser(source, initial_state: nil)
853
+ ensure_runtime_initialized!
854
+ @runtime_observation_mutex.synchronize do
855
+ ensure_driver_available_without_lock!
856
+ if @push_status == :active
857
+ raise ParseError, "(input):1:1: cannot start another parser driver during an active push session"
858
+ end
859
+
860
+ @driver_status = :pull
861
+ @runtime_driver_thread = Thread.current
862
+ end
863
+
864
+ begin
865
+ prepare_parse(source, initial_state: initial_state)
866
+ tables = @runtime_parser_tables
867
+ if tables && compact_fast_driver_eligible?(tables)
868
+ fast_outcome = drive_compact_fast_parser(tables)
869
+ return @value_stack.last if fast_outcome.equal?(COMPACT_ACCEPTED)
870
+ return fast_outcome.fetch(1) if fast_outcome.is_a?(Array) &&
871
+ TERMINAL_OUTCOMES.include?(fast_outcome[0])
872
+ end
873
+ loop do
874
+ outcome = perform(action_for_current_state)
875
+ case outcome[0]
876
+ when :accepted, :done then return outcome[1]
877
+ end
878
+ end
879
+ ensure
880
+ @source = nil
881
+ @runtime_parser_tables = nil
882
+ @runtime_fast_path = false
883
+ release_driver
884
+ end
885
+ end
886
+
887
+ # Generated compact tables can keep the unobserved, location-free pull
888
+ # loop on their frozen displacement arrays. Any extension boundary or
889
+ # exceptional table shape returns control to the generic driver without
890
+ # replaying a committed action.
891
+ # rubocop:disable Metrics/AbcSize, Metrics/BlockNesting, Metrics/CyclomaticComplexity
892
+ # rubocop:disable Metrics/MethodLength, Metrics/PerceivedComplexity
893
+ # Direct comparisons avoid predicate-method dispatch inside the parser loop.
894
+ # rubocop:disable Style/BitwisePredicate, Style/NumericPredicate
895
+ # @rbs (Hash[Symbol, untyped] tables) -> (Object | [:accepted, untyped] | [:done, untyped])?
896
+ def drive_compact_fast_parser(tables)
897
+ actions = tables.fetch(:actions)
898
+ gotos = tables.fetch(:gotos)
899
+ productions = tables.fetch(:productions)
900
+ default_codes = tables.fetch(:compact_default_actions)
901
+ token_ids = tables.fetch(:tokens)
902
+ dense_action_codes = actions.dense_codes
903
+ action_column_count = actions.column_count
904
+ dense_goto_values = gotos.dense_values
905
+ goto_dense_width = gotos.dense_width
906
+ return unless dense_action_codes && action_column_count &&
907
+ dense_goto_values && goto_dense_width &&
908
+ (tables[:compact_action_encoding] == :signed || default_codes.empty?)
909
+
910
+ production_lhs_ids = productions.lhs_ids
911
+ production_lengths = productions.lengths
912
+ production_actions = productions.actions
913
+ production_flags = productions.flags
914
+ borrowed_values_flag = Ibex::Tables::CompactProductions::BORROWED_VALUES_ACTION
915
+ positional_action_flag = Ibex::Tables::CompactProductions::POSITIONAL_ACTION
916
+ states = @state_stack
917
+ values = @value_stack
918
+ stack_limit = @resource_limits.max_stack_depth
919
+ accept_code = Ibex::Tables::CompactActions::ACCEPT_CODE
920
+ error_code = Ibex::Tables::CompactActions::ERROR_CODE
921
+ shift_base = Ibex::Tables::CompactActions::SHIFT_BASE
922
+ reduce_base = Ibex::Tables::CompactActions::REDUCE_BASE
923
+
924
+ while @runtime_fast_path
925
+ if @lookahead.equal?(NO_LOOKAHEAD)
926
+ source = @source
927
+
928
+ token = begin
929
+ source ? source.call : next_token
930
+ rescue StopIteration
931
+ false
932
+ end
933
+ @runtime_fast_path = false if
934
+ @yydebug || @runtime_observers || @repair_policy || @location_stack ||
935
+ @semantic_error || @accept_requested
936
+ if token.nil? || token == false
937
+ @lookahead = EOF_TOKEN
938
+ @lookahead_value = nil
939
+ @lookahead_location = nil
940
+ else
941
+ external_token, @lookahead_value, @lookahead_location = token
942
+ if external_token.nil? || external_token == false
943
+ @lookahead = EOF_TOKEN
944
+ else
945
+ token_id = token_ids[external_token]
946
+ if token_id
947
+ @lookahead = token_id
948
+ else
949
+ @unknown_token_name = external_token.inspect
950
+ @unknown_token_id = -external_token.object_id.abs
951
+ @lookahead = @unknown_token_id
952
+ end
953
+ end
954
+ end
955
+ @runtime_fast_path = false unless nil.equal?(@lookahead_location)
956
+ unless @runtime_fast_path
957
+ materialize_compatible_lookahead
958
+ return
959
+ end
960
+ end
961
+ return if @unknown_token_id == @lookahead
962
+
963
+ state = states[-1]
964
+ code = dense_action_codes[(state * action_column_count) + @lookahead] ||
965
+ default_codes[state]
966
+ return unless code
967
+
968
+ if code == accept_code
969
+ return COMPACT_ACCEPTED
970
+ elsif code == error_code
971
+ return
972
+ elsif code > 0
973
+ ensure_stack_capacity! if states.length >= stack_limit
974
+ states << (code - shift_base)
975
+ values << @lookahead_value
976
+ @lookahead = NO_LOOKAHEAD
977
+ @lookahead_location = nil
978
+ @runtime_lookahead_token_display = nil
979
+ else
980
+ production_id = reduce_base - code
981
+ lhs = production_lhs_ids[production_id]
982
+ length = production_lengths[production_id]
983
+ return unless length <= values.length && length < states.length
984
+
985
+ semantic_action = production_actions[production_id]
986
+ if !semantic_action && length == 1
987
+ goto_row = states[-2]
988
+ next_state = dense_goto_values[(goto_row * goto_dense_width) + lhs]
989
+ raise ParseError, "(tables):1:1: missing goto for production #{production_id}" if next_state.nil?
990
+
991
+ states[-1] = next_state
992
+ next
993
+ end
994
+ hook_values = EMPTY_LOCATIONS
995
+ if semantic_action
996
+ flags = production_flags[production_id]
997
+ positional_action = (flags & positional_action_flag) != 0
998
+ if positional_action
999
+ case length
1000
+ when 1
1001
+ value0 = values[-1]
1002
+ when 2
1003
+ value0 = values[-2]
1004
+ value1 = values[-1]
1005
+ when 3
1006
+ value0 = values[-3]
1007
+ value1 = values[-2]
1008
+ value2 = values[-1]
1009
+ when 4
1010
+ value0 = values[-4]
1011
+ value1 = values[-3]
1012
+ value2 = values[-2]
1013
+ value3 = values[-1]
1014
+ end
1015
+ else
1016
+ reduction_values = values.last(length)
1017
+ hook_values = if (flags & borrowed_values_flag) == 0
1018
+ reduction_values.dup
1019
+ else
1020
+ reduction_values
1021
+ end
1022
+ end
1023
+ else
1024
+ positional_action = false
1025
+ result = length.zero? ? nil : values[-length]
1026
+ end
1027
+
1028
+ case length
1029
+ when 0
1030
+ nil
1031
+ when 1
1032
+ states.pop
1033
+ values.pop
1034
+ when 2
1035
+ states.pop
1036
+ states.pop
1037
+ values.pop
1038
+ values.pop
1039
+ when 3
1040
+ states.pop
1041
+ states.pop
1042
+ states.pop
1043
+ values.pop
1044
+ values.pop
1045
+ values.pop
1046
+ when 4
1047
+ states.pop
1048
+ states.pop
1049
+ states.pop
1050
+ states.pop
1051
+ values.pop
1052
+ values.pop
1053
+ values.pop
1054
+ values.pop
1055
+ else
1056
+ remaining = length
1057
+ while remaining > 0
1058
+ states.pop
1059
+ values.pop
1060
+ remaining -= 1
1061
+ end
1062
+ end
1063
+
1064
+ if semantic_action
1065
+ previous_locations = @semantic_locations
1066
+ previous_names = @semantic_location_names
1067
+ previous_result_location = @semantic_result_location
1068
+ @semantic_locations = EMPTY_LOCATIONS
1069
+ @semantic_location_names = EMPTY_LOCATION_NAMES
1070
+ @semantic_result_location = nil
1071
+ begin
1072
+ result = if positional_action
1073
+ case length
1074
+ when 0 then __send__(semantic_action)
1075
+ when 1 then __send__(semantic_action, value0)
1076
+ when 2 then __send__(semantic_action, value0, value1)
1077
+ when 3 then __send__(semantic_action, value0, value1, value2)
1078
+ when 4 then __send__(semantic_action, value0, value1, value2, value3)
1079
+ end
1080
+ else
1081
+ __send__(semantic_action, reduction_values)
1082
+ end
1083
+ ensure
1084
+ @semantic_locations = previous_locations
1085
+ @semantic_location_names = previous_names
1086
+ @semantic_result_location = previous_result_location
1087
+ end
1088
+ @runtime_fast_path = false if
1089
+ @yydebug || @runtime_observers || @repair_policy || @location_stack ||
1090
+ @semantic_error || @accept_requested
1091
+ end
1092
+
1093
+ goto_row = states[-1]
1094
+ next_state = dense_goto_values[(goto_row * goto_dense_width) + lhs]
1095
+ raise ParseError, "(tables):1:1: missing goto for production #{production_id}" if next_state.nil?
1096
+
1097
+ ensure_stack_capacity! if states.length >= stack_limit
1098
+ states << next_state
1099
+ location_stack = @location_stack
1100
+ location_stack << nil if location_stack
1101
+ values << result
1102
+ if semantic_action
1103
+ trace_reduction(production_id, length, lhs, result, next_state) if @yydebug
1104
+ unless @runtime_fast_path
1105
+ if positional_action
1106
+ case length
1107
+ when 0 then hook_values = EMPTY_LOCATIONS
1108
+ when 1 then hook_values = [value0]
1109
+ when 2 then hook_values = [value0, value1]
1110
+ when 3 then hook_values = [value0, value1, value2]
1111
+ when 4 then hook_values = [value0, value1, value2, value3]
1112
+ end
1113
+ end
1114
+ lookup = Object.instance_method(:method)
1115
+ unless runtime_method_unchanged?(lookup, :on_reduce, :__ibex_fast_path_on_reduce)
1116
+ on_reduce(production_id, hook_values, result)
1117
+ end
1118
+ unless runtime_method_unchanged?(lookup, :on_reduce_location, :__ibex_fast_path_on_reduce_location)
1119
+ on_reduce_location(production_id, hook_values, result, Array.new(length), nil)
1120
+ end
1121
+ end
1122
+ return accept_reduction(result) if @accept_requested
1123
+ return recover(report: false) if @semantic_error
1124
+
1125
+ next
1126
+ end
1127
+ end
1128
+ end
1129
+ nil
1130
+ end
1131
+ # rubocop:enable Metrics/AbcSize, Metrics/BlockNesting, Metrics/CyclomaticComplexity
1132
+ # rubocop:enable Metrics/MethodLength, Metrics/PerceivedComplexity
1133
+ # rubocop:enable Style/BitwisePredicate, Style/NumericPredicate
1134
+
1135
+ # @rbs (Hash[Symbol, untyped]? tables) -> bool
1136
+ def compact_fast_driver_eligible?(tables)
1137
+ return false unless @runtime_fast_path && tables
1138
+ return false unless tables[:compact_fast_driver] == true
1139
+ return false unless tables.fetch(:eager_reductions, EMPTY_ROW).empty?
1140
+ return false unless defined?(Ibex::Tables::Compact)
1141
+
1142
+ tables.fetch(:actions).instance_of?(Ibex::Tables::CompactActions) &&
1143
+ tables.fetch(:gotos).instance_of?(Ibex::Tables::Compact) &&
1144
+ tables.fetch(:productions).instance_of?(Ibex::Tables::CompactProductions) &&
1145
+ tables.fetch(:productions).direct_values?
1146
+ end
1147
+
1148
+ # @rbs () -> void
1149
+ def start_push_session
1150
+ if @push_status == :finished
1151
+ raise ParseError, "(input):1:1: push session is finished; call reset_push before supplying more input"
1152
+ end
1153
+ return if @push_status == :active
1154
+
1155
+ prepare_parse(-> { raise ParseError, "(input):1:1: push session needs another token" })
1156
+ @push_status = :active
1157
+ end
1158
+
1159
+ # @rbs () -> (:need_more | [:accepted, untyped] | [:rejected, untyped])
1160
+ def run_push_lookahead
1161
+ loop do
1162
+ outcome = perform(action_for_current_state)
1163
+ return :need_more if outcome.first == :repair_pending
1164
+
1165
+ case outcome.first
1166
+ when :accepted
1167
+ finish_push_session
1168
+ return [:accepted, outcome.fetch(1)]
1169
+ when :done
1170
+ finish_push_session
1171
+ return [:rejected, outcome.fetch(1)]
1172
+ end
1173
+ next unless @lookahead.equal?(NO_LOOKAHEAD)
1174
+
1175
+ repair_buffer = @repair_input_buffer
1176
+ next if repair_buffer && !repair_buffer.empty?
1177
+
1178
+ return :need_more
1179
+ end
1180
+ end
1181
+
1182
+ # @rbs () { () -> untyped } -> untyped
1183
+ def run_push_driver
1184
+ ensure_runtime_initialized!
1185
+ @runtime_observation_mutex.synchronize do
1186
+ ensure_driver_available_without_lock!
1187
+ @driver_status = :push
1188
+ @runtime_driver_thread = Thread.current
1189
+ end
1190
+ begin
1191
+ yield
1192
+ rescue StandardError
1193
+ finish_push_session
1194
+ raise
1195
+ ensure
1196
+ release_driver
1197
+ end
1198
+ end
1199
+
1200
+ # @rbs () -> void
1201
+ def ensure_driver_available_without_lock!
1202
+ return if @driver_status == :idle
1203
+
1204
+ raise ParseError, "(input):1:1: parser driver is already running"
1205
+ end
1206
+
1207
+ # @rbs () -> void
1208
+ def release_driver
1209
+ @runtime_observation_mutex.synchronize do
1210
+ @driver_status = :idle
1211
+ @runtime_driver_thread = nil
1212
+ end
1213
+ end
1214
+
1215
+ # @rbs () -> void
1216
+ def finish_push_session
1217
+ @push_status = :finished
1218
+ @source = nil
1219
+ @lookahead = NO_LOOKAHEAD
1220
+ @lookahead_location = nil
1221
+ @repair_input_buffer = nil
1222
+ @repair_selected = false
1223
+ @runtime_parser_tables = nil
1224
+ @runtime_fast_path = false
1225
+ clear_sync_recovery
1226
+ end
1227
+
1228
+ # @rbs ((^() -> untyped)? source, ?initial_state: Integer?) -> void
1229
+ def prepare_parse(source, initial_state: nil)
1230
+ @runtime_parser_tables = load_parser_tables
1231
+ tables = validate_parser_table_format!
1232
+ initial_state = resolve_initial_state(tables, initial_state)
1233
+
1234
+ @source = source
1235
+ @state_stack.clear << initial_state
1236
+ @value_stack.clear
1237
+ initialize_runtime_fast_path(tables)
1238
+ @lookahead = NO_LOOKAHEAD
1239
+ @lookahead_value = nil
1240
+ @lookahead_location = nil
1241
+ @runtime_lookahead_token_display = nil
1242
+ reset_parse_recovery_state
1243
+ @accept_requested = false
1244
+ @unknown_token_id = nil
1245
+ reset_cst_results(tables)
1246
+ @recovery_attempts = 0
1247
+ trace("start state #{initial_state}") if @yydebug
1248
+ @runtime_event_sequence = 0
1249
+ return unless @runtime_observers
1250
+
1251
+ emit_runtime_event(
1252
+ :start,
1253
+ {
1254
+ "driver" => @driver_status.to_s,
1255
+ "initial_state" => initial_state,
1256
+ "table_format_version" => tables.fetch(:format_version),
1257
+ "grammar_digest" => tables[:grammar_digest],
1258
+ "state_count" => tables[:state_count],
1259
+ "production_count" => tables[:production_count]
1260
+ }
1261
+ )
1262
+ end
1263
+
1264
+ # @rbs () -> void
1265
+ def reset_parse_recovery_state
1266
+ @recovery_shifts = 0
1267
+ @semantic_error = false
1268
+ @repair_input_buffer = @repair_policy ? [] : nil
1269
+ @repair_selected = false
1270
+ clear_sync_recovery
1271
+ end
1272
+
1273
+ # @rbs (Hash[Symbol, untyped] tables, Integer? requested) -> Integer
1274
+ def resolve_initial_state(tables, requested)
1275
+ initial_state = requested || tables[:initial_state] || 0
1276
+ return initial_state if initial_state.is_a?(Integer) &&
1277
+ initial_state.between?(0, parser_state_count(tables) - 1)
1278
+
1279
+ raise ParseError, "(tables):1:1: parser initial state #{initial_state.inspect} is invalid"
1280
+ end
1281
+
1282
+ # @rbs () -> Hash[Symbol, untyped]
1283
+ def validate_parser_table_format!
1284
+ tables = parser_tables
1285
+ unless tables.key?(:format_version)
1286
+ raise ParseError,
1287
+ "(tables):1:1: parser tables for #{self.class} are missing :format_version; " \
1288
+ "regenerate the parser with the installed Ibex version"
1289
+ end
1290
+
1291
+ actual = tables.fetch(:format_version)
1292
+ unless SUPPORTED_PARSER_TABLE_FORMAT_VERSIONS.include?(actual)
1293
+ raise ParseError,
1294
+ "(tables):1:1: unsupported parser table format version #{actual.inspect} for #{self.class}; " \
1295
+ "runtime supports #{SUPPORTED_PARSER_TABLE_FORMAT_VERSIONS.join(', ')}; " \
1296
+ "regenerate the parser with the installed Ibex version"
1297
+ end
1298
+
1299
+ validate_current_cst_tables!(tables, actual)
1300
+ if actual >= 3 && !generated_action_contracts_validated?(tables)
1301
+ validate_generated_action_contracts!(tables, actual)
1302
+ cache_generated_action_contracts!(tables)
1303
+ end
1304
+ tables
1305
+ end
1306
+
1307
+ # @rbs (Hash[Symbol, untyped] tables, Integer actual) -> void
1308
+ def validate_current_cst_tables!(tables, actual)
1309
+ cst = tables[:cst]
1310
+ return if cst.nil? || cst == false
1311
+ return if actual == PARSER_TABLE_FORMAT_VERSION && cst.is_a?(Hash)
1312
+
1313
+ raise ParseError,
1314
+ "(tables):1:1: legacy CST parser tables are unsupported; " \
1315
+ "regenerate the parser with the installed Ibex version"
1316
+ end
1317
+
1318
+ # Generated parser tables are deeply frozen before they can cross a
1319
+ # Ractor boundary. Their action-marker contract only needs one scan per
1320
+ # parser class and exact table object, including across parser instances.
1321
+ # Mutable application tables remain intentionally uncached.
1322
+ # @rbs (Hash[Symbol, untyped] tables) -> bool
1323
+ def generated_action_contracts_validated?(tables)
1324
+ return false unless generated_action_contract_cache_accessible?
1325
+
1326
+ cached = self.class.instance_variable_get(:@__ibex_validated_parser_tables)
1327
+ cached.equal?(tables)
1328
+ end
1329
+
1330
+ # @rbs (Hash[Symbol, untyped] tables) -> void
1331
+ def cache_generated_action_contracts!(tables)
1332
+ shareable = defined?(Ractor) && Ractor.respond_to?(:shareable?) && Ractor.shareable?(tables)
1333
+ return unless shareable
1334
+ return unless generated_action_contract_cache_accessible?
1335
+
1336
+ self.class.instance_variable_set(:@__ibex_validated_parser_tables, tables)
1337
+ rescue FrozenError
1338
+ nil
1339
+ end
1340
+
1341
+ # Ruby 3.0-3.3 expose the main Ractor as an object instead of exposing
1342
+ # Ractor.main?. Class instance variables cannot be read or written from
1343
+ # another Ractor on those versions.
1344
+ # @rbs () -> bool
1345
+ def generated_action_contract_cache_accessible?
1346
+ return true unless Object.const_defined?(:Ractor, false)
1347
+
1348
+ ractor = Object.const_get(:Ractor)
1349
+ return ractor.__send__(:main?) if ractor.respond_to?(:main?)
1350
+ return ractor.__send__(:current).equal?(ractor.__send__(:main)) if
1351
+ ractor.respond_to?(:current) && ractor.respond_to?(:main)
1352
+
1353
+ true
1354
+ end
1355
+
1356
+ # @rbs (Hash[Symbol, untyped] tables, Integer version) -> void
1357
+ def validate_generated_action_contracts!(tables, version)
1358
+ tables.fetch(:productions).each_with_index do |production, index|
1359
+ validate_composition_action_contract!(production, index, version)
1360
+ validate_values_action_contract!(production, index, version)
1361
+ validate_positional_action_contract!(production, index, version)
1362
+ end
1363
+ end
1364
+
1365
+ # @rbs (Hash[Symbol, untyped] production, Integer index, Integer version) -> void
1366
+ def validate_composition_action_contract!(production, index, version)
1367
+ return unless production[:composition_action] == true
1368
+ return if production[:location_action] == true && generated_action_symbol?(production[:action])
1369
+
1370
+ raise ParseError,
1371
+ "(tables):1:1: parser table format version #{version} production #{index} has an inconsistent " \
1372
+ ":composition_action marker; a generated action Symbol with :location_action is required"
1373
+ end
1374
+
1375
+ # @rbs (Hash[Symbol, untyped] production, Integer index, Integer version) -> void
1376
+ def validate_values_action_contract!(production, index, version)
1377
+ borrowed = production[:borrowed_values_action] == true
1378
+ if borrowed && production[:values_action] != true
1379
+ raise ParseError,
1380
+ "(tables):1:1: parser table format version #{version} production #{index} has an inconsistent " \
1381
+ ":borrowed_values_action marker; :values_action is required"
1382
+ end
1383
+ return if version < 4 || production[:values_action] != true
1384
+ return if generated_action_symbol?(production[:action]) &&
1385
+ production[:location_action] != true &&
1386
+ production[:composition_action] != true &&
1387
+ production.fetch(:location_context_length, 0).zero?
1388
+
1389
+ raise ParseError,
1390
+ "(tables):1:1: parser table format version #{version} production #{index} has an inconsistent " \
1391
+ ":values_action marker; a generated action Symbol without location, composition, or context " \
1392
+ "markers is required"
1393
+ end
1394
+
1395
+ # @rbs (Hash[Symbol, untyped] production, Integer index, Integer version) -> void
1396
+ def validate_positional_action_contract!(production, index, version)
1397
+ return if version < 5 || production[:positional_action] != true
1398
+
1399
+ return if positional_action_contract?(production)
1400
+
1401
+ raise ParseError,
1402
+ "(tables):1:1: parser table format version #{version} production #{index} has an inconsistent " \
1403
+ ":positional_action marker; a generated action Symbol with zero to four RHS values and no other " \
1404
+ "action ABI markers is required"
1405
+ end
1406
+
1407
+ # @rbs (Hash[Symbol, untyped] production) -> bool
1408
+ def positional_action_contract?(production)
1409
+ production[:length].is_a?(Integer) &&
1410
+ production[:length].between?(0, 4) &&
1411
+ generated_action_symbol?(production[:action]) &&
1412
+ production[:values_action] != true &&
1413
+ production[:borrowed_values_action] != true &&
1414
+ production[:location_action] != true &&
1415
+ production[:composition_action] != true &&
1416
+ production.fetch(:location_context_length, 0).zero?
1417
+ end
1418
+
1419
+ # @rbs () -> untyped
1420
+ def action_for_current_state
1421
+ loop do
1422
+ if @lookahead.equal?(NO_LOOKAHEAD)
1423
+ state = @state_stack.last
1424
+ eager = parser_tables.fetch(:eager_reductions, EMPTY_ROW)[state]
1425
+ return eager if eager
1426
+
1427
+ read_lookahead
1428
+ next if @lookahead.equal?(NO_LOOKAHEAD)
1429
+ end
1430
+ return SYNC_RECOVER_ACTION if sync_recovery_active?
1431
+
1432
+ state = @state_stack.last
1433
+ return ERROR_ACTION unless parser_tables.fetch(:token_names).key?(@lookahead)
1434
+
1435
+ explicit = table_lookup(parser_tables.fetch(:actions), state, @lookahead)
1436
+ return explicit if explicit
1437
+
1438
+ return default_action(state) || ERROR_ACTION
1439
+ end
1440
+ end
1441
+
1442
+ # @rbs (untyped action) -> untyped
1443
+ def perform(action)
1444
+ case action.first
1445
+ when :shift
1446
+ next_state = action.fetch(1)
1447
+ @runtime_fast_path ? fast_shift(next_state) : shift(next_state)
1448
+ when :reduce
1449
+ production_id = action.fetch(1)
1450
+ if @runtime_fast_path
1451
+ production = parser_tables.fetch(:productions).fetch(production_id)
1452
+ length = production.fetch(:length)
1453
+ return fast_reduce(production_id, production, length) if fast_reduction_eligible?(production, length)
1454
+ end
1455
+
1456
+ reduce(production_id, production)
1457
+ when :accept
1458
+ result = finalize_cst(@value_stack.last)
1459
+ emit_accept_event(EventSanitizer.value(result), "table") if @runtime_observers
1460
+ [:accepted, result]
1461
+ when :error
1462
+ materialize_lookahead_token_display!
1463
+ recover
1464
+ when :sync_recover
1465
+ materialize_lookahead_token_display!
1466
+ continue_sync_recovery
1467
+ else raise ParseError, "(tables):1:1: unknown parser action #{action.inspect}"
1468
+ end
1469
+ end
1470
+
1471
+ # @rbs (Integer next_state) -> [:continue]
1472
+ def fast_shift(next_state)
1473
+ value = @lookahead_value
1474
+ ensure_stack_capacity!
1475
+ @state_stack << next_state
1476
+ @value_stack << value
1477
+ @lookahead = NO_LOOKAHEAD
1478
+ @lookahead_location = nil
1479
+ @runtime_lookahead_token_display = nil
1480
+ @recovery_shifts -= 1 if @recovery_shifts.positive?
1481
+ CONTINUE_OUTCOME
1482
+ end
1483
+
1484
+ # @rbs (Hash[Symbol, untyped] production, untyped length) -> bool
1485
+ def fast_reduction_eligible?(production, length)
1486
+ action = production[:action]
1487
+ return false if action && !generated_values_action?(production, action)
1488
+
1489
+ length.is_a?(Integer) && !length.negative? &&
1490
+ length <= @value_stack.length && length < @state_stack.length
1491
+ end
1492
+
1493
+ # @rbs (Integer production_id, Hash[Symbol, untyped] production, Integer length) -> [:continue]
1494
+ def fast_reduce(production_id, production, length)
1495
+ action = production[:action]
1496
+ return fast_values_reduce(production_id, production, length, action) if action
1497
+
1498
+ result = length.zero? ? nil : @value_stack.fetch(@value_stack.length - length)
1499
+ remaining = length
1500
+ while remaining.positive?
1501
+ @state_stack.pop
1502
+ @value_stack.pop
1503
+ remaining -= 1
1504
+ end
1505
+ next_state = table_lookup(parser_tables.fetch(:gotos), @state_stack.last, production.fetch(:lhs))
1506
+ raise ParseError, "(tables):1:1: missing goto for production #{production_id}" if next_state.nil?
1507
+
1508
+ ensure_stack_capacity!
1509
+ @state_stack << next_state
1510
+ @value_stack << result
1511
+ CONTINUE_OUTCOME
1512
+ end
1513
+
1514
+ # @rbs (Integer production_id, Hash[Symbol, untyped] production, Integer length, Symbol action) -> untyped
1515
+ def fast_values_reduce(production_id, production, length, action)
1516
+ hook_values = @value_stack.last(length)
1517
+ values = hook_values.dup
1518
+ remaining = length
1519
+ while remaining.positive?
1520
+ @state_stack.pop
1521
+ @value_stack.pop
1522
+ remaining -= 1
1523
+ end
1524
+ result = values_reduction_value(production, action, values, EMPTY_LOCATIONS, nil)
1525
+ refresh_runtime_fast_path_after_user_code!
1526
+ next_state = table_lookup(parser_tables.fetch(:gotos), @state_stack.last, production.fetch(:lhs))
1527
+ raise ParseError, "(tables):1:1: missing goto for production #{production_id}" if next_state.nil?
1528
+
1529
+ ensure_stack_capacity!
1530
+ push_reduction_result(next_state, result, nil)
1531
+ trace_reduction(production_id, length, production.fetch(:lhs), result, next_state)
1532
+ unless @runtime_fast_path
1533
+ lookup = Object.instance_method(:method)
1534
+ unless runtime_method_unchanged?(lookup, :on_reduce, :__ibex_fast_path_on_reduce)
1535
+ on_reduce(production_id, hook_values, result)
1536
+ end
1537
+ unless runtime_method_unchanged?(lookup, :on_reduce_location, :__ibex_fast_path_on_reduce_location)
1538
+ on_reduce_location(production_id, hook_values, result, Array.new(length), nil)
1539
+ end
1540
+ end
1541
+ return accept_reduction(result) if @accept_requested
1542
+ return recover(report: false) if @semantic_error
1543
+
1544
+ CONTINUE_OUTCOME
1545
+ end
1546
+
1547
+ # @rbs (Integer next_state) -> untyped
1548
+ def shift(next_state)
1549
+ token_id = @lookahead
1550
+ value = @lookahead_value
1551
+ location = @lookahead_location
1552
+ from_state = @state_stack.last
1553
+ token_display = token_to_str(token_id)
1554
+ trace("shift #{token_display}#{trace_value_suffix(value, token_id)} -> state #{next_state}") if @yydebug
1555
+ event_observers = runtime_observer_snapshot if @runtime_observers
1556
+ event_data = if event_observers
1557
+ runtime_token_data(
1558
+ token_id: token_id,
1559
+ token_display: token_display,
1560
+ value: value,
1561
+ location: location,
1562
+ state: next_state
1563
+ ).merge("from_state" => @state_stack.last).freeze
1564
+ end
1565
+ ensure_stack_capacity!
1566
+ @state_stack << next_state
1567
+ push_location(location)
1568
+ @value_stack << value
1569
+ shift_green_token(token_id, value, location, from_state)
1570
+ @lookahead = NO_LOOKAHEAD
1571
+ @lookahead_location = nil
1572
+ @runtime_lookahead_token_display = nil
1573
+ @recovery_shifts -= 1 if @recovery_shifts.positive?
1574
+ on_shift(token_id, value, next_state)
1575
+ on_shift_location(token_id, value, next_state, location)
1576
+ emit_runtime_event(:shift, event_data, observers: event_observers) if event_data
1577
+ CONTINUE_OUTCOME
1578
+ end
1579
+
1580
+ # @rbs (Integer production_id, ?Hash[Symbol, untyped]? prefetched_production) -> untyped
1581
+ def reduce(production_id, prefetched_production = nil) # rubocop:disable Metrics -- allocation-free hot path.
1582
+ production = prefetched_production || parser_tables.fetch(:productions).fetch(production_id)
1583
+ length = production.fetch(:length)
1584
+ event_observers = runtime_observer_snapshot if @runtime_observers
1585
+ pre_state = @state_stack.last if event_observers
1586
+ values = @value_stack.last(length)
1587
+ locations = pop_reduction_locations(length)
1588
+ hook_values = values.dup
1589
+ # Array#last above preserves the native negative-length exception before either stack mutates.
1590
+ if length.is_a?(Integer) && !length.negative? &&
1591
+ length <= @value_stack.length && length < @state_stack.length
1592
+ remaining = length
1593
+ while remaining.positive?
1594
+ @state_stack.pop
1595
+ @value_stack.pop
1596
+ remaining -= 1
1597
+ end
1598
+ else
1599
+ @state_stack.pop(length)
1600
+ @value_stack.pop(length)
1601
+ end
1602
+ post_state = @state_stack.last if event_observers
1603
+ location = LocationSpan.for_reduction(locations, lookahead: @lookahead_location)
1604
+ result = reduction_value(production_id, production, values, locations, location)
1605
+ refresh_runtime_fast_path_after_user_code!
1606
+ reduce_green(production_id, production, length, @state_stack.last)
1607
+ next_state = table_lookup(parser_tables.fetch(:gotos), @state_stack.last, production.fetch(:lhs))
1608
+ raise ParseError, "(tables):1:1: missing goto for production #{production_id}" if next_state.nil?
1609
+
1610
+ ensure_stack_capacity!
1611
+ push_reduction_result(next_state, result, location)
1612
+ trace_reduction(production_id, length, production.fetch(:lhs), result, next_state)
1613
+ event_data = build_reduce_event_data(
1614
+ event_observers, production_id, production, length, pre_state, post_state, next_state, result, location
1615
+ )
1616
+ on_reduce(production_id, hook_values, result)
1617
+ on_reduce_location(production_id, hook_values, result, locations.dup, location)
1618
+ emit_runtime_event(:reduce, event_data, observers: event_observers) if event_data
1619
+ return accept_reduction(result) if @accept_requested
1620
+ return recover(report: false) if @semantic_error
1621
+
1622
+ CONTINUE_OUTCOME
1623
+ end
1624
+
1625
+ # @rbs (Integer next_state, untyped result, untyped location) -> void
1626
+ def push_reduction_result(next_state, result, location)
1627
+ @state_stack << next_state
1628
+ push_location(location)
1629
+ @value_stack << result
1630
+ end
1631
+
1632
+ # @rbs (Integer production_id, Integer length, Integer lhs, untyped result, Integer next_state) -> void
1633
+ def trace_reduction(production_id, length, lhs, result, next_state)
1634
+ return unless @yydebug
1635
+
1636
+ trace("reduce #{production_id} (#{length})#{trace_value_suffix(result, lhs)} -> state #{next_state}")
1637
+ end
1638
+
1639
+ # @rbs (Array[Proc]? observers, Integer production_id, Hash[Symbol, untyped] production, Integer length,
1640
+ # Integer? pre_state, Integer? post_state, Integer next_state, untyped result,
1641
+ # LocationSpan? location) -> Hash[String, untyped]?
1642
+ def build_reduce_event_data(
1643
+ observers, production_id, production, length, pre_state, post_state, next_state, result, location
1644
+ )
1645
+ return unless observers
1646
+
1647
+ runtime_reduce_data(
1648
+ production_id, production, length, pre_state, post_state, next_state, result, location
1649
+ )
1650
+ end
1651
+
1652
+ # @rbs (Integer production_id, Hash[Symbol, untyped] production, Integer length,
1653
+ # Integer? pre_state, Integer? post_state, Integer next_state, untyped result,
1654
+ # LocationSpan? location) -> Hash[String, untyped]
1655
+ def runtime_reduce_data(production_id, production, length, pre_state, post_state, next_state, result, location)
1656
+ {
1657
+ "production_id" => production_id,
1658
+ "lhs" => production.fetch(:lhs),
1659
+ "rhs_length" => length,
1660
+ "pre_state" => pre_state,
1661
+ "post_state" => post_state,
1662
+ "goto_state" => next_state,
1663
+ "result" => EventSanitizer.value(result),
1664
+ "location" => EventSanitizer.location(location)
1665
+ }.freeze
1666
+ end
1667
+
1668
+ # @rbs (untyped result) -> [:accepted, untyped]
1669
+ def accept_reduction(result)
1670
+ result = finalize_cst(result)
1671
+ emit_accept_event(EventSanitizer.value(result), "semantic") if @runtime_observers
1672
+ [:accepted, result]
1673
+ end
1674
+
1675
+ # @rbs (Integer production_id, Hash[Symbol, untyped] production, Array[untyped] values,
1676
+ # Array[untyped] locations, LocationSpan? location) -> untyped
1677
+ def reduction_value(production_id, production, values, locations, location) # rubocop:disable Metrics -- explicit hot path.
1678
+ previous_locations = @semantic_locations
1679
+ previous_names = @semantic_location_names
1680
+ previous_result_location = @semantic_result_location
1681
+ return values.first if @syntax_only
1682
+
1683
+ action = production[:action]
1684
+ return actionless_reduction_value(production_id, production, values, locations, location) unless action
1685
+
1686
+ context_length = production.fetch(:location_context_length, 0)
1687
+ action_locations = if context_length.positive?
1688
+ (@location_stack || []).last(context_length)
1689
+ else
1690
+ locations
1691
+ end
1692
+ @semantic_locations = action_locations
1693
+ @semantic_location_names = production.fetch(:location_names, EMPTY_LOCATION_NAMES)
1694
+ @semantic_result_location = location
1695
+ return __send__(action, *values) if generated_positional_action?(production, action)
1696
+ return __send__(action, values) if generated_values_action?(production, action)
1697
+
1698
+ value_stack = @value_stack.dup
1699
+ callable = action.respond_to?(:call)
1700
+ if generated_location_action?(production, action)
1701
+ location_stack = (@location_stack || []).dup
1702
+ if generated_composition_action?(production, action)
1703
+ if callable
1704
+ return instance_exec(
1705
+ values, value_stack, locations, location_stack, location, @lookahead_location, &action
1706
+ )
1707
+ end
1708
+
1709
+ return __send__(action, values, value_stack, locations, location_stack, location, @lookahead_location)
1710
+ end
1711
+
1712
+ return instance_exec(values, value_stack, locations, location_stack, location, &action) if callable
1713
+
1714
+ return __send__(action, values, value_stack, locations, location_stack, location)
1715
+ end
1716
+
1717
+ callable ? instance_exec(values, value_stack, &action) : __send__(action, values, value_stack)
1718
+ ensure
1719
+ @semantic_locations = previous_locations
1720
+ @semantic_location_names = previous_names
1721
+ @semantic_result_location = previous_result_location
1722
+ end
1723
+
1724
+ # @rbs (Hash[Symbol, untyped] production, Symbol action, Array[untyped] values,
1725
+ # Array[untyped] locations, LocationSpan? location) -> untyped
1726
+ def values_reduction_value(production, action, values, locations, location)
1727
+ previous_locations = @semantic_locations
1728
+ previous_names = @semantic_location_names
1729
+ previous_result_location = @semantic_result_location
1730
+ @semantic_locations = locations
1731
+ @semantic_location_names = production.fetch(:location_names, EMPTY_LOCATION_NAMES)
1732
+ @semantic_result_location = location
1733
+ __send__(action, values)
1734
+ ensure
1735
+ @semantic_locations = previous_locations
1736
+ @semantic_location_names = previous_names
1737
+ @semantic_result_location = previous_result_location
1738
+ end
1739
+
1740
+ # @rbs (Integer production_id, Hash[Symbol, untyped] production, Array[untyped] values,
1741
+ # Array[untyped] locations, LocationSpan? location) -> untyped
1742
+ def actionless_reduction_value(_production_id, _production, values, _locations, _location)
1743
+ values.first
1744
+ end
1745
+
1746
+ # @rbs (Hash[Symbol, untyped] production, untyped action) -> bool
1747
+ def generated_location_action?(production, action)
1748
+ parser_tables.fetch(:format_version) >= 2 &&
1749
+ production[:location_action] == true &&
1750
+ generated_action_symbol?(action)
1751
+ end
1752
+
1753
+ # @rbs (Hash[Symbol, untyped] production, untyped action) -> bool
1754
+ def generated_values_action?(production, action)
1755
+ parser_tables.fetch(:format_version) >= 4 &&
1756
+ production[:values_action] == true &&
1757
+ generated_action_symbol?(action)
1758
+ end
1759
+
1760
+ # @rbs (Hash[Symbol, untyped] production, untyped action) -> bool
1761
+ def generated_positional_action?(production, action)
1762
+ parser_tables.fetch(:format_version) >= 5 &&
1763
+ production[:positional_action] == true &&
1764
+ generated_action_symbol?(action)
1765
+ end
1766
+
1767
+ # @rbs (Hash[Symbol, untyped] production, untyped action) -> bool
1768
+ def generated_composition_action?(production, action)
1769
+ parser_tables.fetch(:format_version) >= 3 &&
1770
+ production[:composition_action] == true &&
1771
+ generated_location_action?(production, action)
1772
+ end
1773
+
1774
+ # @rbs (untyped action) -> bool
1775
+ def generated_action_symbol?(action)
1776
+ action.is_a?(Symbol) && action.name.match?(GENERATED_ACTION_NAME)
1777
+ end
1778
+
1779
+ # @rbs () -> bool
1780
+ def cst_enabled?
1781
+ parser_tables[:cst].is_a?(Hash)
1782
+ end
1783
+
1784
+ # @rbs (Integer token_id, untyped value, untyped location, Symbol reason) -> void
1785
+ def capture_cst_error(token_id, value, location, reason)
1786
+ return unless cst_enabled?
1787
+ return if token_id == EOF_TOKEN
1788
+
1789
+ @syntax_diagnostics << {
1790
+ token_id: token_id, value: value, location: location, reason: reason
1791
+ }.freeze
1792
+ end
1793
+
1794
+ # @rbs (untyped value) -> untyped
1795
+ def finalize_cst(value)
1796
+ return value unless cst_enabled?
1797
+
1798
+ finalize_red_green_cst(value)
1799
+ end
1800
+
1801
+ # @rbs () -> nil
1802
+ def failed_cst
1803
+ finalize_failed_green_cst
1804
+ nil
1805
+ end
1806
+
1807
+ # @rbs (ParseError error) -> nil
1808
+ def cst_lexical_failure(error)
1809
+ @syntax_diagnostics << error
1810
+ finalize_lexical_green_cst(error)
1811
+ nil
1812
+ end
1813
+
1814
+ # @rbs (untyped location, Symbol key) -> untyped
1815
+ def cst_location_value(location, key)
1816
+ if location.is_a?(Hash)
1817
+ value = location[key]
1818
+ return value if value || location.key?(key)
1819
+ return if location.key?(:ibex_lexer_start_state)
1820
+
1821
+ return location[key.name]
1822
+ end
1823
+ return location.public_send(key) if location.respond_to?(key)
1824
+
1825
+ nil
1826
+ end
1827
+
1828
+ # @rbs (Hash[Symbol, untyped] tables) -> void
1829
+ def prepare_green_cst(tables)
1830
+ @syntax_root = nil
1831
+ @syntax_parse_memo = nil
1832
+ @syntax_diagnostics.clear
1833
+ @green_pending_skipped.clear
1834
+ config = tables[:cst]
1835
+ unless config.is_a?(Hash)
1836
+ @green_builder = nil
1837
+ @green_kinds = nil
1838
+ @green_cache = nil
1839
+ return
1840
+ end
1841
+
1842
+ kinds = tables[:cst_kinds]
1843
+ kinds = CST::Kind.new(config.fetch(:kinds), slots: config.fetch(:slots)) unless kinds.is_a?(CST::Kind)
1844
+ cache = @green_cache_override || CST::NodeCache.new
1845
+ @green_kinds = kinds
1846
+ @green_cache = cache
1847
+ @green_token_states.clear
1848
+ @green_memo_stack.clear
1849
+ @green_parse_memo_valid = true
1850
+ @green_initial_state = @state_stack.first || 0
1851
+ @green_reused_right_edge = false
1852
+ @incremental_reused_descendants = 0
1853
+ @green_builder = CST::GreenBuilder.new(kinds: kinds, cache: cache)
1854
+ end
1855
+
1856
+ # @rbs (Hash[Symbol, untyped] tables) -> void
1857
+ def reset_cst_results(tables)
1858
+ prepare_green_cst(tables)
1859
+ end
1860
+
1861
+ # rubocop:disable Metrics/PerceivedComplexity -- generated locations have a deliberate allocation-free path.
1862
+ # @rbs (Integer token_id, untyped value, untyped location, Integer from_state) -> void
1863
+ def shift_green_token(token_id, value, location, from_state)
1864
+ builder = @green_builder
1865
+ kinds = @green_kinds
1866
+ return unless builder && kinds
1867
+
1868
+ if location.is_a?(Hash) && location.key?(:ibex_lexer_start_state)
1869
+ trailing = location[:cst_previous_trailing] || EMPTY_GREEN_TRIVIA
1870
+ location_leading = location[:leading_trivia] || EMPTY_GREEN_TRIVIA
1871
+ repair = location[:ibex_repair]
1872
+ token_text = location[:ibex_cst_text]
1873
+ token_text = value unless token_text.is_a?(String)
1874
+ token_text = "" unless token_text.is_a?(String)
1875
+ lexer_state = location[:ibex_lexer_start_state]
1876
+ lexer_state = lexer_state ? lexer_state.to_sym : :INITIAL
1877
+ else
1878
+ trailing = green_location_trivia(location, :cst_previous_trailing)
1879
+ location_leading = green_location_trivia(location, :leading_trivia)
1880
+ repair = cst_location_value(location, :ibex_repair)
1881
+ token_text = green_token_text(value, location)
1882
+ lexer_state = green_lexer_state(location)
1883
+ end
1884
+
1885
+ commit_green_token_shift(
1886
+ builder, kinds, token_id, from_state, trailing, location_leading, repair, token_text, lexer_state
1887
+ )
1888
+ end
1889
+ # rubocop:enable Metrics/PerceivedComplexity
1890
+
1891
+ # @rbs (CST::GreenBuilder builder, CST::Kind kinds, Integer token_id, Integer from_state,
1892
+ # Array[CST::GreenTrivia] trailing, Array[CST::GreenTrivia] location_leading, untyped repair,
1893
+ # String token_text, Symbol lexer_state) -> void
1894
+ def commit_green_token_shift(
1895
+ builder, kinds, token_id, from_state, trailing, location_leading, repair, token_text, lexer_state
1896
+ )
1897
+ builder.append_trailing_to_last_token(trailing) if !@green_reused_right_edge && !trailing.empty?
1898
+ if repair == :insert
1899
+ builder.missing(token_id)
1900
+ @green_token_states << lexer_state
1901
+ @green_memo_stack << CST::ParseMemo::Entry.new(from_state)
1902
+ return
1903
+ end
1904
+
1905
+ if @green_pending_skipped.empty?
1906
+ leading = location_leading
1907
+ flags = 0
1908
+ else
1909
+ leading = @green_pending_skipped + location_leading
1910
+ @green_pending_skipped.clear
1911
+ flags = CST::Flags::CONTAINS_SKIPPED
1912
+ end
1913
+ flags |= CST::Flags::CONTAINS_ERROR if repair == :replace || token_id == ERROR_TOKEN
1914
+ kind = token_id.negative? ? kinds.fetch(:lexical_error_token) : token_id
1915
+ builder.token(kind, token_text, leading: leading, flags: flags)
1916
+ @green_token_states << lexer_state
1917
+ @green_memo_stack << CST::ParseMemo::Entry.new(from_state)
1918
+ @green_reused_right_edge = false
1919
+ end
1920
+
1921
+ # @rbs (Integer production_id, Hash[Symbol, untyped] production, Integer length, Integer left_state) -> void
1922
+ def reduce_green(production_id, production, length, left_state)
1923
+ builder = @green_builder
1924
+ return unless builder
1925
+
1926
+ config = parser_tables.fetch(:cst)
1927
+ slot = config.fetch(:slots).fetch(production_id, nil)
1928
+ kind = slot ? slot.fetch(:node_kind) : production.fetch(:lhs)
1929
+ flags = length.zero? ? CST::Flags::SYNTHETIC : 0
1930
+ builder.node(kind, length, flags: flags)
1931
+ entry = if length.zero?
1932
+ CST::ParseMemo::Entry.new(left_state)
1933
+ else
1934
+ CST::ParseMemo::Entry.new(left_state, children: @green_memo_stack.pop(length).freeze)
1935
+ end
1936
+ @green_memo_stack << entry
1937
+ end
1938
+
1939
+ # @rbs (untyped value, untyped location) -> String
1940
+ def green_token_text(value, location)
1941
+ text = if location.is_a?(Hash) && location.key?(:ibex_lexer_start_state)
1942
+ location[:ibex_cst_text]
1943
+ else
1944
+ cst_location_value(location, :ibex_cst_text)
1945
+ end
1946
+ return text if text.is_a?(String)
1947
+ return value if value.is_a?(String)
1948
+
1949
+ ""
1950
+ end
1951
+
1952
+ # @rbs (untyped location, Symbol key) -> Array[CST::GreenTrivia]
1953
+ def green_location_trivia(location, key)
1954
+ trivia = if location.is_a?(Hash) && location.key?(:ibex_lexer_start_state)
1955
+ location[key]
1956
+ else
1957
+ cst_location_value(location, key)
1958
+ end
1959
+ return EMPTY_GREEN_TRIVIA unless trivia.is_a?(Array)
1960
+ return trivia if trivia.frozen?
1961
+ return trivia if trivia.all?(CST::GreenTrivia)
1962
+
1963
+ trivia.grep(CST::GreenTrivia)
1964
+ end
1965
+
1966
+ # @rbs (untyped value) -> untyped
1967
+ def finalize_red_green_cst(value)
1968
+ builder = @green_builder || raise(ParseError, "(cst):1:1: Green builder is unavailable")
1969
+ kinds = @green_kinds || raise(ParseError, "(cst):1:1: kind metadata is unavailable")
1970
+ incomplete = @lookahead != EOF_TOKEN
1971
+ unless incomplete || @green_reused_right_edge
1972
+ builder.append_trailing_to_last_token(green_location_trivia(@lookahead_location, :cst_previous_trailing))
1973
+ end
1974
+ empty_trivia = [] #: Array[CST::GreenTrivia]
1975
+ eof_leading = incomplete ? empty_trivia : green_location_trivia(@lookahead_location, :leading_trivia)
1976
+ if @green_pending_skipped.empty?
1977
+ leading = eof_leading
1978
+ else
1979
+ leading = @green_pending_skipped + eof_leading
1980
+ @green_pending_skipped.clear
1981
+ end
1982
+ eof = builder.make_token(EOF_TOKEN, "", leading: leading)
1983
+ @green_token_states << green_lexer_state(@lookahead_location)
1984
+ green = if builder.size == 1
1985
+ builder.finish_source_file(eof, incomplete: incomplete)
1986
+ else
1987
+ builder.finish_synthetic_root([eof], incomplete: incomplete)
1988
+ end
1989
+ install_syntax_root(green, kinds)
1990
+ install_parse_memo(green)
1991
+ value
1992
+ end
1993
+
1994
+ # @rbs () -> void
1995
+ def finalize_failed_green_cst
1996
+ builder = @green_builder || raise(ParseError, "(cst):1:1: Green builder is unavailable")
1997
+ kinds = @green_kinds || raise(ParseError, "(cst):1:1: kind metadata is unavailable")
1998
+ trailing = [] #: Array[CST::GreenNode | CST::GreenToken]
1999
+ builder.append_trailing_to_last_token(green_location_trivia(@lookahead_location, :cst_previous_trailing))
2000
+ leading = @green_pending_skipped + green_location_trivia(@lookahead_location, :leading_trivia)
2001
+ @green_pending_skipped.clear
2002
+ if @lookahead.equal?(NO_LOOKAHEAD)
2003
+ trailing << builder.make_token(EOF_TOKEN, "", leading: leading) unless leading.empty?
2004
+ elsif @lookahead == EOF_TOKEN
2005
+ trailing << builder.make_token(EOF_TOKEN, "", leading: leading)
2006
+ else
2007
+ kind = @lookahead.is_a?(Integer) && @lookahead >= 0 ? @lookahead : kinds.fetch(:lexical_error_token)
2008
+ trailing << builder.make_token(
2009
+ kind, green_token_text(@lookahead_value, @lookahead_location),
2010
+ leading: leading,
2011
+ flags: CST::Flags::CONTAINS_ERROR
2012
+ )
2013
+ end
2014
+ install_syntax_root(builder.finish_synthetic_root(trailing), kinds)
2015
+ end
2016
+
2017
+ # @rbs (ParseError error) -> void
2018
+ def finalize_lexical_green_cst(error)
2019
+ builder = @green_builder || raise(ParseError, "(cst):1:1: Green builder is unavailable")
2020
+ kinds = @green_kinds || raise(ParseError, "(cst):1:1: kind metadata is unavailable")
2021
+ unmatched_text = cst_location_value(error.location, :ibex_cst_unmatched_text)
2022
+ text = if unmatched_text.is_a?(String)
2023
+ unmatched_text
2024
+ elsif error.token_value.is_a?(String)
2025
+ error.token_value
2026
+ else
2027
+ error.token_value.to_s
2028
+ end
2029
+ token = builder.make_token(
2030
+ kinds.fetch(:lexical_error_token), text,
2031
+ leading: pending_green_trivia + green_location_trivia(error.location, :leading_trivia),
2032
+ flags: CST::Flags::CONTAINS_ERROR
2033
+ )
2034
+ file = cst_location_value(error.location, :file)
2035
+ install_syntax_root(
2036
+ builder.finish_synthetic_root([token]), kinds,
2037
+ file: file.is_a?(String) ? file : nil
2038
+ )
2039
+ end
2040
+
2041
+ # @rbs () -> Array[CST::GreenTrivia]
2042
+ def pending_green_trivia
2043
+ return [] unless respond_to?(:take_cst_pending_green_trivia, true)
2044
+
2045
+ __send__(:take_cst_pending_green_trivia)
2046
+ end
2047
+
2048
+ # @rbs (CST::GreenNode green, CST::Kind kinds, ?file: String?) -> void
2049
+ def install_syntax_root(green, kinds, file: nil)
2050
+ config = parser_tables.fetch(:cst)
2051
+ policy = config.fetch(:trivia_policy)
2052
+ location_file = cst_location_value(@lookahead_location, :file)
2053
+ source_file = file || (location_file if location_file.is_a?(String))
2054
+ source = CST::SourceText.new(green.to_source.freeze, file: source_file)
2055
+ @syntax_root = CST::SyntaxNode.new(
2056
+ green: green, kinds: kinds, trivia_policy: policy, source_text: source
2057
+ )
2058
+ emit_incremental_event(
2059
+ :cst_built,
2060
+ {
2061
+ "descendant_count" => green.descendant_count,
2062
+ "full_width" => green.full_width,
2063
+ "contains_error" => green.flags.anybits?(CST::Flags::CONTAINS_ERROR)
2064
+ }
2065
+ )
2066
+ end
2067
+
2068
+ # @rbs (untyped value) -> CST::ParseResult
2069
+ def syntax_parse_result(value)
2070
+ root = @syntax_root
2071
+ raise ParseError, "(cst):1:1: parse_with_syntax requires a format-v6 CST parser" unless root
2072
+
2073
+ CST::ParseResult.new(value: value, syntax_root: root, diagnostics: @syntax_diagnostics)
2074
+ end
2075
+
2076
+ # @rbs () -> Array[Symbol]
2077
+ def syntax_token_states
2078
+ @green_token_states.dup.freeze
2079
+ end
2080
+
2081
+ # @rbs (CST::GreenNode green) -> void
2082
+ def install_parse_memo(green)
2083
+ return unless @green_parse_memo_valid && @green_memo_stack.one?
2084
+
2085
+ states = [@green_initial_state] #: Array[Integer?]
2086
+ @green_memo_stack.fetch(0).append_to(states)
2087
+ states << @state_stack.last
2088
+ return unless states.length == green.descendant_count
2089
+
2090
+ @syntax_parse_memo = CST::ParseMemo.new(
2091
+ left_states: states,
2092
+ grammar_digest: parser_tables.fetch(:grammar_digest),
2093
+ state_count: parser_tables.fetch(:state_count),
2094
+ production_count: parser_tables.fetch(:production_count)
2095
+ )
2096
+ end
2097
+
2098
+ # @rbs (untyped location) -> Symbol
2099
+ def green_lexer_state(location)
2100
+ state = if location.is_a?(Hash) && location.key?(:ibex_lexer_start_state)
2101
+ location[:ibex_lexer_start_state]
2102
+ else
2103
+ cst_location_value(location, :ibex_lexer_start_state)
2104
+ end
2105
+ state ? state.to_sym : :INITIAL
2106
+ end
2107
+
2108
+ # @rbs (CST::NodeCache cache) { () -> untyped } -> untyped
2109
+ def with_syntax_only(cache)
2110
+ previous_mode = @syntax_only
2111
+ previous_cache = @green_cache_override
2112
+ @syntax_only = true
2113
+ @green_cache_override = cache
2114
+ @runtime_fast_path = false
2115
+ yield
2116
+ ensure
2117
+ @syntax_only = previous_mode
2118
+ @green_cache_override = previous_cache
2119
+ end
2120
+
2121
+ # @rbs (untyped source, CST::NodeCache cache) -> CST::SyntaxResult
2122
+ def parse_syntax_token_source(source, cache)
2123
+ with_syntax_only(cache) do
2124
+ value = drive_parser(source)
2125
+ parsed = syntax_parse_result(value)
2126
+ CST::SyntaxResult.new(
2127
+ syntax_root: parsed.syntax_root,
2128
+ diagnostics: parsed.diagnostics,
2129
+ reused_ratio: 0.0
2130
+ )
2131
+ end
2132
+ end
2133
+
2134
+ # @rbs (CST::ReusableSubtree subtree) -> void
2135
+ def push_reusable_green_subtree(subtree)
2136
+ unless @syntax_only && @state_stack.last == subtree.left_state
2137
+ raise ParseError, "(incremental):1:1: reusable subtree state mismatch"
2138
+ end
2139
+
2140
+ builder = @green_builder || raise(ParseError, "(incremental):1:1: Green builder is unavailable")
2141
+ builder.append_trailing_to_last_token(subtree.previous_trailing) unless @green_reused_right_edge
2142
+ next_state = table_lookup(parser_tables.fetch(:gotos), @state_stack.last, subtree.lhs)
2143
+ raise ParseError, "(incremental):1:1: reusable subtree has no goto" unless next_state
2144
+
2145
+ ensure_stack_capacity!
2146
+ @state_stack << next_state
2147
+ push_location(nil)
2148
+ @value_stack << nil
2149
+ builder.subtree(subtree.green)
2150
+ @green_memo_stack << CST::ParseMemo::Entry.new(nil, segment: subtree.left_states)
2151
+ @green_reused_right_edge = true
2152
+ @incremental_reused_descendants += subtree.green.descendant_count
2153
+ end
2154
+
2155
+ # @rbs (Symbol type, Hash[untyped, untyped] data) -> void
2156
+ def emit_incremental_event(type, data)
2157
+ emit_runtime_event(type, data) if @runtime_observers
2158
+ end
2159
+
2160
+ # @rbs () -> void
2161
+ def discard_green_lookahead
2162
+ @green_parse_memo_valid = false
2163
+ builder = @green_builder
2164
+ kinds = @green_kinds
2165
+ return unless builder && kinds
2166
+
2167
+ kind = @lookahead.is_a?(Integer) && @lookahead >= 0 ? @lookahead : kinds.fetch(:lexical_error_token)
2168
+ token = builder.make_token(
2169
+ kind, green_token_text(@lookahead_value, @lookahead_location),
2170
+ leading: green_location_trivia(@lookahead_location, :leading_trivia),
2171
+ flags: CST::Flags::CONTAINS_ERROR
2172
+ )
2173
+ return if builder.append_to_last_error(token)
2174
+
2175
+ @green_pending_skipped << CST::GreenTrivia.new(
2176
+ kind: kinds.fetch(:skipped_tokens), text: token.to_source
2177
+ )
2178
+ end
2179
+
2180
+ # @rbs (?report: bool) -> untyped
2181
+ def recover(report: true)
2182
+ @runtime_fast_path = false
2183
+ materialize_lookahead_token_display! unless @lookahead.equal?(NO_LOOKAHEAD)
2184
+ @semantic_error = false
2185
+ return continue_recovery if @recovery_shifts.positive?
2186
+
2187
+ begin_recovery(report)
2188
+ end
2189
+
2190
+ # @rbs () -> untyped
2191
+ def continue_recovery
2192
+ return reject_recovery_eof if @lookahead == EOF_TOKEN
2193
+
2194
+ token_display = token_to_str(@lookahead)
2195
+ event_observers = runtime_observer_snapshot if @runtime_observers
2196
+ event_data = runtime_discard_data(token_display) if event_observers
2197
+ trace("discard #{token_display} during recovery") if @yydebug
2198
+ on_discard(@lookahead, @lookahead_value, @lookahead_location, :recovery)
2199
+ discard_green_lookahead
2200
+ @lookahead = NO_LOOKAHEAD
2201
+ @lookahead_location = nil
2202
+ @runtime_lookahead_token_display = nil
2203
+ emit_runtime_event(:discard, event_data, observers: event_observers) if event_data
2204
+ CONTINUE_OUTCOME
2205
+ end
2206
+
2207
+ # @rbs () -> [:done, nil]
2208
+ def reject_recovery_eof
2209
+ if @runtime_observers
2210
+ observers = runtime_observer_snapshot
2211
+ data = runtime_current_token_data
2212
+ emit_reject_event("eof_during_recovery", data, observers: observers)
2213
+ end
2214
+ [:done, cst_enabled? ? failed_cst : nil]
2215
+ end
2216
+
2217
+ # @rbs (String token_display) -> Hash[String, untyped]
2218
+ def runtime_discard_data(token_display)
2219
+ runtime_token_data(
2220
+ token_id: @lookahead,
2221
+ token_display: token_display,
2222
+ value: @lookahead_value,
2223
+ location: @lookahead_location,
2224
+ state: @state_stack.last
2225
+ ).merge("reason" => "recovery").freeze
2226
+ end
2227
+
2228
+ # @rbs (bool report) -> untyped
2229
+ def begin_recovery(report)
2230
+ consume_recovery_attempt!
2231
+ repair = report ? selected_repair : nil
2232
+ return REPAIR_PENDING_OUTCOME if repair.equal?(RepairSearch::NEED_INPUT)
2233
+
2234
+ context = recovery_context(report)
2235
+ capture_cst_error(context[:token_id], context[:value], context[:location], context[:reason].to_sym)
2236
+ token_data, recovery_observers = publish_error_context(context)
2237
+ notify_error_handler(context, repair) if report
2238
+ return commit_repair(repair) if repair.is_a?(RepairPlan)
2239
+
2240
+ fallback_recovery(context, token_data, recovery_observers)
2241
+ end
2242
+
2243
+ # @rbs (bool report) -> Hash[Symbol, untyped]
2244
+ def recovery_context(report)
2245
+ {
2246
+ token_id: @lookahead,
2247
+ value: @lookahead_value,
2248
+ token_display: @runtime_lookahead_token_display,
2249
+ location: @lookahead_location,
2250
+ value_stack: @value_stack.dup,
2251
+ state: @state_stack.last,
2252
+ reason: report ? "syntax" : "semantic"
2253
+ }
2254
+ end
2255
+
2256
+ # @rbs (Hash[Symbol, untyped] context) -> [Hash[String, untyped]?, Array[Proc]?]
2257
+ def publish_error_context(context)
2258
+ error_observers = runtime_observer_snapshot if @runtime_observers
2259
+ if error_observers
2260
+ token_data = runtime_original_token_data(
2261
+ context[:token_id], context[:token_display], context[:value], context[:location], context[:state]
2262
+ )
2263
+ emit_runtime_event(
2264
+ :error, token_data.merge("reason" => context[:reason]), observers: error_observers
2265
+ )
2266
+ end
2267
+ [token_data, (runtime_observer_snapshot if @runtime_observers)]
2268
+ end
2269
+
2270
+ # @rbs (Hash[Symbol, untyped] context, untyped repair) -> void
2271
+ def notify_error_handler(context, repair)
2272
+ @repair_selected = repair.is_a?(RepairPlan)
2273
+ begin
2274
+ on_error(context[:token_id], context[:value], context.fetch(:value_stack).dup)
2275
+ ensure
2276
+ @repair_selected = false
2277
+ end
2278
+ end
2279
+
2280
+ # @rbs (RepairPlan repair) -> [:continue]
2281
+ def commit_repair(repair)
2282
+ on_repair(repair)
2283
+ apply_repair(repair)
2284
+ CONTINUE_OUTCOME
2285
+ end
2286
+
2287
+ # @rbs (Hash[Symbol, untyped] context, Hash[String, untyped]? token_data,
2288
+ # Array[Proc]? recovery_observers) -> untyped
2289
+ # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
2290
+ # Green restoration mirrors the existing state/value/location transactional fallback.
2291
+ def fallback_recovery(context, token_data, recovery_observers)
2292
+ state_stack = @state_stack.dup if sync_recovery_configured?
2293
+ value_stack = @value_stack.dup if state_stack
2294
+ location_stack = @location_stack&.dup if state_stack
2295
+ green_stack = @green_builder&.snapshot if state_stack
2296
+ unless shift_error_token
2297
+ if state_stack
2298
+ @state_stack = state_stack
2299
+ install_value_stack(value_stack || [])
2300
+ @location_stack = location_stack
2301
+ @green_builder&.restore(green_stack || [])
2302
+ return begin_sync_recovery(context, token_data, recovery_observers)
2303
+ end
2304
+
2305
+ return reject_without_recovery(
2306
+ context[:token_id], context[:token_display], context[:value], context[:location], context[:state],
2307
+ token_data, recovery_observers
2308
+ )
2309
+ end
2310
+
2311
+ @recovery_shifts = RECOVERY_SHIFTS
2312
+ finish_recovery(
2313
+ context[:token_id], context[:token_display], context[:value], context[:location], context[:state],
2314
+ context[:value_stack], token_data, context[:reason], recovery_observers
2315
+ )
2316
+ end
2317
+ # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
2318
+
2319
+ # @rbs (untyped token_id, untyped token_display, untyped value, untyped location,
2320
+ # Integer state) -> Hash[String, untyped]
2321
+ def runtime_original_token_data(token_id, token_display, value, location, state)
2322
+ runtime_token_data(
2323
+ token_id: token_id,
2324
+ token_display: token_display,
2325
+ value: value,
2326
+ location: location,
2327
+ state: state
2328
+ )
2329
+ end
2330
+
2331
+ # @rbs (untyped token_id, untyped token_display, untyped value, untyped location,
2332
+ # Integer original_state, Hash[String, untyped]? token_data,
2333
+ # Array[Proc]? observers) -> [:done, nil]
2334
+ def reject_without_recovery(token_id, token_display, value, location, original_state, token_data, observers)
2335
+ if observers
2336
+ reject_data = token_data || runtime_original_token_data(
2337
+ token_id, token_display, value, location, original_state
2338
+ )
2339
+ emit_reject_event(
2340
+ "no_recovery_state",
2341
+ reject_data.merge("state" => @state_stack.last),
2342
+ observers: observers
2343
+ )
2344
+ end
2345
+ [:done, cst_enabled? ? failed_cst : nil]
2346
+ end
2347
+
2348
+ # @rbs (untyped token_id, untyped token_display, untyped value, untyped location,
2349
+ # Integer original_state, Array[untyped] value_stack, Hash[String, untyped]? token_data,
2350
+ # String reason, Array[Proc]? observers) -> [:continue]
2351
+ def finish_recovery(
2352
+ token_id, token_display, value, location, original_state, value_stack, token_data, reason, observers
2353
+ )
2354
+ recover_data = if observers
2355
+ token_data || runtime_original_token_data(
2356
+ token_id, token_display, value, location, original_state
2357
+ )
2358
+ end
2359
+ on_error_recover(token_id, value, value_stack)
2360
+ on_error_recover_location(token_id, value, value_stack, location, @state_stack.last)
2361
+ if recover_data
2362
+ emit_runtime_event(
2363
+ :recover,
2364
+ recover_data.merge(
2365
+ "from_state" => original_state,
2366
+ "state" => @state_stack.last,
2367
+ "reason" => reason
2368
+ ),
2369
+ observers: observers
2370
+ )
2371
+ end
2372
+ CONTINUE_OUTCOME
2373
+ end
2374
+
2375
+ # @rbs (token_id: untyped, token_display: untyped, value: untyped,
2376
+ # location: untyped, state: Integer) -> Hash[String, untyped]
2377
+ def runtime_token_data(token_id:, token_display:, value:, location:, state:)
2378
+ if token_id.equal?(NO_LOOKAHEAD)
2379
+ return {
2380
+ "state" => state,
2381
+ "token_id" => nil,
2382
+ "token" => nil,
2383
+ "value" => nil,
2384
+ "location" => nil
2385
+ }.freeze
2386
+ end
2387
+
2388
+ {
2389
+ "state" => state,
2390
+ "token_id" => token_id,
2391
+ "token" => EventSanitizer.value(token_display),
2392
+ "value" => EventSanitizer.value(value),
2393
+ "location" => EventSanitizer.location(location)
2394
+ }.freeze
2395
+ end
2396
+
2397
+ # @rbs () -> Hash[String, untyped]
2398
+ def runtime_current_token_data
2399
+ runtime_token_data(
2400
+ token_id: @lookahead,
2401
+ token_display: @runtime_lookahead_token_display,
2402
+ value: @lookahead_value,
2403
+ location: @lookahead_location,
2404
+ state: @state_stack.last
2405
+ )
2406
+ end
2407
+
2408
+ # @rbs (untyped result_summary, String reason) -> void
2409
+ def emit_accept_event(result_summary, reason)
2410
+ emit_runtime_event(
2411
+ :accept,
2412
+ { "state" => @state_stack.last, "result" => result_summary, "reason" => reason }
2413
+ )
2414
+ end
2415
+
2416
+ # @rbs (String reason, Hash[String, untyped] token_data, ?observers: Array[Proc]?) -> void
2417
+ def emit_reject_event(reason, token_data, observers: runtime_observer_snapshot)
2418
+ emit_runtime_event(
2419
+ :reject,
2420
+ token_data.merge("reason" => reason),
2421
+ observers: observers
2422
+ )
2423
+ end
2424
+
2425
+ # @rbs () -> bool
2426
+ def shift_error_token
2427
+ popped = 0
2428
+ @green_parse_memo_valid = false
2429
+ loop do
2430
+ action = table_lookup(parser_tables.fetch(:actions), @state_stack.last, ERROR_TOKEN)
2431
+ if action&.first == :shift
2432
+ trace("recover: shift error -> state #{action.fetch(1)}") if @yydebug
2433
+ ensure_stack_capacity!
2434
+ @state_stack << action.fetch(1)
2435
+ push_location(@lookahead_location)
2436
+ @value_stack << nil
2437
+ @green_builder&.absorb_into_error(popped)
2438
+ empty = [] #: Array[CST::ParseMemo::Entry]
2439
+ children = popped.zero? ? empty : @green_memo_stack.pop(popped)
2440
+ @green_memo_stack << CST::ParseMemo::Entry.new(@state_stack.last, children: children)
2441
+ return true
2442
+ end
2443
+ return false if @state_stack.length == 1
2444
+
2445
+ trace("recover: pop state #{@state_stack.last}") if @yydebug
2446
+ @state_stack.pop
2447
+ @value_stack.pop
2448
+ @location_stack&.pop
2449
+ popped += 1
2450
+ end
2451
+ end
2452
+
2453
+ # @rbs () -> void
2454
+ def ensure_stack_capacity!
2455
+ observed = @state_stack.length + 1
2456
+ limit = @resource_limits.max_stack_depth
2457
+ return if observed <= limit
2458
+
2459
+ raise ResourceLimitError.new(
2460
+ resource: :stack_depth, limit: limit, observed: observed,
2461
+ state: @state_stack.last, location: @lookahead_location
2462
+ )
2463
+ end
2464
+
2465
+ # @rbs () -> void
2466
+ def consume_recovery_attempt!
2467
+ @recovery_attempts += 1
2468
+ limit = @resource_limits.max_recovery_attempts
2469
+ return if @recovery_attempts <= limit
2470
+
2471
+ raise ResourceLimitError.new(
2472
+ resource: :recovery_attempts, limit: limit, observed: @recovery_attempts,
2473
+ state: @state_stack.last, location: @lookahead_location
2474
+ )
2475
+ end
2476
+
2477
+ # @rbs () -> (RepairPlan | Object | nil)
2478
+ def selected_repair
2479
+ policy = @repair_policy
2480
+ return unless policy
2481
+
2482
+ tokens, complete = repair_search_tokens(policy)
2483
+ RepairSearch.new(parser_tables, policy, tokens, complete: complete).search(@state_stack)
2484
+ end
2485
+
2486
+ # @rbs (RepairPolicy policy) -> [Array[RepairInput], bool]
2487
+ def repair_search_tokens(policy)
2488
+ current = RepairInput.new(
2489
+ token_id: @lookahead,
2490
+ token_name: @runtime_lookahead_token_display || token_to_str(@lookahead),
2491
+ value: @lookahead_value,
2492
+ location: @lookahead_location
2493
+ )
2494
+ buffer = @repair_input_buffer || raise(ParseError, "(repair):1:1: repair input buffer is unavailable")
2495
+ while @driver_status == :pull && buffer.length + 1 < policy.max_lookahead &&
2496
+ !buffer.last&.eof?
2497
+ buffer << repair_input_from_external(read_external_token)
2498
+ end
2499
+ tokens = ([current] + buffer).take(policy.max_lookahead)
2500
+ complete = tokens.any?(&:eof?) || tokens.length >= policy.max_lookahead
2501
+ [tokens.freeze, complete]
2502
+ end
2503
+
2504
+ # @rbs (RepairPlan plan) -> void
2505
+ def apply_repair(plan)
2506
+ source = [current_repair_input] + (@repair_input_buffer || [])
2507
+ @repair_input_buffer = replay_repair_edits(source, plan.edits)
2508
+ clear_repair_lookahead
2509
+ @recovery_shifts = 0
2510
+ trace("repair cost #{plan.cost}: #{repair_trace(plan)}") if @yydebug
2511
+ end
2512
+
2513
+ # @rbs () -> RepairInput
2514
+ def current_repair_input
2515
+ RepairInput.new(
2516
+ token_id: @lookahead,
2517
+ token_name: @runtime_lookahead_token_display || token_to_str(@lookahead),
2518
+ value: @lookahead_value,
2519
+ location: @lookahead_location
2520
+ )
2521
+ end
2522
+
2523
+ # @rbs (Array[RepairInput] source, Array[RepairEdit] edits) -> Array[RepairInput]
2524
+ def replay_repair_edits(source, edits)
2525
+ by_position = edits.group_by(&:position)
2526
+ repaired = [] #: Array[RepairInput]
2527
+ source.each_with_index do |input, position|
2528
+ consumed = append_repair_edits(repaired, input, by_position.fetch(position, []))
2529
+ repaired << input unless consumed
2530
+ end
2531
+ by_position.fetch(source.length, []).each do |edit|
2532
+ repaired << synthetic_repair_input(edit, value: nil, location: @lookahead_location)
2533
+ end
2534
+ repaired
2535
+ end
2536
+
2537
+ # @rbs (Array[RepairInput] output, RepairInput input, Array[RepairEdit] edits) -> bool
2538
+ def append_repair_edits(output, input, edits)
2539
+ preserve_deleted_repair_input(input) if edits.any? { |edit| edit.kind == :delete }
2540
+ edits.each do |edit|
2541
+ if edit.kind == :insert
2542
+ output << synthetic_repair_input(edit, value: nil, location: input.location)
2543
+ elsif edit.kind == :replace
2544
+ output << synthetic_repair_input(edit, value: input.value, location: input.location)
2545
+ end
2546
+ end
2547
+ edits.any? { |edit| %i[delete replace].include?(edit.kind) }
2548
+ end
2549
+
2550
+ # Retain a repair deletion in the concrete tree even though it is absent
2551
+ # from the repaired token stream.
2552
+ # @rbs (RepairInput input) -> void
2553
+ def preserve_deleted_repair_input(input)
2554
+ return unless cst_enabled?
2555
+
2556
+ builder = @green_builder
2557
+ kinds = @green_kinds
2558
+ return unless builder && kinds
2559
+
2560
+ previous = green_location_trivia(input.location, :cst_previous_trailing)
2561
+ empty = [] #: Array[CST::GreenTrivia]
2562
+ previous = empty if builder.append_trailing_to_last_token(previous)
2563
+ leading = previous + green_location_trivia(input.location, :leading_trivia)
2564
+ source = leading.each_with_object(String.new(encoding: Encoding::BINARY)) do |trivia, text|
2565
+ text << trivia.text
2566
+ end
2567
+ source << green_token_text(input.value, input.location)
2568
+ @green_pending_skipped << CST::GreenTrivia.new(
2569
+ kind: kinds.fetch(:skipped_tokens), text: source
2570
+ )
2571
+ end
2572
+
2573
+ # @rbs () -> void
2574
+ def clear_repair_lookahead
2575
+ @lookahead = NO_LOOKAHEAD
2576
+ @lookahead_value = nil
2577
+ @lookahead_location = nil
2578
+ @runtime_lookahead_token_display = nil
2579
+ end
2580
+
2581
+ # @rbs (RepairEdit edit, value: untyped, location: untyped) -> RepairInput
2582
+ def synthetic_repair_input(edit, value:, location:)
2583
+ location = cst_repair_location(location, edit.kind) if cst_enabled?
2584
+ RepairInput.new(token_id: edit.token_id, token_name: edit.token_name, value: value, location: location)
2585
+ end
2586
+
2587
+ # @rbs (untyped location, Symbol kind) -> Hash[Symbol, untyped]
2588
+ def cst_repair_location(location, kind)
2589
+ return location.merge(ibex_repair: kind).freeze if location.is_a?(Hash)
2590
+
2591
+ {
2592
+ file: cst_location_value(location, :file),
2593
+ line: cst_location_value(location, :line),
2594
+ column: cst_location_value(location, :column),
2595
+ end_line: cst_location_value(location, :end_line),
2596
+ end_column: cst_location_value(location, :end_column),
2597
+ origin: location,
2598
+ ibex_repair: kind
2599
+ }.freeze
2600
+ end
2601
+
2602
+ # @rbs (RepairPlan plan) -> String
2603
+ def repair_trace(plan)
2604
+ plan.edits.map { |edit| "#{edit.kind}(#{edit.token_name}@#{edit.position})" }.join(", ")
2605
+ end
2606
+
2607
+ # @rbs () -> void
2608
+ def read_lookahead
2609
+ return read_repair_lookahead if @repair_policy
2610
+
2611
+ read_compatible_lookahead
2612
+ end
2613
+
2614
+ # @rbs () -> void
2615
+ def read_repair_lookahead
2616
+ buffer = @repair_input_buffer
2617
+ input = if buffer&.any?
2618
+ buffer.shift
2619
+ else
2620
+ repair_input_from_external(read_external_token)
2621
+ end
2622
+ assign_repair_input(input)
2623
+ end
2624
+
2625
+ # @rbs () -> void
2626
+ def read_compatible_lookahead
2627
+ token = read_external_token
2628
+ refresh_runtime_fast_path_after_user_code!
2629
+ if token.is_a?(CST::ReusableSubtree)
2630
+ push_reusable_green_subtree(token)
2631
+ return
2632
+ end
2633
+
2634
+ if token.nil? || token == false
2635
+ @lookahead = EOF_TOKEN
2636
+ @lookahead_value = nil
2637
+ @lookahead_location = nil
2638
+ else
2639
+ external_token, @lookahead_value, @lookahead_location = token
2640
+ @lookahead = if external_token.nil? || external_token == false
2641
+ EOF_TOKEN
2642
+ else
2643
+ internal_token_id(external_token)
2644
+ end
2645
+ end
2646
+ @runtime_fast_path = false unless nil.equal?(@lookahead_location)
2647
+ materialize_compatible_lookahead
2648
+ end
2649
+
2650
+ # @rbs () -> void
2651
+ def materialize_compatible_lookahead
2652
+ if @runtime_fast_path
2653
+ @runtime_lookahead_token_display = nil
2654
+ else
2655
+ token_display = materialize_lookahead_token_display!
2656
+ trace("read #{token_display}") if @yydebug
2657
+ end
2658
+ end
2659
+
2660
+ # @rbs () -> String
2661
+ def materialize_lookahead_token_display!
2662
+ token_display = @runtime_lookahead_token_display
2663
+ return token_display if token_display
2664
+
2665
+ @runtime_lookahead_token_display = token_to_str(@lookahead)
2666
+ end
2667
+
2668
+ # @rbs (untyped external_token, untyped value, untyped location) -> RepairInput
2669
+ def repair_input(external_token, value, location)
2670
+ token_id = parser_tables.fetch(:tokens)[external_token]
2671
+ if token_id
2672
+ return RepairInput.new(
2673
+ token_id: token_id, token_name: token_to_str(token_id), value: value, location: location
2674
+ )
2675
+ end
2676
+
2677
+ RepairInput.new(
2678
+ token_id: -external_token.object_id.abs,
2679
+ token_name: external_token.inspect,
2680
+ value: value,
2681
+ location: location
2682
+ )
2683
+ end
2684
+
2685
+ # @rbs (untyped token) -> RepairInput
2686
+ def repair_input_from_external(token)
2687
+ if token.nil? || token == false
2688
+ return RepairInput.new(token_id: EOF_TOKEN, token_name: token_to_str(EOF_TOKEN), value: nil, location: nil)
2689
+ end
2690
+
2691
+ external_token, value, location = token
2692
+ if external_token.nil? || external_token == false
2693
+ return RepairInput.new(
2694
+ token_id: EOF_TOKEN,
2695
+ token_name: token_to_str(EOF_TOKEN),
2696
+ value: nil,
2697
+ location: location
2698
+ )
2699
+ end
2700
+ repair_input(external_token, value, location)
2701
+ end
2702
+
2703
+ # @rbs (RepairInput input) -> void
2704
+ def enqueue_or_assign_repair_input(input)
2705
+ buffer = @repair_input_buffer || raise(ParseError, "(repair):1:1: repair input buffer is unavailable")
2706
+ if @lookahead.equal?(NO_LOOKAHEAD) && buffer.empty?
2707
+ assign_repair_input(input)
2708
+ else
2709
+ buffer << input
2710
+ end
2711
+ end
2712
+
2713
+ # @rbs (RepairInput input) -> void
2714
+ def assign_repair_input(input)
2715
+ @lookahead = input.token_id
2716
+ @lookahead_value = input.value
2717
+ @lookahead_location = input.location
2718
+ @runtime_lookahead_token_display = input.token_name
2719
+ if input.token_id.negative?
2720
+ @unknown_token_id = input.token_id
2721
+ @unknown_token_name = input.token_name
2722
+ end
2723
+ trace("read #{input.token_name}") if @yydebug
2724
+ end
2725
+
2726
+ # @rbs () -> untyped
2727
+ def read_external_token
2728
+ source = @source
2729
+ return next_token unless source
2730
+ return source.next_for_state(@state_stack.last) if source.respond_to?(:next_for_state)
2731
+
2732
+ source.call
2733
+ rescue StopIteration
2734
+ false
2735
+ end
2736
+
2737
+ # @rbs (untyped external_token) -> Integer
2738
+ def internal_token_id(external_token)
2739
+ token_id = parser_tables.fetch(:tokens)[external_token]
2740
+ return token_id if token_id
2741
+
2742
+ @unknown_token_name = external_token.inspect
2743
+ @unknown_token_id = -external_token.object_id.abs
2744
+ end
2745
+
2746
+ # @rbs () -> Hash[Symbol, untyped]
2747
+ def parser_tables
2748
+ tables = @runtime_parser_tables
2749
+ return tables if tables
2750
+
2751
+ load_parser_tables
2752
+ end
2753
+
2754
+ # @rbs () -> Hash[Symbol, untyped]
2755
+ def load_parser_tables
2756
+ self.class.__send__(:parser_tables)
2757
+ rescue NoMethodError
2758
+ raise ParseError, "(tables):1:1: #{self.class} must define .parser_tables"
2759
+ end
2760
+
2761
+ # @rbs (untyped action) -> bool
2762
+ def error_action?(action)
2763
+ action.nil? || action.first == :error
2764
+ end
2765
+
2766
+ # @rbs (untyped configured) -> [String?, String?]
2767
+ def configured_error_message(configured)
2768
+ return [nil, configured] unless configured.is_a?(Hash)
2769
+
2770
+ error_id = configured[:id] || configured["id"]
2771
+ message = configured[:message] || configured["message"]
2772
+ [error_id, message]
2773
+ end
2774
+
2775
+ # @rbs (String actual, Array[String] expected) -> Array[String]
2776
+ def token_suggestions(actual, expected)
2777
+ word = normalized_token_word(actual)
2778
+ return [] unless word
2779
+
2780
+ threshold = word.length < 5 ? 1 : 2
2781
+ expected.filter_map do |candidate|
2782
+ candidate_word = normalized_token_word(candidate)
2783
+ candidate if candidate_word && edit_distance(word, candidate_word) <= threshold
2784
+ end
2785
+ end
2786
+
2787
+ # @rbs (String token_name) -> String?
2788
+ def normalized_token_word(token_name)
2789
+ word = token_name.delete_prefix(":")
2790
+ word.upcase if word.match?(/\A[A-Za-z_][A-Za-z0-9_]*\z/)
2791
+ end
2792
+
2793
+ # @rbs (String left, String right) -> Integer
2794
+ def edit_distance(left, right)
2795
+ previous = (0..right.length).to_a
2796
+ left.each_char.with_index(1) do |left_character, row|
2797
+ current = [row]
2798
+ right.each_char.with_index(1) do |right_character, column|
2799
+ current[column] = [
2800
+ current[column - 1] + 1,
2801
+ previous[column] + 1,
2802
+ previous[column - 1] + (left_character == right_character ? 0 : 1)
2803
+ ].min
2804
+ end
2805
+ previous = current
2806
+ end
2807
+ previous.last
2808
+ end
2809
+
2810
+ # @rbs (Integer state) -> untyped
2811
+ def default_action(state)
2812
+ parser_tables.fetch(:default_actions, EMPTY_ROW)[state]
2813
+ end
2814
+
2815
+ # @rbs (Hash[Symbol, untyped] tables) -> bool
2816
+ def track_locations?(tables)
2817
+ tables.fetch(:uses_locations, false) || !@runtime_observers.nil?
2818
+ end
2819
+
2820
+ # @rbs (Hash[Symbol, untyped] tables) -> void
2821
+ def initialize_runtime_fast_path(tables)
2822
+ @location_stack = track_locations?(tables) ? [] : nil
2823
+ @runtime_fast_path = runtime_fast_path_eligible?(tables)
2824
+ return unless @runtime_fast_path
2825
+ return if @runtime_fast_path_tracker_installed
2826
+
2827
+ install_runtime_fast_path_tracker!
2828
+ end
2829
+
2830
+ # @rbs (?Class? singleton) -> void
2831
+ def install_runtime_fast_path_tracker!(singleton = nil)
2832
+ singleton ||= Object.instance_method(:singleton_class).bind_call(self)
2833
+ Module.instance_method(:prepend).bind_call(singleton, FastPathMutationTracker)
2834
+ @runtime_fast_path_tracker_installed = true
2835
+ @runtime_fast_path_hooks_mutated = false
2836
+ @runtime_fast_path_singleton_ancestors = singleton.ancestors.freeze
2837
+ rescue FrozenError, TypeError
2838
+ @runtime_fast_path = false
2839
+ end
2840
+
2841
+ # @rbs () -> void
2842
+ def install_runtime_fast_path_class_tracker!
2843
+ parser_class = self.class
2844
+ return if parser_class.instance_variable_get(:@__ibex_fast_path_class_tracker_installed) == true
2845
+
2846
+ singleton = parser_class.singleton_class
2847
+ singleton.prepend(FastPathClassMutationTracker)
2848
+ parser_class.instance_variable_set(:@__ibex_fast_path_hook_version, 0)
2849
+ parser_class.instance_variable_set(:@__ibex_fast_path_class_tracker_installed, true)
2850
+ rescue FrozenError, TypeError
2851
+ nil
2852
+ end
2853
+
2854
+ # The generic driver remains authoritative whenever a public runtime
2855
+ # extension can observe a committed shift or reduction.
2856
+ # @rbs (Hash[Symbol, untyped] tables) -> bool
2857
+ def runtime_fast_path_eligible?(tables)
2858
+ !@syntax_only &&
2859
+ !@yydebug &&
2860
+ @runtime_observers.nil? &&
2861
+ @repair_policy.nil? &&
2862
+ !tables[:cst] &&
2863
+ !tables.fetch(:uses_locations, false) &&
2864
+ @location_stack.nil? &&
2865
+ runtime_fast_path_hooks_eligible?
2866
+ end
2867
+
2868
+ # @rbs () -> bool
2869
+ def runtime_fast_path_hooks_eligible?
2870
+ cached = cached_runtime_fast_path_hooks_eligibility
2871
+ return cached unless cached.nil?
2872
+
2873
+ lookup = Object.instance_method(:method)
2874
+ hooks_unchanged =
2875
+ runtime_method_unchanged?(lookup, :on_shift, :__ibex_fast_path_on_shift) &&
2876
+ runtime_method_unchanged?(lookup, :on_shift_location, :__ibex_fast_path_on_shift_location) &&
2877
+ runtime_method_unchanged?(lookup, :on_reduce, :__ibex_fast_path_on_reduce) &&
2878
+ runtime_method_unchanged?(lookup, :on_reduce_location, :__ibex_fast_path_on_reduce_location) &&
2879
+ runtime_method_unchanged?(lookup, :token_to_str, :__ibex_fast_path_token_to_str)
2880
+ return false unless hooks_unchanged
2881
+ return true unless @runtime_fast_path_tracker_installed
2882
+
2883
+ tracker_effective = FastPathMutationTracker.effective_for?(self, lookup)
2884
+ if tracker_effective
2885
+ @runtime_fast_path_hooks_mutated = false
2886
+ singleton = Object.instance_method(:singleton_class).bind_call(self)
2887
+ @runtime_fast_path_singleton_ancestors = singleton.ancestors.freeze
2888
+ end
2889
+ tracker_effective
2890
+ end
2891
+
2892
+ # @rbs () -> bool?
2893
+ def cached_runtime_fast_path_hooks_eligibility
2894
+ return nil unless @runtime_fast_path_tracker_installed && !@runtime_fast_path_hooks_mutated
2895
+
2896
+ singleton = Object.instance_method(:singleton_class).bind_call(self)
2897
+ return false if singleton.frozen?
2898
+ return nil unless @runtime_fast_path_singleton_ancestors == singleton.ancestors
2899
+
2900
+ runtime_fast_path_class_hooks_eligible?
2901
+ end
2902
+
2903
+ # @rbs () -> bool
2904
+ def runtime_fast_path_class_hooks_eligible?
2905
+ install_runtime_fast_path_class_tracker!
2906
+ parser_class = self.class
2907
+ version = parser_class.instance_variable_get(:@__ibex_fast_path_hook_version) || 0
2908
+ cached = parser_class.instance_variable_get(:@__ibex_fast_path_hook_cache)
2909
+ return cached.fetch(1) if cached.is_a?(Array) && cached[0] == version
2910
+
2911
+ eligible = FAST_PATH_HOOK_REFERENCES.all? do |name, reference|
2912
+ runtime_methods_equivalent?(
2913
+ parser_class.instance_method(name),
2914
+ Parser.instance_method(reference)
2915
+ )
2916
+ rescue NameError
2917
+ false
2918
+ end
2919
+ parser_class.instance_variable_set(:@__ibex_fast_path_hook_cache, [version, eligible].freeze)
2920
+ eligible
2921
+ rescue FrozenError, TypeError
2922
+ false
2923
+ end
2924
+
2925
+ # @rbs (UnboundMethod lookup, Symbol name, Symbol reference) -> bool
2926
+ def runtime_method_unchanged?(lookup, name, reference)
2927
+ implementation = runtime_core_method(lookup, name)
2928
+ expected = runtime_core_method(lookup, reference)
2929
+ return false if implementation.nil? || expected.nil?
2930
+
2931
+ runtime_methods_equivalent?(implementation, expected)
2932
+ end
2933
+
2934
+ # Ruby 3.0 and 3.1 do not preserve UnboundMethod#== for an inherited
2935
+ # method and its alias even though both still identify the same body.
2936
+ # @rbs (Method | UnboundMethod implementation, Method | UnboundMethod expected) -> bool
2937
+ def runtime_methods_equivalent?(implementation, expected)
2938
+ return true if implementation == expected
2939
+
2940
+ implementation.owner.equal?(expected.owner) &&
2941
+ implementation.original_name == expected.original_name &&
2942
+ implementation.source_location == expected.source_location
2943
+ end
2944
+
2945
+ # Bypass an application-defined `method` helper while retaining Ruby's
2946
+ # complete singleton/prepend/subclass lookup semantics.
2947
+ # @rbs (UnboundMethod lookup, Symbol name) -> Method?
2948
+ def runtime_core_method(lookup, name)
2949
+ lookup.bind_call(self, name)
2950
+ rescue NameError
2951
+ nil
2952
+ end
2953
+
2954
+ # Re-check after lexer and semantic-action callbacks because those are
2955
+ # supported points at which an application can install instrumentation.
2956
+ # A disabled fast path never becomes active again within the session.
2957
+ # @rbs () -> void
2958
+ def refresh_runtime_fast_path_after_user_code!
2959
+ return unless @runtime_fast_path
2960
+
2961
+ @runtime_fast_path = false unless
2962
+ !@yydebug &&
2963
+ @runtime_observers.nil? &&
2964
+ @repair_policy.nil? &&
2965
+ @location_stack.nil? &&
2966
+ @semantic_error == false &&
2967
+ @accept_requested == false
2968
+ end
2969
+
2970
+ # Keep allocation out of the ordinary two-element lexer path. If a
2971
+ # location first appears after values have already shifted, backfill the
2972
+ # parallel prefix with nil exactly once.
2973
+ # @rbs (untyped location) -> void
2974
+ def push_location(location)
2975
+ stack = @location_stack
2976
+ if stack
2977
+ stack << location
2978
+ elsif location
2979
+ new_stack = Array.new(@value_stack.length)
2980
+ new_stack << location
2981
+ @location_stack = new_stack
2982
+ end
2983
+ end
2984
+
2985
+ # @rbs (Integer length) -> Array[untyped]
2986
+ def pop_reduction_locations(length)
2987
+ stack = @location_stack
2988
+ return Array.new(length) unless stack
2989
+
2990
+ locations = stack.last(length)
2991
+ stack.pop(length)
2992
+ locations
2993
+ end
2994
+
2995
+ # @rbs (untyped table, Integer row, Integer column) -> untyped
2996
+ def table_lookup(table, row, column)
2997
+ return table.lookup(row, column) if table.respond_to?(:lookup)
2998
+
2999
+ table.fetch(row, EMPTY_ROW)[column]
3000
+ end
3001
+
3002
+ # @rbs (String message) -> void
3003
+ def trace(message)
3004
+ @yydebug_output.puts("ibex: #{message}") if @yydebug
3005
+ end
3006
+
3007
+ # @rbs (untyped value, Integer symbol_id) -> String
3008
+ def trace_value_suffix(value, symbol_id)
3009
+ return "" unless @yydebug
3010
+
3011
+ printer = @trace_value_printer
3012
+ rendered = if printer
3013
+ printer.call(value)
3014
+ else
3015
+ method_name = parser_tables.fetch(:value_printers, EMPTY_ROW)[symbol_id]
3016
+ return "" unless method_name
3017
+
3018
+ __send__(method_name, value)
3019
+ end
3020
+ " value=#{rendered}"
3021
+ rescue StandardError => e
3022
+ " value=<printer error: #{e.class}>"
3023
+ end
3024
+ end
3025
+ # rubocop:enable Metrics/ClassLength
3026
+ end
3027
+ end