ibex-runtime 0.2.0 → 0.4.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 (71) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +100 -272
  3. data/lib/ibex/runtime/cst/green/node.rb +1 -1
  4. data/lib/ibex/runtime/cst/green/token.rb +1 -1
  5. data/lib/ibex/runtime/cst/green/trivia.rb +1 -1
  6. data/lib/ibex/runtime/cst/incremental/blender.rb +1 -1
  7. data/lib/ibex/runtime/cst/incremental/lexed_syntax.rb +2 -2
  8. data/lib/ibex/runtime/cst/incremental/parse_memo.rb +2 -2
  9. data/lib/ibex/runtime/cst/incremental/relexer.rb +3 -0
  10. data/lib/ibex/runtime/cst/incremental/session.rb +26 -2
  11. data/lib/ibex/runtime/cst/parse_result.rb +7 -7
  12. data/lib/ibex/runtime/cst/serialize.rb +6 -3
  13. data/lib/ibex/runtime/cst/syntax_node.rb +4 -4
  14. data/lib/ibex/runtime/cst/syntax_token.rb +5 -5
  15. data/lib/ibex/runtime/cst/typed_node.rb +1 -1
  16. data/lib/ibex/runtime/cst/validator.rb +40 -30
  17. data/lib/ibex/runtime/embedded_source.rb +98 -0
  18. data/lib/ibex/runtime/event.rb +7 -4
  19. data/lib/ibex/runtime/event_jsonl_tracer.rb +1 -1
  20. data/lib/ibex/runtime/event_sanitizer.rb +32 -26
  21. data/lib/ibex/runtime/generated_lexer.rb +23 -16
  22. data/lib/ibex/runtime/lexer_input.rb +1 -1
  23. data/lib/ibex/runtime/location_span.rb +15 -15
  24. data/lib/ibex/runtime/observation.rb +1 -1
  25. data/lib/ibex/runtime/parser.rb +309 -191
  26. data/lib/ibex/runtime/parser_sync_recovery.rb +19 -15
  27. data/lib/ibex/runtime/repair.rb +36 -5
  28. data/lib/ibex/runtime/repair_priority_queue.rb +10 -6
  29. data/lib/ibex/runtime/repair_search.rb +92 -29
  30. data/lib/ibex/runtime/syntax_repair.rb +490 -0
  31. data/lib/ibex/runtime/syntax_session.rb +416 -0
  32. data/lib/ibex/runtime/table_format.rb +1 -1
  33. data/lib/ibex/runtime/version.rb +1 -1
  34. data/lib/ibex/runtime.rb +2 -1
  35. data/lib/ibex/tables/compact.rb +16 -12
  36. data/lib/ibex/tables/compact_actions.rb +45 -33
  37. data/lib/ibex/tables/compact_productions.rb +18 -15
  38. data/sig/ibex/runtime/cst/green/node.rbs +2 -2
  39. data/sig/ibex/runtime/cst/green/token.rbs +2 -2
  40. data/sig/ibex/runtime/cst/green/trivia.rbs +2 -2
  41. data/sig/ibex/runtime/cst/incremental/blender.rbs +2 -2
  42. data/sig/ibex/runtime/cst/incremental/lexed_syntax.rbs +3 -3
  43. data/sig/ibex/runtime/cst/incremental/parse_memo.rbs +4 -4
  44. data/sig/ibex/runtime/cst/incremental/session.rbs +8 -2
  45. data/sig/ibex/runtime/cst/parse_result.rbs +9 -9
  46. data/sig/ibex/runtime/cst/serialize.rbs +8 -6
  47. data/sig/ibex/runtime/cst/syntax_node.rbs +6 -6
  48. data/sig/ibex/runtime/cst/syntax_token.rbs +10 -10
  49. data/sig/ibex/runtime/cst/typed_node.rbs +2 -2
  50. data/sig/ibex/runtime/cst/validator.rbs +46 -42
  51. data/sig/ibex/runtime/embedded_source.rbs +21 -0
  52. data/sig/ibex/runtime/event.rbs +7 -5
  53. data/sig/ibex/runtime/event_jsonl_tracer.rbs +1 -1
  54. data/sig/ibex/runtime/event_sanitizer.rbs +50 -44
  55. data/sig/ibex/runtime/generated_lexer.rbs +31 -24
  56. data/sig/ibex/runtime/lexer_input.rbs +2 -2
  57. data/sig/ibex/runtime/location_span.rbs +28 -28
  58. data/sig/ibex/runtime/observation.rbs +2 -2
  59. data/sig/ibex/runtime/parser.rbs +266 -252
  60. data/sig/ibex/runtime/parser_sync_recovery.rbs +15 -15
  61. data/sig/ibex/runtime/repair.rbs +28 -8
  62. data/sig/ibex/runtime/repair_priority_queue.rbs +9 -7
  63. data/sig/ibex/runtime/repair_search.rbs +38 -19
  64. data/sig/ibex/runtime/syntax_repair.rbs +177 -0
  65. data/sig/ibex/runtime/syntax_session.rbs +186 -0
  66. data/sig/ibex/tables/compact.rbs +12 -12
  67. data/sig/ibex/tables/compact_actions.rbs +10 -17
  68. data/sig/ibex/tables/compact_productions.rbs +19 -19
  69. metadata +12 -6
  70. data/lib/ibex/runtime/jsonl_tracer.rb +0 -75
  71. data/sig/ibex/runtime/jsonl_tracer.rbs +0 -42
