parsanol 1.3.13-arm-linux
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 +7 -0
- data/HISTORY.txt +98 -0
- data/LICENSE +23 -0
- data/README.adoc +905 -0
- data/Rakefile +141 -0
- data/lib/parsanol/3.2/parsanol_native.so +0 -0
- data/lib/parsanol/3.3/parsanol_native.so +0 -0
- data/lib/parsanol/3.4/parsanol_native.so +0 -0
- data/lib/parsanol/4.0/parsanol_native.so +0 -0
- data/lib/parsanol/ast_visitor.rb +122 -0
- data/lib/parsanol/atoms/alternative.rb +123 -0
- data/lib/parsanol/atoms/base.rb +208 -0
- data/lib/parsanol/atoms/can_flatten.rb +194 -0
- data/lib/parsanol/atoms/capture.rb +38 -0
- data/lib/parsanol/atoms/context.rb +357 -0
- data/lib/parsanol/atoms/context_optimized.rb +38 -0
- data/lib/parsanol/atoms/custom.rb +110 -0
- data/lib/parsanol/atoms/cut.rb +66 -0
- data/lib/parsanol/atoms/dsl.rb +96 -0
- data/lib/parsanol/atoms/dynamic.rb +39 -0
- data/lib/parsanol/atoms/entity.rb +75 -0
- data/lib/parsanol/atoms/ignored.rb +37 -0
- data/lib/parsanol/atoms/infix.rb +167 -0
- data/lib/parsanol/atoms/lookahead.rb +85 -0
- data/lib/parsanol/atoms/named.rb +74 -0
- data/lib/parsanol/atoms/re.rb +83 -0
- data/lib/parsanol/atoms/repetition.rb +277 -0
- data/lib/parsanol/atoms/scope.rb +35 -0
- data/lib/parsanol/atoms/sequence.rb +195 -0
- data/lib/parsanol/atoms/str.rb +109 -0
- data/lib/parsanol/atoms/visitor.rb +91 -0
- data/lib/parsanol/atoms.rb +46 -0
- data/lib/parsanol/buffer.rb +133 -0
- data/lib/parsanol/builder_callbacks.rb +353 -0
- data/lib/parsanol/cause.rb +122 -0
- data/lib/parsanol/context.rb +39 -0
- data/lib/parsanol/convenience.rb +36 -0
- data/lib/parsanol/edit_tracker.rb +111 -0
- data/lib/parsanol/error_reporter/contextual.rb +99 -0
- data/lib/parsanol/error_reporter/deepest.rb +120 -0
- data/lib/parsanol/error_reporter/tree.rb +63 -0
- data/lib/parsanol/error_reporter.rb +100 -0
- data/lib/parsanol/expression/treetop.rb +154 -0
- data/lib/parsanol/expression.rb +106 -0
- data/lib/parsanol/fast_mode.rb +187 -0
- data/lib/parsanol/first_set.rb +79 -0
- data/lib/parsanol/grammar_builder.rb +179 -0
- data/lib/parsanol/incremental_parser.rb +182 -0
- data/lib/parsanol/interval_tree.rb +226 -0
- data/lib/parsanol/lazy_result.rb +179 -0
- data/lib/parsanol/mermaid.rb +142 -0
- data/lib/parsanol/native/batch_decoder.rb +255 -0
- data/lib/parsanol/native/dynamic.rb +238 -0
- data/lib/parsanol/native/parser.rb +102 -0
- data/lib/parsanol/native/serializer.rb +252 -0
- data/lib/parsanol/native/transformer.rb +604 -0
- data/lib/parsanol/native/types.rb +29 -0
- data/lib/parsanol/native.rb +223 -0
- data/lib/parsanol/optimizer.rb +85 -0
- data/lib/parsanol/optimizers/choice_optimizer.rb +78 -0
- data/lib/parsanol/optimizers/cut_inserter.rb +182 -0
- data/lib/parsanol/optimizers/lookahead_optimizer.rb +56 -0
- data/lib/parsanol/optimizers/quantifier_optimizer.rb +60 -0
- data/lib/parsanol/optimizers/sequence_optimizer.rb +97 -0
- data/lib/parsanol/options/zero_copy.rb +127 -0
- data/lib/parsanol/options.rb +21 -0
- data/lib/parsanol/parallel.rb +128 -0
- data/lib/parsanol/parser.rb +242 -0
- data/lib/parsanol/parslet.rb +151 -0
- data/lib/parsanol/pattern/binding.rb +91 -0
- data/lib/parsanol/pattern.rb +162 -0
- data/lib/parsanol/pool.rb +219 -0
- data/lib/parsanol/pools/array_pool.rb +75 -0
- data/lib/parsanol/pools/buffer_pool.rb +182 -0
- data/lib/parsanol/pools/position_pool.rb +92 -0
- data/lib/parsanol/pools/slice_pool.rb +64 -0
- data/lib/parsanol/position.rb +94 -0
- data/lib/parsanol/resettable.rb +29 -0
- data/lib/parsanol/result.rb +46 -0
- data/lib/parsanol/result_builder.rb +208 -0
- data/lib/parsanol/result_stream.rb +266 -0
- data/lib/parsanol/rig/rspec.rb +71 -0
- data/lib/parsanol/rope.rb +81 -0
- data/lib/parsanol/scope.rb +104 -0
- data/lib/parsanol/slice.rb +160 -0
- data/lib/parsanol/source/line_cache.rb +102 -0
- data/lib/parsanol/source.rb +185 -0
- data/lib/parsanol/source_location.rb +167 -0
- data/lib/parsanol/streaming_parser.rb +124 -0
- data/lib/parsanol/string_view.rb +198 -0
- data/lib/parsanol/transform.rb +226 -0
- data/lib/parsanol/version.rb +5 -0
- data/lib/parsanol/wasm/README.md +80 -0
- data/lib/parsanol/wasm/package.json +51 -0
- data/lib/parsanol/wasm/parsanol.js +252 -0
- data/lib/parsanol/wasm/parslet.d.ts +129 -0
- data/lib/parsanol/wasm_parser.rb +240 -0
- data/lib/parsanol.rb +278 -0
- data/parsanol.gemspec +67 -0
- metadata +279 -0
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Parsanol
|
|
4
|
+
# Module for streaming builder callbacks.
|
|
5
|
+
# Include this module in your builder class to receive callbacks
|
|
6
|
+
# during single-pass parsing with the streaming builder API.
|
|
7
|
+
#
|
|
8
|
+
# The streaming builder API allows maximum performance by eliminating
|
|
9
|
+
# intermediate AST construction. Instead, callbacks are invoked as
|
|
10
|
+
# parsing progresses, allowing you to construct custom output directly.
|
|
11
|
+
#
|
|
12
|
+
# @example Basic string collector
|
|
13
|
+
# class StringCollector
|
|
14
|
+
# include Parsanol::BuilderCallbacks
|
|
15
|
+
#
|
|
16
|
+
# def initialize
|
|
17
|
+
# @strings = []
|
|
18
|
+
# end
|
|
19
|
+
#
|
|
20
|
+
# def on_string(value, offset, length)
|
|
21
|
+
# @strings << value
|
|
22
|
+
# end
|
|
23
|
+
#
|
|
24
|
+
# def finish
|
|
25
|
+
# @strings
|
|
26
|
+
# end
|
|
27
|
+
# end
|
|
28
|
+
#
|
|
29
|
+
# grammar = Parsanol::Native.serialize_grammar(parser.root)
|
|
30
|
+
# builder = StringCollector.new
|
|
31
|
+
# result = Parsanol::Native.parse_with_builder(grammar, input, builder)
|
|
32
|
+
# # result: ["hello", "world"]
|
|
33
|
+
#
|
|
34
|
+
# @example Building a typed AST
|
|
35
|
+
# class AstBuilder
|
|
36
|
+
# include Parsanol::BuilderCallbacks
|
|
37
|
+
#
|
|
38
|
+
# def initialize
|
|
39
|
+
# @stack = []
|
|
40
|
+
# @current_hash = nil
|
|
41
|
+
# @current_key = nil
|
|
42
|
+
# end
|
|
43
|
+
#
|
|
44
|
+
# def on_hash_start(size = nil)
|
|
45
|
+
# @stack.push(@current_hash) if @current_hash
|
|
46
|
+
# @current_hash = {}
|
|
47
|
+
# end
|
|
48
|
+
#
|
|
49
|
+
# def on_hash_end(size)
|
|
50
|
+
# finished = @current_hash
|
|
51
|
+
# @current_hash = @stack.pop
|
|
52
|
+
# if @current_hash && @current_key
|
|
53
|
+
# @current_hash[@current_key] = finished
|
|
54
|
+
# @current_key = nil
|
|
55
|
+
# end
|
|
56
|
+
# finished
|
|
57
|
+
# end
|
|
58
|
+
#
|
|
59
|
+
# def on_hash_key(key)
|
|
60
|
+
# @current_key = key
|
|
61
|
+
# end
|
|
62
|
+
#
|
|
63
|
+
# def on_string(value, offset, length)
|
|
64
|
+
# if @current_hash && @current_key
|
|
65
|
+
# @current_hash[@current_key] = value
|
|
66
|
+
# @current_key = nil
|
|
67
|
+
# end
|
|
68
|
+
# end
|
|
69
|
+
#
|
|
70
|
+
# def finish
|
|
71
|
+
# @current_hash
|
|
72
|
+
# end
|
|
73
|
+
# end
|
|
74
|
+
#
|
|
75
|
+
module BuilderCallbacks
|
|
76
|
+
# Called when parsing starts.
|
|
77
|
+
#
|
|
78
|
+
# @param input [String] The input being parsed
|
|
79
|
+
# @return [void]
|
|
80
|
+
def on_start(input); end
|
|
81
|
+
|
|
82
|
+
# Called when parsing succeeds.
|
|
83
|
+
#
|
|
84
|
+
# @return [void]
|
|
85
|
+
def on_success; end
|
|
86
|
+
|
|
87
|
+
# Called when parsing fails.
|
|
88
|
+
#
|
|
89
|
+
# @param message [String] The error message
|
|
90
|
+
# @return [void]
|
|
91
|
+
def on_error(message); end
|
|
92
|
+
|
|
93
|
+
# Called when a string value is matched.
|
|
94
|
+
#
|
|
95
|
+
# @param value [String] The matched string value
|
|
96
|
+
# @param offset [Integer] Byte offset in the original input
|
|
97
|
+
# @param length [Integer] Length of the matched string in bytes
|
|
98
|
+
# @return [void]
|
|
99
|
+
def on_string(value, offset, length); end
|
|
100
|
+
|
|
101
|
+
# Called when an integer value is matched.
|
|
102
|
+
#
|
|
103
|
+
# @param value [Integer] The matched integer value
|
|
104
|
+
# @return [void]
|
|
105
|
+
def on_int(value); end
|
|
106
|
+
|
|
107
|
+
# Called when a float value is matched.
|
|
108
|
+
#
|
|
109
|
+
# @param value [Float] The matched float value
|
|
110
|
+
# @return [void]
|
|
111
|
+
def on_float(value); end
|
|
112
|
+
|
|
113
|
+
# Called when a boolean value is matched.
|
|
114
|
+
#
|
|
115
|
+
# @param value [Boolean] The matched boolean value
|
|
116
|
+
# @return [void]
|
|
117
|
+
def on_bool(value); end
|
|
118
|
+
|
|
119
|
+
# Called when a nil/null value is matched.
|
|
120
|
+
#
|
|
121
|
+
# @return [void]
|
|
122
|
+
def on_nil; end
|
|
123
|
+
|
|
124
|
+
# Called when starting to parse a hash/object.
|
|
125
|
+
# Use this to initialize state for collecting key-value pairs.
|
|
126
|
+
#
|
|
127
|
+
# @param size [Integer, nil] Expected number of entries (may be nil)
|
|
128
|
+
# @return [void]
|
|
129
|
+
def on_hash_start(size = nil); end
|
|
130
|
+
|
|
131
|
+
# Called when finishing parsing a hash/object.
|
|
132
|
+
#
|
|
133
|
+
# @param size [Integer] Actual number of entries
|
|
134
|
+
# @return [void]
|
|
135
|
+
def on_hash_end(size); end
|
|
136
|
+
|
|
137
|
+
# Called when a hash key is encountered.
|
|
138
|
+
# The next value callback(s) will provide the value for this key.
|
|
139
|
+
#
|
|
140
|
+
# @param key [String] The hash key name
|
|
141
|
+
# @return [void]
|
|
142
|
+
def on_hash_key(key); end
|
|
143
|
+
|
|
144
|
+
# Called when a hash value is about to be parsed.
|
|
145
|
+
# Called after on_hash_key for the corresponding value.
|
|
146
|
+
#
|
|
147
|
+
# @param key [String] The hash key name
|
|
148
|
+
# @return [void]
|
|
149
|
+
def on_hash_value(key); end
|
|
150
|
+
|
|
151
|
+
# Called when starting to parse an array.
|
|
152
|
+
# Use this to initialize state for collecting array elements.
|
|
153
|
+
#
|
|
154
|
+
# @param size [Integer, nil] Expected number of elements (may be nil)
|
|
155
|
+
# @return [void]
|
|
156
|
+
def on_array_start(size = nil); end
|
|
157
|
+
|
|
158
|
+
# Called when an array element is about to be parsed.
|
|
159
|
+
#
|
|
160
|
+
# @param index [Integer] The index of the element
|
|
161
|
+
# @return [void]
|
|
162
|
+
def on_array_element(index); end
|
|
163
|
+
|
|
164
|
+
# Called when finishing parsing an array.
|
|
165
|
+
#
|
|
166
|
+
# @param size [Integer] Actual number of elements
|
|
167
|
+
# @return [void]
|
|
168
|
+
def on_array_end(size); end
|
|
169
|
+
|
|
170
|
+
# Called when starting to parse a named rule.
|
|
171
|
+
#
|
|
172
|
+
# @param name [String] The rule name
|
|
173
|
+
# @return [void]
|
|
174
|
+
def on_named_start(name); end
|
|
175
|
+
|
|
176
|
+
# Called when finishing parsing a named rule.
|
|
177
|
+
#
|
|
178
|
+
# @param name [String] The rule name
|
|
179
|
+
# @return [void]
|
|
180
|
+
def on_named_end(name); end
|
|
181
|
+
|
|
182
|
+
# Called when parsing is complete.
|
|
183
|
+
# Override this method to return your final constructed result.
|
|
184
|
+
#
|
|
185
|
+
# @return [Object] The final result of the builder
|
|
186
|
+
def finish; end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Built-in builders for common use cases
|
|
190
|
+
module Builders
|
|
191
|
+
# Debug builder that collects all events as strings.
|
|
192
|
+
# Useful for understanding the parsing flow.
|
|
193
|
+
class DebugBuilder
|
|
194
|
+
include BuilderCallbacks
|
|
195
|
+
|
|
196
|
+
attr_reader :events
|
|
197
|
+
|
|
198
|
+
def initialize
|
|
199
|
+
@events = []
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def on_start(input)
|
|
203
|
+
@events << "start: #{input.inspect}"
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def on_success
|
|
207
|
+
@events << "success"
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def on_error(message)
|
|
211
|
+
@events << "error: #{message}"
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def on_string(value, offset, length)
|
|
215
|
+
@events << "string: #{value.inspect} @ #{offset}(#{length})"
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def on_int(value)
|
|
219
|
+
@events << "int: #{value}"
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def on_float(value)
|
|
223
|
+
@events << "float: #{value}"
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def on_bool(value)
|
|
227
|
+
@events << "bool: #{value}"
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def on_nil
|
|
231
|
+
@events << "nil"
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def on_hash_start(size = nil)
|
|
235
|
+
@events << "hash_start(#{size.inspect})"
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def on_hash_end(size)
|
|
239
|
+
@events << "hash_end(#{size})"
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def on_hash_key(key)
|
|
243
|
+
@events << "hash_key: #{key.inspect}"
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def on_hash_value(key)
|
|
247
|
+
@events << "hash_value: #{key.inspect}"
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def on_array_start(size = nil)
|
|
251
|
+
@events << "array_start(#{size.inspect})"
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def on_array_element(index)
|
|
255
|
+
@events << "array_element[#{index}]"
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def on_array_end(size)
|
|
259
|
+
@events << "array_end(#{size})"
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def on_named_start(name)
|
|
263
|
+
@events << "named_start: #{name}"
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def on_named_end(name)
|
|
267
|
+
@events << "named_end: #{name}"
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def finish
|
|
271
|
+
@events.join("\n")
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
# Builder that collects all string values.
|
|
276
|
+
class StringCollector
|
|
277
|
+
include BuilderCallbacks
|
|
278
|
+
|
|
279
|
+
attr_reader :strings
|
|
280
|
+
|
|
281
|
+
def initialize
|
|
282
|
+
@strings = []
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def on_start(input); end
|
|
286
|
+
|
|
287
|
+
def on_success; end
|
|
288
|
+
|
|
289
|
+
def on_error(message); end
|
|
290
|
+
|
|
291
|
+
def on_string(value, _offset, _length)
|
|
292
|
+
@strings << value
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def finish
|
|
296
|
+
@strings
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
# Builder that counts nodes by type.
|
|
301
|
+
class NodeCounter
|
|
302
|
+
include BuilderCallbacks
|
|
303
|
+
|
|
304
|
+
attr_reader :counts
|
|
305
|
+
|
|
306
|
+
def initialize
|
|
307
|
+
@counts = Hash.new(0)
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
def on_start(input); end
|
|
311
|
+
|
|
312
|
+
def on_success; end
|
|
313
|
+
|
|
314
|
+
def on_error(message); end
|
|
315
|
+
|
|
316
|
+
def on_string(_value, _offset, _length)
|
|
317
|
+
@counts[:string] += 1
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
def on_int(_value)
|
|
321
|
+
@counts[:int] += 1
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
def on_float(_value)
|
|
325
|
+
@counts[:float] += 1
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
def on_bool(_value)
|
|
329
|
+
@counts[:bool] += 1
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
def on_nil
|
|
333
|
+
@counts[:nil] += 1
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
def on_hash_start(_size = nil)
|
|
337
|
+
@counts[:hash] += 1
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
def on_array_start(_size = nil)
|
|
341
|
+
@counts[:array] += 1
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
def on_named_start(_name)
|
|
345
|
+
@counts[:named] += 1
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
def finish
|
|
349
|
+
@counts
|
|
350
|
+
end
|
|
351
|
+
end
|
|
352
|
+
end
|
|
353
|
+
end
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Represents a cause why a parse did fail. Stores information about
|
|
4
|
+
# parse failure including:
|
|
5
|
+
# - message: Human-readable error description
|
|
6
|
+
# - source: The source object being parsed
|
|
7
|
+
# - position: Byte position where error occurred
|
|
8
|
+
# - children: Nested causes (for deeper context)
|
|
9
|
+
#
|
|
10
|
+
# @example
|
|
11
|
+
# cause = Parsanol::Cause.new(
|
|
12
|
+
# "Expected at least one",
|
|
13
|
+
# source,
|
|
14
|
+
# 5
|
|
15
|
+
# )
|
|
16
|
+
# cause.children # => []
|
|
17
|
+
# cause.to_s # => "Expected at least one"
|
|
18
|
+
#
|
|
19
|
+
module Parsanol
|
|
20
|
+
class Cause
|
|
21
|
+
# @return [Array<String>] Error message parts
|
|
22
|
+
attr_reader :message
|
|
23
|
+
|
|
24
|
+
# @return [Parsanol::Source] Source being parsed
|
|
25
|
+
attr_reader :source
|
|
26
|
+
|
|
27
|
+
# @return [Integer] Byte position where error occurred
|
|
28
|
+
attr_reader :position
|
|
29
|
+
|
|
30
|
+
# Alias for position (API compatibility)
|
|
31
|
+
alias pos position
|
|
32
|
+
|
|
33
|
+
# @return [Array<Cause>] Child causes
|
|
34
|
+
attr_reader :children
|
|
35
|
+
|
|
36
|
+
# Creates a new cause for parse failure
|
|
37
|
+
#
|
|
38
|
+
# @param message [String, Array<String>] Error description
|
|
39
|
+
# @param source [Parsanol::Source] Source being parsed
|
|
40
|
+
# @param position [Integer] Byte position where error occurred
|
|
41
|
+
# @param children [Array<Cause>] Nested causes (optional)
|
|
42
|
+
def initialize(message, source, position, children = [])
|
|
43
|
+
@message = Array(message)
|
|
44
|
+
@source = source
|
|
45
|
+
@position = position
|
|
46
|
+
@children = children.nil? ? [] : children
|
|
47
|
+
@parsing_label = nil
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Factory method for creating a cause
|
|
51
|
+
#
|
|
52
|
+
# @param source [Parsanol::Source] source being parsed
|
|
53
|
+
# @param position [Integer] Byte position where error occurred
|
|
54
|
+
# @param message [String, Array<String>] Error description
|
|
55
|
+
# @param children [Array<Cause>] Nested causes
|
|
56
|
+
# @return [Cause] New cause instance
|
|
57
|
+
def self.format(source, position, message, children = [])
|
|
58
|
+
new(message, source, position, children)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Associates a label with this cause for parsing context
|
|
62
|
+
#
|
|
63
|
+
# @param label [String] Description of what was being parsed
|
|
64
|
+
# @return [void]
|
|
65
|
+
def set_label(label)
|
|
66
|
+
@parsing_label = " while parsing #{label}"
|
|
67
|
+
@children.each { |c| c.set_label(label) }
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Formats this cause as a human-readable string
|
|
71
|
+
#
|
|
72
|
+
# @return [String] Formatted error message
|
|
73
|
+
def to_s
|
|
74
|
+
line_num, col_num = @source.line_and_column(@position)
|
|
75
|
+
|
|
76
|
+
formatted_msg = @message.map do |msg|
|
|
77
|
+
msg.respond_to?(:to_slice) ? msg.content.inspect : msg.to_s
|
|
78
|
+
end.join
|
|
79
|
+
|
|
80
|
+
"#{formatted_msg} at line #{line_num} char #{col_num}#{@parsing_label}."
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Generates a tree-style visualization of error causes
|
|
84
|
+
#
|
|
85
|
+
# @return [String] ASCII tree representation
|
|
86
|
+
def ascii_tree
|
|
87
|
+
output = StringIO.new
|
|
88
|
+
build_tree_recursive(self, output, [true])
|
|
89
|
+
output.string
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Raises a ParseFailed exception with this cause's information
|
|
93
|
+
#
|
|
94
|
+
# @raise [Parsanol::ParseFailed] Always
|
|
95
|
+
def raise
|
|
96
|
+
exc = Parsanol::ParseFailed.new(to_s, self)
|
|
97
|
+
Kernel.raise exc
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
private
|
|
101
|
+
|
|
102
|
+
def build_tree_recursive(node, stream, prefix_flags)
|
|
103
|
+
render_prefix(stream, prefix_flags)
|
|
104
|
+
stream.puts node.to_s
|
|
105
|
+
|
|
106
|
+
node.children.each do |child|
|
|
107
|
+
is_last_child = (node.children.last == child)
|
|
108
|
+
build_tree_recursive(child, stream, prefix_flags + [is_last_child])
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def render_prefix(stream, prefix_flags)
|
|
113
|
+
return if prefix_flags.size < 2
|
|
114
|
+
|
|
115
|
+
prefix_flags[1..-2].each do |is_last|
|
|
116
|
+
stream.print is_last ? " " : "| "
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
stream.print prefix_flags.last ? "`- " : "|- "
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Execution context for tree transformation rules.
|
|
4
|
+
# Provides a clean interface for accessing bound variables within transformation blocks.
|
|
5
|
+
#
|
|
6
|
+
# @example
|
|
7
|
+
# ctx = Parsanol::Context.new(name: 'Alice', value: 42)
|
|
8
|
+
# ctx.instance_eval { name } # => 'Alice'
|
|
9
|
+
# ctx.instance_eval { @value } # => 42
|
|
10
|
+
#
|
|
11
|
+
# Inspired by Parslet (MIT License).
|
|
12
|
+
module Parsanol
|
|
13
|
+
class Context
|
|
14
|
+
include Parsanol
|
|
15
|
+
|
|
16
|
+
# Creates a new context with the given bindings.
|
|
17
|
+
# Each binding becomes accessible as both a method and an instance variable.
|
|
18
|
+
#
|
|
19
|
+
# @param bindings [Hash] variable name => value pairs
|
|
20
|
+
def initialize(bindings)
|
|
21
|
+
bindings.each_pair do |key, val|
|
|
22
|
+
# Define accessor method on singleton class
|
|
23
|
+
define_singleton_method(key) { val }
|
|
24
|
+
# Also set as instance variable for @-style access
|
|
25
|
+
instance_variable_set("@#{key}", val)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
# Defines a method on the object's singleton class.
|
|
32
|
+
#
|
|
33
|
+
# @param name [Symbol, String] method name
|
|
34
|
+
# @yield block to execute when method is called
|
|
35
|
+
def define_singleton_method(name, &)
|
|
36
|
+
singleton_class.define_method(name, &)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Debug helper for parser development.
|
|
4
|
+
# Adds a convenient method to Parsanol::Atoms::Base for debugging parse failures.
|
|
5
|
+
#
|
|
6
|
+
# @example
|
|
7
|
+
# class MyParser < Parsanol::Parser
|
|
8
|
+
# rule(:foo) { str('foo') }
|
|
9
|
+
# root(:foo)
|
|
10
|
+
# end
|
|
11
|
+
#
|
|
12
|
+
# # Instead of writing rescue blocks:
|
|
13
|
+
# MyParser.new.parse_with_debug('invalid')
|
|
14
|
+
# # Prints the error tree automatically and returns nil
|
|
15
|
+
#
|
|
16
|
+
# Inspired by Parslet (MIT License).
|
|
17
|
+
module Parsanol
|
|
18
|
+
module Atoms
|
|
19
|
+
class Base
|
|
20
|
+
# Parses input and automatically displays error information on failure.
|
|
21
|
+
# This is a convenience method for development and debugging.
|
|
22
|
+
# Unlike #parse, this method catches ParseFailed and prints debug info.
|
|
23
|
+
#
|
|
24
|
+
# @param input [String] the input to parse
|
|
25
|
+
# @param options [Hash] options passed to #parse
|
|
26
|
+
# @return [Object] parse result on success, nil on failure
|
|
27
|
+
def parse_with_debug(input, options = {})
|
|
28
|
+
parse(input, options)
|
|
29
|
+
rescue Parsanol::ParseFailed => e
|
|
30
|
+
# Display the error tree for debugging
|
|
31
|
+
puts e.parse_failure_cause.ascii_tree
|
|
32
|
+
nil
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Edit tracking for GPeg-style incremental parsing
|
|
4
|
+
# Based on the GPeg paper: "Fast Incremental PEG Parsing" (Yedidia, SLE 2021)
|
|
5
|
+
#
|
|
6
|
+
# Tracks edits to the input as [position, delta] pairs and enables lazy shifting
|
|
7
|
+
# of cached intervals without rebuilding the entire cache (O(1) edit cost).
|
|
8
|
+
#
|
|
9
|
+
module Parsanol
|
|
10
|
+
class EditTracker
|
|
11
|
+
# An edit operation: insertion (+delta) or deletion (-delta) at a position
|
|
12
|
+
class Edit
|
|
13
|
+
attr_reader :position, :delta
|
|
14
|
+
|
|
15
|
+
def initialize(position, delta)
|
|
16
|
+
@position = position
|
|
17
|
+
@delta = delta
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def to_s
|
|
21
|
+
if @delta.positive?
|
|
22
|
+
"Insert(#{@delta} chars at #{@position})"
|
|
23
|
+
else
|
|
24
|
+
"Delete(#{-@delta} chars at #{@position})"
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def initialize
|
|
30
|
+
@edits = [] # List of edits in chronological order
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Record an insertion at position
|
|
34
|
+
# @param position [Integer] Where the insertion occurred
|
|
35
|
+
# @param length [Integer] Number of characters inserted
|
|
36
|
+
def insert(position, length)
|
|
37
|
+
@edits << Edit.new(position, length)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Record a deletion at position
|
|
41
|
+
# @param position [Integer] Where the deletion occurred
|
|
42
|
+
# @param length [Integer] Number of characters deleted
|
|
43
|
+
def delete(position, length)
|
|
44
|
+
@edits << Edit.new(position, -length)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Shift an interval based on accumulated edits
|
|
48
|
+
# Returns the shifted interval [low', high') or nil if interval is invalidated
|
|
49
|
+
#
|
|
50
|
+
# An interval is invalidated if any edit overlaps with it, as the cached
|
|
51
|
+
# parse result is no longer valid.
|
|
52
|
+
#
|
|
53
|
+
# @param low [Integer] Interval start position
|
|
54
|
+
# @param high [Integer] Interval end position (exclusive)
|
|
55
|
+
# @return [Array<Integer>, nil] Shifted [low, high) or nil if invalidated
|
|
56
|
+
def shift_interval(low, high)
|
|
57
|
+
shifted_low = low
|
|
58
|
+
shifted_high = high
|
|
59
|
+
|
|
60
|
+
@edits.each do |edit|
|
|
61
|
+
# Skip zero-length edits (no-ops)
|
|
62
|
+
next if edit.delta.zero?
|
|
63
|
+
|
|
64
|
+
# Check if edit overlaps with current interval
|
|
65
|
+
# Edit overlaps if it occurs within [shifted_low, shifted_high)
|
|
66
|
+
if edit.position >= shifted_low && edit.position < shifted_high
|
|
67
|
+
# Edit inside interval - invalidate
|
|
68
|
+
return nil
|
|
69
|
+
elsif edit.position < shifted_low
|
|
70
|
+
# Edit before interval - shift both boundaries
|
|
71
|
+
shifted_low += edit.delta
|
|
72
|
+
shifted_high += edit.delta
|
|
73
|
+
elsif edit.position >= shifted_high
|
|
74
|
+
# Edit after interval - no shift needed
|
|
75
|
+
# Continue to next edit
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Sanity check: ensure interval remains valid
|
|
79
|
+
return nil if shifted_low.negative? || shifted_high < shifted_low
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
[shifted_low, shifted_high]
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Check if interval needs invalidation (overlaps with any edit)
|
|
86
|
+
# @param low [Integer] Interval start position
|
|
87
|
+
# @param high [Integer] Interval end position (exclusive)
|
|
88
|
+
# @return [Boolean] true if interval should be invalidated
|
|
89
|
+
def invalidates?(low, high)
|
|
90
|
+
shift_interval(low, high).nil?
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Clear all recorded edits
|
|
94
|
+
def clear
|
|
95
|
+
@edits.clear
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Number of edits tracked
|
|
99
|
+
def size
|
|
100
|
+
@edits.size
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Check if any edits have been recorded
|
|
104
|
+
def empty?
|
|
105
|
+
@edits.empty?
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Get all edits (for debugging)
|
|
109
|
+
attr_reader :edits
|
|
110
|
+
end
|
|
111
|
+
end
|