parsanol 1.0.1-aarch64-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.
Files changed (101) hide show
  1. checksums.yaml +7 -0
  2. data/HISTORY.txt +12 -0
  3. data/LICENSE +23 -0
  4. data/README.adoc +487 -0
  5. data/Rakefile +135 -0
  6. data/lib/parsanol/3.2/parsanol_native.so +0 -0
  7. data/lib/parsanol/3.3/parsanol_native.so +0 -0
  8. data/lib/parsanol/3.4/parsanol_native.so +0 -0
  9. data/lib/parsanol/4.0/parsanol_native.so +0 -0
  10. data/lib/parsanol/ast_visitor.rb +122 -0
  11. data/lib/parsanol/atoms/alternative.rb +122 -0
  12. data/lib/parsanol/atoms/base.rb +202 -0
  13. data/lib/parsanol/atoms/can_flatten.rb +194 -0
  14. data/lib/parsanol/atoms/capture.rb +38 -0
  15. data/lib/parsanol/atoms/context.rb +334 -0
  16. data/lib/parsanol/atoms/context_optimized.rb +38 -0
  17. data/lib/parsanol/atoms/custom.rb +110 -0
  18. data/lib/parsanol/atoms/cut.rb +66 -0
  19. data/lib/parsanol/atoms/dsl.rb +96 -0
  20. data/lib/parsanol/atoms/dynamic.rb +39 -0
  21. data/lib/parsanol/atoms/entity.rb +75 -0
  22. data/lib/parsanol/atoms/ignored.rb +37 -0
  23. data/lib/parsanol/atoms/infix.rb +162 -0
  24. data/lib/parsanol/atoms/lookahead.rb +82 -0
  25. data/lib/parsanol/atoms/named.rb +74 -0
  26. data/lib/parsanol/atoms/re.rb +83 -0
  27. data/lib/parsanol/atoms/repetition.rb +259 -0
  28. data/lib/parsanol/atoms/scope.rb +35 -0
  29. data/lib/parsanol/atoms/sequence.rb +194 -0
  30. data/lib/parsanol/atoms/str.rb +103 -0
  31. data/lib/parsanol/atoms/visitor.rb +91 -0
  32. data/lib/parsanol/atoms.rb +46 -0
  33. data/lib/parsanol/buffer.rb +133 -0
  34. data/lib/parsanol/builder_callbacks.rb +353 -0
  35. data/lib/parsanol/cause.rb +122 -0
  36. data/lib/parsanol/context.rb +39 -0
  37. data/lib/parsanol/convenience.rb +36 -0
  38. data/lib/parsanol/edit_tracker.rb +111 -0
  39. data/lib/parsanol/error_reporter/contextual.rb +99 -0
  40. data/lib/parsanol/error_reporter/deepest.rb +120 -0
  41. data/lib/parsanol/error_reporter/tree.rb +63 -0
  42. data/lib/parsanol/error_reporter.rb +100 -0
  43. data/lib/parsanol/expression/treetop.rb +154 -0
  44. data/lib/parsanol/expression.rb +106 -0
  45. data/lib/parsanol/fast_mode.rb +149 -0
  46. data/lib/parsanol/first_set.rb +79 -0
  47. data/lib/parsanol/grammar_builder.rb +177 -0
  48. data/lib/parsanol/incremental_parser.rb +177 -0
  49. data/lib/parsanol/interval_tree.rb +217 -0
  50. data/lib/parsanol/lazy_result.rb +179 -0
  51. data/lib/parsanol/lexer.rb +144 -0
  52. data/lib/parsanol/mermaid.rb +139 -0
  53. data/lib/parsanol/native/parser.rb +612 -0
  54. data/lib/parsanol/native/serializer.rb +248 -0
  55. data/lib/parsanol/native/transformer.rb +435 -0
  56. data/lib/parsanol/native/types.rb +42 -0
  57. data/lib/parsanol/native.rb +217 -0
  58. data/lib/parsanol/optimizer.rb +85 -0
  59. data/lib/parsanol/optimizers/choice_optimizer.rb +78 -0
  60. data/lib/parsanol/optimizers/cut_inserter.rb +179 -0
  61. data/lib/parsanol/optimizers/lookahead_optimizer.rb +50 -0
  62. data/lib/parsanol/optimizers/quantifier_optimizer.rb +60 -0
  63. data/lib/parsanol/optimizers/sequence_optimizer.rb +97 -0
  64. data/lib/parsanol/options/ruby_transform.rb +107 -0
  65. data/lib/parsanol/options/serialized.rb +94 -0
  66. data/lib/parsanol/options/zero_copy.rb +128 -0
  67. data/lib/parsanol/options.rb +20 -0
  68. data/lib/parsanol/parallel.rb +133 -0
  69. data/lib/parsanol/parser.rb +182 -0
  70. data/lib/parsanol/parslet.rb +151 -0
  71. data/lib/parsanol/pattern/binding.rb +91 -0
  72. data/lib/parsanol/pattern.rb +159 -0
  73. data/lib/parsanol/pool.rb +219 -0
  74. data/lib/parsanol/pools/array_pool.rb +75 -0
  75. data/lib/parsanol/pools/buffer_pool.rb +175 -0
  76. data/lib/parsanol/pools/position_pool.rb +92 -0
  77. data/lib/parsanol/pools/slice_pool.rb +64 -0
  78. data/lib/parsanol/position.rb +94 -0
  79. data/lib/parsanol/resettable.rb +29 -0
  80. data/lib/parsanol/result.rb +46 -0
  81. data/lib/parsanol/result_builder.rb +208 -0
  82. data/lib/parsanol/result_stream.rb +261 -0
  83. data/lib/parsanol/rig/rspec.rb +71 -0
  84. data/lib/parsanol/rope.rb +81 -0
  85. data/lib/parsanol/scope.rb +104 -0
  86. data/lib/parsanol/slice.rb +146 -0
  87. data/lib/parsanol/source/line_cache.rb +109 -0
  88. data/lib/parsanol/source.rb +180 -0
  89. data/lib/parsanol/source_location.rb +167 -0
  90. data/lib/parsanol/streaming_parser.rb +124 -0
  91. data/lib/parsanol/string_view.rb +195 -0
  92. data/lib/parsanol/transform.rb +226 -0
  93. data/lib/parsanol/version.rb +5 -0
  94. data/lib/parsanol/wasm/README.md +80 -0
  95. data/lib/parsanol/wasm/package.json +51 -0
  96. data/lib/parsanol/wasm/parsanol.js +252 -0
  97. data/lib/parsanol/wasm/parslet.d.ts +129 -0
  98. data/lib/parsanol/wasm_parser.rb +240 -0
  99. data/lib/parsanol.rb +280 -0
  100. data/parsanol-ruby.gemspec +67 -0
  101. metadata +280 -0
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Parser atoms - the building blocks for grammars.
4
+ # Each atom type handles a specific parsing primitive or combinator.
5
+ module Parsanol
6
+ module Atoms
7
+ # Precedence levels for pretty-printing.
8
+ # Higher values bind more loosely.
9
+ module Precedence
10
+ ATOM = 1 # literals, entities
11
+ LOOKAHEAD = 2 # &expr, !expr
12
+ REPETITION = 3 # expr*, expr+, expr?
13
+ SEQUENCE = 4 # expr expr
14
+ CHOICE = 5 # expr | expr
15
+ TOP = 6 # outer level
16
+
17
+ # Backward-compatible aliases
18
+ BASE = ATOM
19
+ ALTERNATE = CHOICE
20
+ OUTER = TOP
21
+ end
22
+
23
+ # Load atom implementations
24
+ require 'parsanol/atoms/can_flatten'
25
+ require 'parsanol/atoms/context'
26
+ require 'parsanol/atoms/dsl'
27
+ require 'parsanol/atoms/base'
28
+ require 'parsanol/atoms/custom'
29
+ require 'parsanol/atoms/ignored'
30
+ require 'parsanol/atoms/named'
31
+ require 'parsanol/atoms/lookahead'
32
+ require 'parsanol/atoms/cut'
33
+ require 'parsanol/atoms/alternative'
34
+ require 'parsanol/atoms/sequence'
35
+ require 'parsanol/atoms/repetition'
36
+ require 'parsanol/atoms/re'
37
+ require 'parsanol/atoms/str'
38
+ require 'parsanol/atoms/entity'
39
+ require 'parsanol/atoms/capture'
40
+ require 'parsanol/atoms/dynamic'
41
+ require 'parsanol/atoms/scope'
42
+ require 'parsanol/atoms/infix'
43
+ # Load visitor pattern (must be after all atom classes)
44
+ require 'parsanol/atoms/visitor'
45
+ end
46
+ end
@@ -0,0 +1,133 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Parsanol
4
+ # A fixed-size buffer for efficient array operations.
5
+ #
6
+ # Buffer wraps an array with a logical size separate from capacity.
7
+ # This allows reusing buffers without reallocating arrays, reducing GC pressure.
8
+ #
9
+ # == Usage
10
+ #
11
+ # buffer = Buffer.new(capacity: 10)
12
+ # buffer.push("a")
13
+ # buffer.push("b")
14
+ # buffer.size # => 2
15
+ # buffer.to_a # => ["a", "b"]
16
+ # buffer.clear!
17
+ # buffer.size # => 0 (but capacity still 10)
18
+ #
19
+ # == Size vs Capacity
20
+ #
21
+ # - Size: Number of elements logically in the buffer
22
+ # - Capacity: Maximum elements before reallocation
23
+ #
24
+ # Reusing buffers maintains capacity while resetting size.
25
+ #
26
+ class Buffer
27
+ include Resettable
28
+
29
+ # @return [Integer] Logical size (number of elements)
30
+ attr_reader :size
31
+
32
+ # @return [Integer] Maximum capacity before growth
33
+ attr_reader :capacity
34
+
35
+ # @return [Array] Underlying array storage
36
+ attr_reader :storage
37
+
38
+ # Initialize a new buffer with specified capacity.
39
+ #
40
+ # @param capacity [Integer] Initial capacity (default: 10)
41
+ #
42
+ def initialize(capacity: 10)
43
+ @capacity = capacity
44
+ @storage = Array.new(capacity)
45
+ @size = 0
46
+ end
47
+
48
+ # Add an element to the buffer.
49
+ #
50
+ # Grows buffer if needed, but this should be rare with proper size classes.
51
+ #
52
+ # @param element [Object] Element to add
53
+ # @return [self] For method chaining
54
+ #
55
+ def push(element)
56
+ grow! if @size >= @capacity
57
+ @storage[@size] = element
58
+ @size += 1
59
+ self
60
+ end
61
+
62
+ alias << push
63
+
64
+ # Get element at index.
65
+ #
66
+ # @param index [Integer] Zero-based index
67
+ # @return [Object] Element at index, or nil if out of bounds
68
+ #
69
+ def [](index)
70
+ return nil if index >= @size
71
+
72
+ @storage[index]
73
+ end
74
+
75
+ # Set element at index.
76
+ #
77
+ # @param index [Integer] Zero-based index
78
+ # @param value [Object] Value to set
79
+ #
80
+ def []=(index, value)
81
+ @storage[index] = value if index < @size
82
+ end
83
+
84
+ # Convert buffer to array (creates new array slice of logical size).
85
+ #
86
+ # @return [Array] Array containing elements [0...size]
87
+ #
88
+ def to_a
89
+ @storage[0...@size]
90
+ end
91
+
92
+ # Clear the buffer (reset logical size, keep capacity).
93
+ #
94
+ # @return [self] For method chaining
95
+ #
96
+ def clear!
97
+ # Clear references for GC (keep capacity)
98
+ @size.upto(@capacity - 1) { |i| @storage[i] = nil }
99
+ @size = 0
100
+ self
101
+ end
102
+
103
+ # Check if buffer is empty.
104
+ #
105
+ # @return [Boolean] true if size is zero
106
+ #
107
+ def empty?
108
+ @size.zero?
109
+ end
110
+
111
+ # Reset protocol for ObjectPool compatibility.
112
+ # Delegates to clear! for buffer cleanup.
113
+ #
114
+ # @return [self] For method chaining
115
+ #
116
+ def reset!
117
+ clear!
118
+ end
119
+
120
+ private
121
+
122
+ # Grow buffer capacity (double it).
123
+ # Should be rare with proper size class selection.
124
+ #
125
+ def grow!
126
+ new_capacity = @capacity * 2
127
+ new_storage = Array.new(new_capacity)
128
+ @storage.each_with_index { |elem, i| new_storage[i] = elem }
129
+ @storage = new_storage
130
+ @capacity = new_capacity
131
+ end
132
+ end
133
+ end
@@ -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, &body)
36
+ singleton_class.define_method(name, &body)
37
+ end
38
+ end
39
+ end