@@ -0,0 +1,490 @@
1
+ # frozen_string_literal: true
2
+ # rbs_inline: enabled
3
+
4
+ module Ibex
5
+ module Runtime
6
+ # @rbs!
7
+ # type syntax_edit_document = {
8
+ # kind: Symbol,
9
+ # position: Integer,
10
+ # token_id: Integer,
11
+ # token_name: String,
12
+ # cost: Integer,
13
+ # start_byte: Integer,
14
+ # end_byte: Integer,
15
+ # original_text: String,
16
+ # replacement_text: String?
17
+ # }
18
+
19
+ # One syntax-only projection of a runtime repair edit. It intentionally
20
+ # carries source bytes and token identity, never an application value.
21
+ class SyntaxRepairEdit
22
+ attr_reader :kind #: Symbol
23
+ attr_reader :position #: Integer
24
+ attr_reader :token_id #: Integer
25
+ attr_reader :token_name #: String
26
+ attr_reader :cost #: Integer
27
+ attr_reader :start_byte #: Integer
28
+ attr_reader :end_byte #: Integer
29
+ attr_reader :original_text #: String
30
+ attr_reader :replacement_text #: String?
31
+
32
+ # @rbs (kind: Symbol, position: Integer, token_id: Integer, token_name: String, cost: Integer,
33
+ # start_byte: Integer, end_byte: Integer, original_text: String, replacement_text: String?) -> void
34
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
35
+ def initialize(kind:, position:, token_id:, token_name:, cost:, start_byte:, end_byte:, original_text:,
36
+ replacement_text:)
37
+ raise ArgumentError, "unknown syntax repair edit #{kind.inspect}" unless RepairEdit::KINDS.include?(kind)
38
+ unless position.is_a?(Integer) && position >= 0
39
+ raise ArgumentError, "syntax repair edit position must be nonnegative"
40
+ end
41
+ unless token_id.is_a?(Integer) && token_id >= 0
42
+ raise ArgumentError, "syntax repair token id must be nonnegative"
43
+ end
44
+ unless token_name.is_a?(String) && !token_name.empty?
45
+ raise ArgumentError, "syntax repair token name must be a nonempty String"
46
+ end
47
+ raise ArgumentError, "syntax repair edit cost must be positive" unless cost.is_a?(Integer) && cost.positive?
48
+ unless start_byte.is_a?(Integer) && end_byte.is_a?(Integer) && start_byte >= 0 && end_byte >= start_byte
49
+ raise ArgumentError, "syntax repair byte range is invalid"
50
+ end
51
+ unless original_text.is_a?(String) && (replacement_text.nil? || replacement_text.is_a?(String))
52
+ raise ArgumentError, "syntax repair edit text must be String values"
53
+ end
54
+ if kind == :insert && (start_byte != end_byte || !original_text.empty?)
55
+ raise ArgumentError, "syntax insertion must have an empty source range"
56
+ end
57
+ if kind == :delete && replacement_text != ""
58
+ raise ArgumentError, "syntax deletion must have an empty replacement"
59
+ end
60
+ unless original_text.bytesize == end_byte - start_byte
61
+ raise ArgumentError, "syntax repair original text does not match its byte range"
62
+ end
63
+
64
+ @kind = kind
65
+ @position = position
66
+ @token_id = token_id
67
+ @token_name = token_name.dup.freeze
68
+ @cost = cost
69
+ @start_byte = start_byte
70
+ @end_byte = end_byte
71
+ @original_text = original_text.b.freeze
72
+ @replacement_text = replacement_text&.b&.freeze
73
+ freeze
74
+ end
75
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
76
+
77
+ # @rbs () -> syntax_edit_document
78
+ def to_h
79
+ value = {
80
+ kind: @kind, position: @position, token_id: @token_id, token_name: @token_name, cost: @cost,
81
+ start_byte: @start_byte, end_byte: @end_byte, original_text: @original_text,
82
+ replacement_text: @replacement_text
83
+ } # @type var value: syntax_edit_document
84
+ value.freeze
85
+ end
86
+ end
87
+
88
+ # One selected runtime plan plus its source-oriented edit projection.
89
+ class SyntaxRepairPlan
90
+ attr_reader :runtime_plan #: RepairPlan
91
+ attr_reader :edits #: Array[SyntaxRepairEdit]
92
+ attr_reader :cost #: Integer
93
+ attr_reader :configurations #: Integer
94
+
95
+ # @rbs (runtime_plan: RepairPlan, edits: Array[SyntaxRepairEdit]) -> void
96
+ # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
97
+ def initialize(runtime_plan:, edits:)
98
+ unless runtime_plan.is_a?(RepairPlan) && edits.is_a?(Array) && edits.all?(SyntaxRepairEdit)
99
+ raise ArgumentError, "syntax repair plan requires one runtime plan and syntax edits"
100
+ end
101
+ raise ArgumentError, "syntax repair edit count must match the runtime plan" unless
102
+ runtime_plan.edits.length == edits.length
103
+
104
+ runtime_plan.edits.each_with_index do |runtime_edit, index|
105
+ syntax_edit = edits.fetch(index)
106
+ next if runtime_edit.kind == syntax_edit.kind && runtime_edit.position == syntax_edit.position &&
107
+ runtime_edit.token_id == syntax_edit.token_id && runtime_edit.token_name == syntax_edit.token_name &&
108
+ runtime_edit.cost == syntax_edit.cost
109
+
110
+ raise ArgumentError, "syntax repair edit metadata must match its runtime plan"
111
+ end
112
+
113
+ @runtime_plan = runtime_plan
114
+ @edits = edits.dup.freeze
115
+ @cost = runtime_plan.cost
116
+ @configurations = runtime_plan.configurations
117
+ freeze
118
+ end
119
+ # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
120
+ end
121
+
122
+ # Immutable syntax-only repair attempt. There is deliberately no `value`.
123
+ class SyntaxRepairResult
124
+ STATUSES = %i[accepted progress rejected unavailable exhausted not_found].freeze #: Array[Symbol]
125
+ BOUNDED_STATUSES = %i[selected exhausted not_found].freeze #: Array[Symbol]
126
+
127
+ attr_reader :status #: Symbol
128
+ attr_reader :bounded_status #: Symbol
129
+ attr_reader :reason #: Symbol?
130
+ attr_reader :plan #: SyntaxRepairPlan?
131
+ attr_reader :text_edits #: Array[CST::TextEdit]
132
+ attr_reader :syntax_root #: CST::SyntaxNode
133
+ attr_reader :diagnostics #: Array[SyntaxSessionDiagnostic]
134
+ attr_reader :updated_source #: CST::SourceText?
135
+ attr_reader :validation #: SyntaxSessionResult?
136
+
137
+ # @rbs (status: Symbol, bounded_status: Symbol, reason: Symbol?, plan: SyntaxRepairPlan?,
138
+ # text_edits: Array[CST::TextEdit], syntax_root: CST::SyntaxNode,
139
+ # diagnostics: Array[SyntaxSessionDiagnostic], updated_source: CST::SourceText?,
140
+ # validation: SyntaxSessionResult?) -> void
141
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
142
+ def initialize(status:, bounded_status:, reason:, plan:, text_edits:, syntax_root:, diagnostics:,
143
+ updated_source:, validation:)
144
+ raise ArgumentError, "unknown syntax repair status #{status.inspect}" unless STATUSES.include?(status)
145
+ unless BOUNDED_STATUSES.include?(bounded_status)
146
+ raise ArgumentError, "unknown syntax repair bounded status #{bounded_status.inspect}"
147
+ end
148
+ unless text_edits.is_a?(Array) && text_edits.all?(CST::TextEdit)
149
+ raise ArgumentError, "syntax repair text_edits must contain only CST::TextEdit values"
150
+ end
151
+ unless diagnostics.is_a?(Array) && diagnostics.all?(SyntaxSessionDiagnostic)
152
+ raise ArgumentError, "syntax repair diagnostics must be immutable syntax-session diagnostics"
153
+ end
154
+ raise ArgumentError, "syntax_root must be a CST::SyntaxNode" unless syntax_root.is_a?(CST::SyntaxNode)
155
+ raise ArgumentError, "syntax repair reason must be a Symbol or nil" unless reason.nil? || reason.is_a?(Symbol)
156
+ unless plan.nil? || plan.is_a?(SyntaxRepairPlan)
157
+ raise ArgumentError, "syntax repair plan must be a SyntaxRepairPlan or nil"
158
+ end
159
+ unless updated_source.nil? || updated_source.is_a?(CST::SourceText)
160
+ raise ArgumentError, "updated_source must be a CST::SourceText or nil"
161
+ end
162
+ unless validation.nil? || validation.is_a?(SyntaxSessionResult)
163
+ raise ArgumentError, "validation must be a SyntaxSessionResult or nil"
164
+ end
165
+
166
+ validate_result_shape!(status, bounded_status, reason, plan, text_edits, updated_source, validation)
167
+
168
+ @status = status
169
+ @bounded_status = bounded_status
170
+ @reason = reason
171
+ @plan = plan
172
+ @text_edits = text_edits.dup.freeze
173
+ @syntax_root = syntax_root
174
+ @diagnostics = diagnostics.dup.freeze
175
+ @updated_source = updated_source
176
+ @validation = validation
177
+ freeze
178
+ end
179
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
180
+
181
+ # @rbs () -> bool
182
+ def accepted? = @status == :accepted
183
+
184
+ # @rbs () -> bool
185
+ def progress? = %i[accepted progress].include?(@status)
186
+
187
+ private
188
+
189
+ # @rbs (Symbol, Symbol, Symbol?, SyntaxRepairPlan?, Array[CST::TextEdit], CST::SourceText?,
190
+ # SyntaxSessionResult?) -> void
191
+ # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
192
+ def validate_result_shape!(status, bounded_status, reason, plan, text_edits, updated_source, validation)
193
+ if %i[accepted progress rejected].include?(status)
194
+ raise ArgumentError, "a completed syntax repair must be selected" unless bounded_status == :selected
195
+ raise ArgumentError, "a completed syntax repair requires a plan" unless plan
196
+ raise ArgumentError, "a completed syntax repair requires text edits" if text_edits.empty?
197
+ raise ArgumentError, "a completed syntax repair requires updated source and validation" unless
198
+ updated_source && validation
199
+ raise ArgumentError, "a completed syntax repair cannot have a reason" unless reason.nil?
200
+ raise ArgumentError, "accepted validation must succeed" if status == :accepted && !validation.success?
201
+ if %i[progress rejected].include?(status) && validation.success?
202
+ raise ArgumentError, "non-accepted validation must report an error"
203
+ end
204
+
205
+ return
206
+ end
207
+
208
+ if status == :unavailable
209
+ raise ArgumentError, "unavailable syntax repair must be selected" unless bounded_status == :selected
210
+ raise ArgumentError, "unavailable syntax repair requires a reason" unless reason
211
+ elsif status != bounded_status || !%i[exhausted not_found].include?(status)
212
+ raise ArgumentError, "bounded status does not match syntax repair status"
213
+ end
214
+ raise ArgumentError, "unavailable or bounded syntax repair cannot expose text edits" unless text_edits.empty?
215
+ return unless updated_source || validation
216
+
217
+ raise ArgumentError,
218
+ "unavailable or bounded syntax repair cannot expose validation"
219
+ end
220
+ # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
221
+ end
222
+
223
+ # Executes one fresh syntax-only repair attempt without mutating its source
224
+ # SyntaxSession. Generated lexer actions retain the acknowledged trust
225
+ # profile; parser production actions remain suppressed.
226
+ class SyntaxRepairer
227
+ # @rbs (Class parser_class, CST::SourceText source, SyntaxSessionResult baseline,
228
+ # execution_profile: Symbol, resource_limits: ResourceLimits, limits: SyntaxSessionLimits,
229
+ # cancellation: CancellationToken?, policy: RepairPolicy, token_text: Hash[String, String]) -> void
230
+ def initialize(parser_class, source, baseline, execution_profile:, resource_limits:, limits:, cancellation:,
231
+ policy:, token_text:)
232
+ @parser_class = parser_class
233
+ @source = source
234
+ @baseline = baseline
235
+ @execution_profile = execution_profile
236
+ @resource_limits = resource_limits
237
+ @limits = limits
238
+ @cancellation = cancellation
239
+ @policy = validate_policy(policy)
240
+ @token_text = validate_token_text(token_text)
241
+ end
242
+
243
+ # @rbs () -> SyntaxRepairResult
244
+ def call
245
+ cancellation_checkpoint!
246
+ parser = @parser_class.__send__(:new, resource_limits: @resource_limits)
247
+ captures = install_capture(parser)
248
+ parser.repair_policy = @policy
249
+ parser.observe { |_event| cancellation_checkpoint! }
250
+ parsed = parser.__send__(:parse_syntax_with_cache, @source, CST::NodeCache.new)
251
+ cancellation_checkpoint!
252
+ diagnostics = immutable_diagnostics(parsed.diagnostics)
253
+ return unavailable(:repair_source_not_consumed, parsed, diagnostics) unless
254
+ @source.text.start_with?(parsed.syntax_root.to_source)
255
+
256
+ outcomes = parser.__send__(:syntax_repair_search_results)
257
+ return bounded_failure(:exhausted, :search_exhausted, parsed, diagnostics) if
258
+ outcomes.any? { |outcome| outcome.status == :exhausted }
259
+ return bounded_failure(:not_found, :no_repair_plan, parsed, diagnostics) if captures.empty?
260
+ return unavailable(:multiple_repair_segments, parsed, diagnostics) unless captures.one?
261
+
262
+ build_selected(captures.fetch(0), parsed, diagnostics)
263
+ rescue ResourceLimitError => e
264
+ raise SyntaxSessionResourceLimitError.new(resource: e.resource, limit: e.limit, observed: e.observed), cause: e
265
+ end
266
+
267
+ private
268
+
269
+ # @rbs (Parser parser) -> Array[[RepairPlan, Array[RepairInput]]]
270
+ def install_capture(parser)
271
+ captures = [] #: Array[[RepairPlan, Array[RepairInput]]]
272
+ parser.define_singleton_method(:on_repair) do |plan|
273
+ inputs = __send__(:syntax_repair_inputs)
274
+ captures << [plan, inputs]
275
+ end
276
+ captures
277
+ end
278
+
279
+ # @rbs ([RepairPlan, Array[RepairInput]] capture, CST::SyntaxResult parsed,
280
+ # Array[SyntaxSessionDiagnostic] diagnostics) -> SyntaxRepairResult
281
+ def build_selected(capture, parsed, diagnostics)
282
+ runtime_plan, inputs = capture
283
+ projected = project_edits(runtime_plan, inputs)
284
+ plan = SyntaxRepairPlan.new(runtime_plan: runtime_plan, edits: projected)
285
+ return unavailable(:missing_token_text, parsed, diagnostics, plan: plan) if
286
+ projected.any? { |edit| %i[insert replace].include?(edit.kind) && edit.replacement_text.nil? }
287
+
288
+ text_edits = normalize_text_edits(projected)
289
+ validate_edit_limits!(text_edits)
290
+ updated_source = @source.apply(text_edits)
291
+ enforce_limit!(:source_bytes, @limits.max_source_bytes, updated_source.bytesize)
292
+ cancellation_checkpoint!
293
+ validation = fresh_validation(updated_source)
294
+ cancellation_checkpoint!
295
+ return unavailable(:validation_source_not_consumed, parsed, diagnostics, plan: plan) unless
296
+ updated_source.text.start_with?(validation.syntax_root.to_source)
297
+
298
+ status = validation_status(validation, updated_source)
299
+ SyntaxRepairResult.new(
300
+ status: status, bounded_status: :selected, reason: nil, plan: plan, text_edits: text_edits,
301
+ syntax_root: parsed.syntax_root, diagnostics: diagnostics, updated_source: updated_source,
302
+ validation: validation
303
+ )
304
+ rescue ArgumentError => e
305
+ return unavailable(:overlapping_text_edits, parsed, diagnostics, plan: plan) if
306
+ e.message.include?("text edits overlap")
307
+
308
+ raise
309
+ end
310
+
311
+ # @rbs (RepairPlan plan, Array[RepairInput] inputs) -> Array[SyntaxRepairEdit]
312
+ def project_edits(plan, inputs)
313
+ plan.edits.map do |edit|
314
+ input = inputs.fetch(edit.position)
315
+ start_byte, end_byte = input_range(input)
316
+ replacement = replacement_text(edit)
317
+ end_byte = start_byte if edit.kind == :insert
318
+ SyntaxRepairEdit.new(
319
+ kind: edit.kind, position: edit.position, token_id: edit.token_id,
320
+ token_name: edit.token_name, cost: edit.cost, start_byte: start_byte,
321
+ end_byte: end_byte, original_text: edit.kind == :insert ? "".b : original_text(start_byte, end_byte),
322
+ replacement_text: replacement
323
+ )
324
+ end
325
+ end
326
+
327
+ # @rbs (RepairInput input) -> [Integer, Integer]
328
+ def input_range(input)
329
+ location = input.location
330
+ start_byte = location_value(location, :start_byte)
331
+ end_byte = location_value(location, :end_byte)
332
+ unless start_byte.is_a?(Integer) && end_byte.is_a?(Integer)
333
+ raise ArgumentError, "syntax repair requires byte-oriented generated lexer locations"
334
+ end
335
+
336
+ [start_byte, end_byte]
337
+ end
338
+
339
+ # @rbs (RepairEdit edit) -> String?
340
+ def replacement_text(edit)
341
+ return "".b if edit.kind == :delete
342
+
343
+ explicit = @token_text[edit.token_name]
344
+ return explicit if explicit
345
+
346
+ punctuation_literal(edit.token_name)
347
+ end
348
+
349
+ # @rbs (String name) -> String?
350
+ def punctuation_literal(name)
351
+ inner = if name.length >= 2 && ["'", '"'].include?(name[0]) && name[-1] == name[0]
352
+ name[1...-1]
353
+ else
354
+ name
355
+ end
356
+ return unless inner && !inner.empty? && inner.match?(/\A[[:punct:]]+\z/)
357
+
358
+ inner.b.freeze
359
+ end
360
+
361
+ # @rbs (Integer start_byte, Integer end_byte) -> String
362
+ def original_text(start_byte, end_byte)
363
+ (@source.text.byteslice(start_byte, end_byte - start_byte) || "".b).freeze
364
+ end
365
+
366
+ # @rbs (Array[SyntaxRepairEdit] edits) -> Array[CST::TextEdit]
367
+ def normalize_text_edits(edits)
368
+ CST::TextEdit.normalize(edits.map do |edit|
369
+ insert_text = edit.replacement_text || raise(ArgumentError, "missing token text")
370
+ delete_length = edit.kind == :insert ? 0 : edit.end_byte - edit.start_byte
371
+ CST::TextEdit.new(start: edit.start_byte, delete_length: delete_length, insert_text: insert_text)
372
+ end)
373
+ end
374
+
375
+ # @rbs (Array[CST::TextEdit] edits) -> void
376
+ def validate_edit_limits!(edits)
377
+ enforce_limit!(:edits_per_operation, @limits.max_edits_per_operation, edits.length)
378
+ inserted = edits.sum { |edit| edit.insert_text.bytesize }
379
+ enforce_limit!(:inserted_bytes, @limits.max_inserted_bytes, inserted)
380
+ end
381
+
382
+ # @rbs (CST::SourceText source) -> SyntaxSessionResult
383
+ def fresh_validation(source)
384
+ SyntaxSession.new(
385
+ @parser_class, source, execution_profile: @execution_profile,
386
+ resource_limits: @resource_limits, limits: @limits,
387
+ cancellation: @cancellation, blender: false
388
+ ).result
389
+ end
390
+
391
+ # @rbs (SyntaxSessionResult validation, CST::SourceText source) -> Symbol
392
+ def validation_status(validation, source)
393
+ return :accepted if validation.success? && validation.syntax_root.to_source == source.text
394
+
395
+ baseline_count = @baseline.diagnostics.length
396
+ current_count = validation.diagnostics.length
397
+ return :progress if current_count < baseline_count
398
+
399
+ before = first_diagnostic_byte(@baseline)
400
+ after = first_diagnostic_byte(validation)
401
+ after.is_a?(Integer) && before.is_a?(Integer) && after > before ? :progress : :rejected
402
+ end
403
+
404
+ # @rbs (SyntaxSessionResult result) -> Integer?
405
+ def first_diagnostic_byte(result)
406
+ value = result.diagnostics.first&.data&.dig("location", "start_byte")
407
+ value if value.is_a?(Integer)
408
+ end
409
+
410
+ # @rbs (Symbol status, Symbol reason, CST::SyntaxResult parsed,
411
+ # Array[SyntaxSessionDiagnostic] diagnostics) -> SyntaxRepairResult
412
+ def bounded_failure(status, reason, parsed, diagnostics)
413
+ SyntaxRepairResult.new(
414
+ status: status, bounded_status: status, reason: reason, plan: nil, text_edits: [],
415
+ syntax_root: parsed.syntax_root, diagnostics: diagnostics, updated_source: nil, validation: nil
416
+ )
417
+ end
418
+
419
+ # @rbs (Symbol reason, CST::SyntaxResult parsed, Array[SyntaxSessionDiagnostic] diagnostics,
420
+ # ?plan: SyntaxRepairPlan?) -> SyntaxRepairResult
421
+ def unavailable(reason, parsed, diagnostics, plan: nil)
422
+ SyntaxRepairResult.new(
423
+ status: :unavailable, bounded_status: :selected, reason: reason, plan: plan, text_edits: [],
424
+ syntax_root: parsed.syntax_root, diagnostics: diagnostics, updated_source: nil, validation: nil
425
+ )
426
+ end
427
+
428
+ # @rbs (Array[Object] diagnostics) -> Array[SyntaxSessionDiagnostic]
429
+ def immutable_diagnostics(diagnostics)
430
+ diagnostics.map do |diagnostic|
431
+ if diagnostic.is_a?(ParseError)
432
+ SyntaxSessionDiagnostic.new(
433
+ kind: :parse_error,
434
+ data: {
435
+ message: diagnostic.message, token_id: diagnostic.token_id, token_name: diagnostic.token_name,
436
+ expected_tokens: diagnostic.expected_tokens, location: EventSanitizer.location(diagnostic.location)
437
+ }
438
+ )
439
+ else
440
+ data = diagnostic #: Hash[Object?, Object?]
441
+ SyntaxSessionDiagnostic.new(kind: :syntax_error, data: data)
442
+ end
443
+ end.freeze
444
+ end
445
+
446
+ # @rbs (Object location, Symbol key) -> Object?
447
+ # @rbs (untyped location, Symbol key) -> untyped
448
+ def location_value(location, key)
449
+ return location.public_send(key) if location.respond_to?(key)
450
+
451
+ if location.is_a?(Hash)
452
+ hash = location #: Hash[Object, Object]
453
+ return hash[key] || hash[key.to_s]
454
+ end
455
+
456
+ nil
457
+ end
458
+
459
+ # @rbs (RepairPolicy policy) -> RepairPolicy
460
+ def validate_policy(policy)
461
+ return policy if policy.is_a?(RepairPolicy)
462
+
463
+ raise ArgumentError, "policy must be an Ibex::Runtime::RepairPolicy"
464
+ end
465
+
466
+ # @rbs (Hash[String, String] value) -> Hash[String, String]
467
+ def validate_token_text(value)
468
+ unless value.is_a?(Hash) && value.all? { |name, text| name.is_a?(String) && text.is_a?(String) && !text.empty? }
469
+ raise ArgumentError, "token_text must map String token names to nonempty String source bytes"
470
+ end
471
+
472
+ value.to_h { |name, text| [name.dup.freeze, text.b.freeze] }.freeze
473
+ end
474
+
475
+ # @rbs (Symbol resource, Integer limit, Integer observed) -> void
476
+ def enforce_limit!(resource, limit, observed)
477
+ return unless observed > limit
478
+
479
+ raise SyntaxSessionResourceLimitError.new(resource: resource, limit: limit, observed: observed)
480
+ end
481
+
482
+ # @rbs () -> void
483
+ def cancellation_checkpoint!
484
+ return unless @cancellation&.cancelled?
485
+
486
+ raise SyntaxSessionCancelled, "syntax repair operation was cancelled"
487
+ end
488
+ end
489
+ end
490
+ end