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.
- checksums.yaml +4 -4
- data/README.md +100 -272
- data/lib/ibex/runtime/cst/green/node.rb +1 -1
- data/lib/ibex/runtime/cst/green/token.rb +1 -1
- data/lib/ibex/runtime/cst/green/trivia.rb +1 -1
- data/lib/ibex/runtime/cst/incremental/blender.rb +1 -1
- data/lib/ibex/runtime/cst/incremental/lexed_syntax.rb +2 -2
- data/lib/ibex/runtime/cst/incremental/parse_memo.rb +2 -2
- data/lib/ibex/runtime/cst/incremental/relexer.rb +3 -0
- data/lib/ibex/runtime/cst/incremental/session.rb +26 -2
- data/lib/ibex/runtime/cst/parse_result.rb +7 -7
- data/lib/ibex/runtime/cst/serialize.rb +6 -3
- data/lib/ibex/runtime/cst/syntax_node.rb +4 -4
- data/lib/ibex/runtime/cst/syntax_token.rb +5 -5
- data/lib/ibex/runtime/cst/typed_node.rb +1 -1
- data/lib/ibex/runtime/cst/validator.rb +40 -30
- data/lib/ibex/runtime/embedded_source.rb +98 -0
- data/lib/ibex/runtime/event.rb +7 -4
- data/lib/ibex/runtime/event_jsonl_tracer.rb +1 -1
- data/lib/ibex/runtime/event_sanitizer.rb +32 -26
- data/lib/ibex/runtime/generated_lexer.rb +23 -16
- data/lib/ibex/runtime/lexer_input.rb +1 -1
- data/lib/ibex/runtime/location_span.rb +15 -15
- data/lib/ibex/runtime/observation.rb +1 -1
- data/lib/ibex/runtime/parser.rb +309 -191
- data/lib/ibex/runtime/parser_sync_recovery.rb +19 -15
- data/lib/ibex/runtime/repair.rb +36 -5
- data/lib/ibex/runtime/repair_priority_queue.rb +10 -6
- data/lib/ibex/runtime/repair_search.rb +92 -29
- data/lib/ibex/runtime/syntax_repair.rb +490 -0
- data/lib/ibex/runtime/syntax_session.rb +416 -0
- data/lib/ibex/runtime/table_format.rb +1 -1
- data/lib/ibex/runtime/version.rb +1 -1
- data/lib/ibex/runtime.rb +2 -1
- data/lib/ibex/tables/compact.rb +16 -12
- data/lib/ibex/tables/compact_actions.rb +45 -33
- data/lib/ibex/tables/compact_productions.rb +18 -15
- data/sig/ibex/runtime/cst/green/node.rbs +2 -2
- data/sig/ibex/runtime/cst/green/token.rbs +2 -2
- data/sig/ibex/runtime/cst/green/trivia.rbs +2 -2
- data/sig/ibex/runtime/cst/incremental/blender.rbs +2 -2
- data/sig/ibex/runtime/cst/incremental/lexed_syntax.rbs +3 -3
- data/sig/ibex/runtime/cst/incremental/parse_memo.rbs +4 -4
- data/sig/ibex/runtime/cst/incremental/session.rbs +8 -2
- data/sig/ibex/runtime/cst/parse_result.rbs +9 -9
- data/sig/ibex/runtime/cst/serialize.rbs +8 -6
- data/sig/ibex/runtime/cst/syntax_node.rbs +6 -6
- data/sig/ibex/runtime/cst/syntax_token.rbs +10 -10
- data/sig/ibex/runtime/cst/typed_node.rbs +2 -2
- data/sig/ibex/runtime/cst/validator.rbs +46 -42
- data/sig/ibex/runtime/embedded_source.rbs +21 -0
- data/sig/ibex/runtime/event.rbs +7 -5
- data/sig/ibex/runtime/event_jsonl_tracer.rbs +1 -1
- data/sig/ibex/runtime/event_sanitizer.rbs +50 -44
- data/sig/ibex/runtime/generated_lexer.rbs +31 -24
- data/sig/ibex/runtime/lexer_input.rbs +2 -2
- data/sig/ibex/runtime/location_span.rbs +28 -28
- data/sig/ibex/runtime/observation.rbs +2 -2
- data/sig/ibex/runtime/parser.rbs +266 -252
- data/sig/ibex/runtime/parser_sync_recovery.rbs +15 -15
- data/sig/ibex/runtime/repair.rbs +28 -8
- data/sig/ibex/runtime/repair_priority_queue.rbs +9 -7
- data/sig/ibex/runtime/repair_search.rbs +38 -19
- data/sig/ibex/runtime/syntax_repair.rbs +177 -0
- data/sig/ibex/runtime/syntax_session.rbs +186 -0
- data/sig/ibex/tables/compact.rbs +12 -12
- data/sig/ibex/tables/compact_actions.rbs +10 -17
- data/sig/ibex/tables/compact_productions.rbs +19 -19
- metadata +12 -6
- data/lib/ibex/runtime/jsonl_tracer.rb +0 -75
- 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
|