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.
Files changed (100) hide show
  1. checksums.yaml +7 -0
  2. data/HISTORY.txt +98 -0
  3. data/LICENSE +23 -0
  4. data/README.adoc +905 -0
  5. data/Rakefile +141 -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 +123 -0
  12. data/lib/parsanol/atoms/base.rb +208 -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 +357 -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 +167 -0
  24. data/lib/parsanol/atoms/lookahead.rb +85 -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 +277 -0
  28. data/lib/parsanol/atoms/scope.rb +35 -0
  29. data/lib/parsanol/atoms/sequence.rb +195 -0
  30. data/lib/parsanol/atoms/str.rb +109 -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 +187 -0
  46. data/lib/parsanol/first_set.rb +79 -0
  47. data/lib/parsanol/grammar_builder.rb +179 -0
  48. data/lib/parsanol/incremental_parser.rb +182 -0
  49. data/lib/parsanol/interval_tree.rb +226 -0
  50. data/lib/parsanol/lazy_result.rb +179 -0
  51. data/lib/parsanol/mermaid.rb +142 -0
  52. data/lib/parsanol/native/batch_decoder.rb +255 -0
  53. data/lib/parsanol/native/dynamic.rb +238 -0
  54. data/lib/parsanol/native/parser.rb +102 -0
  55. data/lib/parsanol/native/serializer.rb +252 -0
  56. data/lib/parsanol/native/transformer.rb +604 -0
  57. data/lib/parsanol/native/types.rb +29 -0
  58. data/lib/parsanol/native.rb +223 -0
  59. data/lib/parsanol/optimizer.rb +85 -0
  60. data/lib/parsanol/optimizers/choice_optimizer.rb +78 -0
  61. data/lib/parsanol/optimizers/cut_inserter.rb +182 -0
  62. data/lib/parsanol/optimizers/lookahead_optimizer.rb +56 -0
  63. data/lib/parsanol/optimizers/quantifier_optimizer.rb +60 -0
  64. data/lib/parsanol/optimizers/sequence_optimizer.rb +97 -0
  65. data/lib/parsanol/options/zero_copy.rb +127 -0
  66. data/lib/parsanol/options.rb +21 -0
  67. data/lib/parsanol/parallel.rb +128 -0
  68. data/lib/parsanol/parser.rb +242 -0
  69. data/lib/parsanol/parslet.rb +151 -0
  70. data/lib/parsanol/pattern/binding.rb +91 -0
  71. data/lib/parsanol/pattern.rb +162 -0
  72. data/lib/parsanol/pool.rb +219 -0
  73. data/lib/parsanol/pools/array_pool.rb +75 -0
  74. data/lib/parsanol/pools/buffer_pool.rb +182 -0
  75. data/lib/parsanol/pools/position_pool.rb +92 -0
  76. data/lib/parsanol/pools/slice_pool.rb +64 -0
  77. data/lib/parsanol/position.rb +94 -0
  78. data/lib/parsanol/resettable.rb +29 -0
  79. data/lib/parsanol/result.rb +46 -0
  80. data/lib/parsanol/result_builder.rb +208 -0
  81. data/lib/parsanol/result_stream.rb +266 -0
  82. data/lib/parsanol/rig/rspec.rb +71 -0
  83. data/lib/parsanol/rope.rb +81 -0
  84. data/lib/parsanol/scope.rb +104 -0
  85. data/lib/parsanol/slice.rb +160 -0
  86. data/lib/parsanol/source/line_cache.rb +102 -0
  87. data/lib/parsanol/source.rb +185 -0
  88. data/lib/parsanol/source_location.rb +167 -0
  89. data/lib/parsanol/streaming_parser.rb +124 -0
  90. data/lib/parsanol/string_view.rb +198 -0
  91. data/lib/parsanol/transform.rb +226 -0
  92. data/lib/parsanol/version.rb +5 -0
  93. data/lib/parsanol/wasm/README.md +80 -0
  94. data/lib/parsanol/wasm/package.json +51 -0
  95. data/lib/parsanol/wasm/parsanol.js +252 -0
  96. data/lib/parsanol/wasm/parslet.d.ts +129 -0
  97. data/lib/parsanol/wasm_parser.rb +240 -0
  98. data/lib/parsanol.rb +278 -0
  99. data/parsanol.gemspec +67 -0
  100. metadata +279 -0
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Named capture - assigns a label to matched content.
4
+ # Results appear as { label: value } in the parse tree.
5
+ #
6
+ # @example Labeling matches
7
+ # str('foo').as(:name) # returns { name: 'foo' }
8
+ #
9
+ module Parsanol
10
+ module Atoms
11
+ class Named < Parsanol::Atoms::Base
12
+ # @return [Parsanol::Atoms::Base] wrapped parser
13
+ attr_reader :parslet
14
+
15
+ # @return [Symbol] the capture label
16
+ attr_reader :name
17
+
18
+ # Creates a new named capture.
19
+ #
20
+ # @param parser [Parsanol::Atoms::Base] parser to wrap
21
+ # @param label [Symbol] name for captures
22
+ def initialize(parser, label)
23
+ super()
24
+ @parslet = parser
25
+ @name = label
26
+ end
27
+
28
+ # Applies parser and wraps result in hash.
29
+ #
30
+ # @param source [Parsanol::Source] input
31
+ # @param context [Parsanol::Atoms::Context] context
32
+ # @param consume_all [Boolean] require full consumption
33
+ # @return [Array(Boolean, Object)] result
34
+ def apply(source, context, consume_all)
35
+ success, value = @parslet.apply(source, context, consume_all)
36
+ return [false, value] unless success
37
+
38
+ ok(wrap_result(value))
39
+ end
40
+
41
+ # Named wrappers skip caching (inner parser handles it).
42
+ #
43
+ # @return [Boolean]
44
+ def cached?
45
+ false
46
+ end
47
+
48
+ # String representation.
49
+ #
50
+ # @param prec [Integer] precedence
51
+ # @return [String]
52
+ def to_s_inner(prec)
53
+ "#{@name}:#{@parslet.to_s(prec)}"
54
+ end
55
+
56
+ # FIRST set is wrapped parser's FIRST set.
57
+ #
58
+ # @return [Set]
59
+ def compute_first_set
60
+ @parslet.first_set
61
+ end
62
+
63
+ private
64
+
65
+ # Wraps matched value in labeled hash.
66
+ #
67
+ # @param matched [Object] matched value
68
+ # @return [Hash] labeled result
69
+ def wrap_result(matched)
70
+ { @name => flatten(matched, true) }
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Regular expression matcher for single characters.
4
+ # Matches one character against a character class pattern.
5
+ #
6
+ # @example Character classes
7
+ # match('[a-z]') # matches a-z
8
+ # match('\d') # matches digits
9
+ # any # matches any character
10
+ #
11
+ module Parsanol
12
+ module Atoms
13
+ class Re < Parsanol::Atoms::Base
14
+ # @return [String] the pattern string
15
+ attr_reader :match
16
+
17
+ # @return [Regexp] compiled pattern
18
+ attr_reader :re
19
+
20
+ # Creates a new regex matcher.
21
+ #
22
+ # @param pattern [String, Object] regex character class
23
+ def initialize(pattern)
24
+ super()
25
+ @match = pattern.to_s
26
+ @re = Regexp.new(@match, Regexp::MULTILINE)
27
+
28
+ # Extract pattern for display (strip delimiters)
29
+ @display = @match.inspect[1..-2] || @match
30
+
31
+ # Pre-built error messages
32
+ @eof_error = "Unexpected end of input"
33
+ @no_match_error = "Failed to match #{@display}"
34
+ end
35
+
36
+ # Matches one character against the pattern.
37
+ #
38
+ # @param source [Parsanol::Source] input
39
+ # @param context [Parsanol::Atoms::Context] context
40
+ # @param _consume_all [Boolean] ignored
41
+ # @return [Array(Boolean, Object)] result
42
+ def try(source, context, _consume_all)
43
+ # Fast path: check if next char matches
44
+ return ok(source.consume(1)) if source.matches?(@re)
45
+
46
+ # No input left
47
+ return context.err(self, source, @eof_error) if source.chars_left < 1
48
+
49
+ # Character doesn't match
50
+ context.err(self, source, @no_match_error)
51
+ end
52
+
53
+ # String representation.
54
+ #
55
+ # @param _prec [Integer] unused
56
+ # @return [String]
57
+ def to_s_inner(_prec)
58
+ @display
59
+ end
60
+
61
+ # Simple atoms don't benefit from caching.
62
+ #
63
+ # @return [Boolean]
64
+ def cached?
65
+ false
66
+ end
67
+
68
+ # Produces flat results.
69
+ #
70
+ # @return [Boolean]
71
+ def flat?
72
+ true
73
+ end
74
+
75
+ # FIRST set is this atom.
76
+ #
77
+ # @return [Set]
78
+ def compute_first_set
79
+ Set.new([self])
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,277 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Repetition - matches a parser multiple times.
4
+ # Supports min/max bounds for various quantifier patterns.
5
+ #
6
+ # @example Quantifiers
7
+ # str('a').repeat(1,3) # 1 to 3 'a's
8
+ # str('a').maybe # optional 'a' (0 or 1)
9
+ # str('a').repeat # zero or more
10
+ #
11
+ module Parsanol
12
+ module Atoms
13
+ class Repetition < Parsanol::Atoms::Base
14
+ # @return [Integer] minimum matches required
15
+ attr_reader :min
16
+
17
+ # @return [Integer, nil] maximum matches allowed
18
+ attr_reader :max
19
+
20
+ # @return [Parsanol::Atoms::Base] repeated parser
21
+ attr_reader :parslet
22
+
23
+ # @return [Symbol] result tag
24
+ attr_reader :result_tag
25
+
26
+ # Alias for compatibility
27
+ alias tag result_tag
28
+
29
+ # Creates a new repetition.
30
+ #
31
+ # @param parser [Parsanol::Atoms::Base] parser to repeat
32
+ # @param min_count [Integer] minimum repetitions
33
+ # @param max_count [Integer, nil] maximum repetitions
34
+ # @param tag [Symbol] result tag
35
+ def initialize(parser, min_count, max_count, tag = :repetition)
36
+ super()
37
+
38
+ # Handle nil max_count (unbounded repetition)
39
+ if max_count&.zero?
40
+ raise ArgumentError, "Cannot repeat zero times: #{parser.inspect}"
41
+ end
42
+
43
+ @parslet = parser
44
+ @min = min_count
45
+ @max = max_count
46
+ @result_tag = tag
47
+
48
+ # Internal value for comparisons (nil becomes infinity)
49
+ @max_internal = max_count || Float::INFINITY
50
+
51
+ # Pre-built error messages
52
+ @min_error = "Expected at least #{min_count} of #{parser.inspect}"
53
+ @extra_error = "Extra input after last repetition"
54
+ end
55
+
56
+ # Error messages hash (for compatibility)
57
+ def error_msgs
58
+ { minrep: @min_error, unconsumed: @extra_error }
59
+ end
60
+
61
+ # Executes the repetition.
62
+ #
63
+ # @param source [Parsanol::Source] input
64
+ # @param context [Parsanol::Atoms::Context] context
65
+ # @param consume_all [Boolean] require full consumption
66
+ # @return [Array(Boolean, Object)] result
67
+ def try(source, context, consume_all)
68
+ # Check for tree memoization support
69
+ if context.respond_to?(:use_tree_memoization?) && context.use_tree_memoization?
70
+ return with_tree_cache(source, context, consume_all)
71
+ end
72
+
73
+ # Maybe (0 or 1) - very common, optimize
74
+ if @min.zero? && @max == 1
75
+ return try_maybe(source, context,
76
+ consume_all)
77
+ end
78
+
79
+ # Exact count optimization
80
+ if @min == @max && @max && @max <= 3
81
+ return try_exact(source, context,
82
+ consume_all)
83
+ end
84
+
85
+ # General case
86
+ try_general(source, context, consume_all)
87
+ end
88
+
89
+ precedence REPETITION
90
+
91
+ # String representation.
92
+ #
93
+ # @param prec [Integer] precedence
94
+ # @return [String]
95
+ def to_s_inner(prec)
96
+ suffix = if @min.zero? && @max == 1
97
+ "?"
98
+ else
99
+ "{#{@min}, #{@max}}"
100
+ end
101
+ @parslet.to_s(prec) + suffix
102
+ end
103
+
104
+ # FIRST set includes EPSILON if min == 0.
105
+ #
106
+ # @return [Set]
107
+ def compute_first_set
108
+ first = @parslet.first_set.dup
109
+ first.add(Parsanol::FirstSet::EPSILON) if @min.zero?
110
+ first
111
+ end
112
+
113
+ private
114
+
115
+ # Optional match (0 or 1)
116
+ def try_maybe(source, context, _consume_all)
117
+ success, value = @parslet.apply(source, context, false)
118
+ return ok([@result_tag, value]) if success
119
+
120
+ ok(@result_tag == :repetition ? Parsanol::Atoms::Base::REP_TAG : [@result_tag])
121
+ end
122
+
123
+ # Exact count match (1, 2, or 3)
124
+ def try_exact(source, context, consume_all)
125
+ case @max
126
+ when 1
127
+ single_match(source, context, consume_all)
128
+ when 2
129
+ double_match(source, context, consume_all)
130
+ when 3
131
+ triple_match(source, context, consume_all)
132
+ end
133
+ end
134
+
135
+ def single_match(source, context, consume_all)
136
+ success, value = @parslet.apply(source, context, consume_all)
137
+ return ok([@result_tag, value]) if success
138
+
139
+ context.err_at(self, source, @min_error, source.bytepos, [value])
140
+ end
141
+
142
+ def double_match(source, context, consume_all)
143
+ success, v1 = @parslet.apply(source, context, false)
144
+ unless success
145
+ return context.err_at(self, source, @min_error, source.bytepos,
146
+ [v1])
147
+ end
148
+
149
+ success, v2 = @parslet.apply(source, context, consume_all)
150
+ return ok([@result_tag, v1, v2]) if success
151
+
152
+ context.err_at(self, source, @min_error, source.bytepos, [v2])
153
+ end
154
+
155
+ def triple_match(source, context, consume_all)
156
+ success, v1 = @parslet.apply(source, context, false)
157
+ unless success
158
+ return context.err_at(self, source, @min_error, source.bytepos,
159
+ [v1])
160
+ end
161
+
162
+ success, v2 = @parslet.apply(source, context, false)
163
+ unless success
164
+ return context.err_at(self, source, @min_error, source.bytepos,
165
+ [v2])
166
+ end
167
+
168
+ success, v3 = @parslet.apply(source, context, consume_all)
169
+ return ok([@result_tag, v1, v2, v3]) if success
170
+
171
+ context.err_at(self, source, @min_error, source.bytepos, [v3])
172
+ end
173
+
174
+ # General repetition with buffer pooling
175
+ def try_general(source, context, consume_all)
176
+ start_pos = source.bytepos
177
+ occurrence = 0
178
+
179
+ # Estimate buffer size
180
+ estimate = [@max || 10, 10].min
181
+ buffer = context.acquire_buffer(size: estimate + 1)
182
+ buffer.push(@result_tag)
183
+
184
+ last_error = nil
185
+
186
+ loop do
187
+ success, value = @parslet.apply(source, context, false)
188
+ last_error = value
189
+
190
+ break unless success
191
+
192
+ occurrence += 1
193
+ buffer.push(value)
194
+
195
+ break if @max && occurrence >= @max
196
+ end
197
+
198
+ # Check minimum bound
199
+ if occurrence < @min
200
+ context.release_buffer(buffer)
201
+ source.bytepos = start_pos
202
+ return context.err_at(self, source, @min_error, start_pos,
203
+ [last_error])
204
+ end
205
+
206
+ # Check complete consumption
207
+ if consume_all && source.chars_left.positive?
208
+ context.release_buffer(buffer)
209
+ return context.err(self, source, @extra_error, [last_error])
210
+ end
211
+
212
+ ok(Parsanol::LazyResult.new(buffer, context))
213
+ end
214
+
215
+ # Tree memoization for GPEG-style caching
216
+ def with_tree_cache(source, context, consume_all)
217
+ start_pos = source.bytepos
218
+ cache_key = object_id
219
+
220
+ # Check cache
221
+ cached = context.query_tree_memo(cache_key, start_pos)
222
+ if cached
223
+ values, end_pos = cached
224
+ source.bytepos = end_pos
225
+ return ok([@result_tag] + values)
226
+ end
227
+
228
+ # Parse and cache
229
+ occurrence = 0
230
+ estimate = [@max || 10, 10].min
231
+ buffer = context.acquire_buffer(size: estimate + 1)
232
+ buffer.push(@result_tag)
233
+
234
+ positions = context.acquire_array
235
+ positions << start_pos
236
+ last_error = nil
237
+
238
+ loop do
239
+ source.bytepos
240
+ success, value = @parslet.apply(source, context, false)
241
+ last_error = value
242
+
243
+ break unless success
244
+
245
+ occurrence += 1
246
+ buffer.push(value)
247
+ positions << source.bytepos
248
+
249
+ break if @max && occurrence >= @max
250
+ end
251
+
252
+ # Cache successful prefix
253
+ if occurrence.positive?
254
+ end_pos = positions[occurrence]
255
+ context.store_tree_memo(cache_key, start_pos, buffer.to_a[1..],
256
+ end_pos)
257
+ end
258
+
259
+ # Check minimum
260
+ if occurrence < @min
261
+ context.release_buffer(buffer)
262
+ source.bytepos = start_pos
263
+ return context.err_at(self, source, @min_error, start_pos,
264
+ [last_error])
265
+ end
266
+
267
+ # Check consumption
268
+ if consume_all && source.chars_left.positive?
269
+ context.release_buffer(buffer)
270
+ return context.err(self, source, @extra_error, [last_error])
271
+ end
272
+
273
+ ok(Parsanol::LazyResult.new(buffer, context))
274
+ end
275
+ end
276
+ end
277
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Starts a new scope in the parsing process. Please also see the #captures
4
+ # method.
5
+ #
6
+ module Parsanol
7
+ module Atoms
8
+ class Scope < Parsanol::Atoms::Base
9
+ attr_reader :block
10
+
11
+ def initialize(block)
12
+ super()
13
+
14
+ @block = block
15
+ end
16
+
17
+ def cached?
18
+ false
19
+ end
20
+
21
+ def apply(source, context, consume_all)
22
+ # Phase 55: Cache @block ivar to reduce lookup overhead
23
+ block = @block
24
+ context.scope do
25
+ parslet = block.call
26
+ return parslet.apply(source, context, consume_all)
27
+ end
28
+ end
29
+
30
+ def to_s_inner(prec)
31
+ "scope { #{block.call.to_s(prec)} }"
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,195 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Sequential composition - matches parsers in left-to-right order.
4
+ # All parsers must succeed for the sequence to succeed.
5
+ #
6
+ # @example Sequence of matches
7
+ # str('a') >> str('b') # matches 'a' then 'b'
8
+ #
9
+ module Parsanol
10
+ module Atoms
11
+ class Sequence < Parsanol::Atoms::Base
12
+ # @return [Array<Parsanol::Atoms::Base>] sequence members
13
+ attr_reader :parslets
14
+
15
+ # Creates a new sequence.
16
+ #
17
+ # @param components [Array<Parsanol::Atoms::Base>] parsers to sequence
18
+ def initialize(*components)
19
+ super()
20
+ @parslets = components
21
+
22
+ # Pre-built error message
23
+ @fail_msg = "Failed to match sequence (#{inspect})"
24
+ end
25
+
26
+ # Error messages hash (for compatibility)
27
+ def error_msgs
28
+ { failed: @fail_msg }
29
+ end
30
+
31
+ # Appends a parser to this sequence with flattening.
32
+ #
33
+ # @param parser [Parsanol::Atoms::Base] parser to append
34
+ # @return [Parsanol::Atoms::Sequence] new flattened sequence
35
+ def >>(other)
36
+ # Flatten nested sequences
37
+ expanded = if other.is_a?(Parsanol::Atoms::Sequence)
38
+ @parslets + other.parslets
39
+ else
40
+ @parslets + [other]
41
+ end
42
+
43
+ # Merge adjacent string literals
44
+ merged = merge_adjacent_strings(expanded)
45
+
46
+ self.class.new(*merged)
47
+ end
48
+
49
+ # Executes all parsers in sequence.
50
+ #
51
+ # @param source [Parsanol::Source] input
52
+ # @param context [Parsanol::Atoms::Context] context
53
+ # @param consume_all [Boolean] require full consumption
54
+ # @return [Array(Boolean, Object)] result
55
+ def try(source, context, consume_all)
56
+ components = @parslets
57
+ count = components.size
58
+
59
+ # Dispatch based on size for optimization
60
+ case count
61
+ when 1
62
+ match_single(components[0], source, context, consume_all)
63
+ when 2
64
+ match_pair(components[0], components[1], source, context, consume_all)
65
+ when 3
66
+ match_triple(components[0], components[1], components[2], source,
67
+ context, consume_all)
68
+ else
69
+ match_general(components, source, context, consume_all)
70
+ end
71
+ end
72
+
73
+ precedence SEQUENCE
74
+
75
+ # String representation.
76
+ #
77
+ # @param prec [Integer] precedence
78
+ # @return [String]
79
+ def to_s_inner(prec)
80
+ @parslets.map { |p| p.to_s(prec) }.join(" ")
81
+ end
82
+
83
+ # FIRST set is first element's FIRST set (with epsilon propagation).
84
+ #
85
+ # @return [Set]
86
+ def compute_first_set
87
+ return Set.new if @parslets.empty?
88
+
89
+ result = Set.new
90
+ @parslets.each do |parser|
91
+ first = parser.first_set
92
+ result.merge(first.reject { |x| x == Parsanol::FirstSet::EPSILON })
93
+ break unless first.include?(Parsanol::FirstSet::EPSILON)
94
+ end
95
+ result
96
+ end
97
+
98
+ private
99
+
100
+ # Single element sequence
101
+ def match_single(parser, source, context, consume_all)
102
+ success, value = parser.apply(source, context, consume_all)
103
+ return context.err(self, source, @fail_msg, [value]) unless success
104
+
105
+ ok([:sequence, value])
106
+ end
107
+
108
+ # Two-element sequence with buffer pooling
109
+ def match_pair(p1, p2, source, context, consume_all)
110
+ success, v1 = p1.apply(source, context, false)
111
+ return context.err(self, source, @fail_msg, [v1]) unless success
112
+
113
+ success, v2 = p2.apply(source, context, consume_all)
114
+ return context.err(self, source, @fail_msg, [v2]) unless success
115
+
116
+ buffer = context.acquire_buffer(size: 3)
117
+ buffer.push(:sequence)
118
+ buffer.push(v1)
119
+ buffer.push(v2)
120
+ ok(Parsanol::LazyResult.new(buffer, context))
121
+ end
122
+
123
+ # Three-element sequence with buffer pooling
124
+ def match_triple(p1, p2, p3, source, context, consume_all)
125
+ success, v1 = p1.apply(source, context, false)
126
+ return context.err(self, source, @fail_msg, [v1]) unless success
127
+
128
+ success, v2 = p2.apply(source, context, false)
129
+ return context.err(self, source, @fail_msg, [v2]) unless success
130
+
131
+ success, v3 = p3.apply(source, context, consume_all)
132
+ return context.err(self, source, @fail_msg, [v3]) unless success
133
+
134
+ buffer = context.acquire_buffer(size: 4)
135
+ buffer.push(:sequence)
136
+ buffer.push(v1)
137
+ buffer.push(v2)
138
+ buffer.push(v3)
139
+ ok(Parsanol::LazyResult.new(buffer, context))
140
+ end
141
+
142
+ # General case for N elements
143
+ def match_general(components, source, context, consume_all)
144
+ buffer = context.acquire_buffer(size: components.size + 1)
145
+ buffer.push(:sequence)
146
+
147
+ last_idx = components.size - 1
148
+ idx = 0
149
+
150
+ while idx <= last_idx
151
+ must_consume = consume_all && (idx == last_idx)
152
+ success, value = components[idx].apply(source, context, must_consume)
153
+
154
+ unless success
155
+ context.release_buffer(buffer)
156
+ return context.err(self, source, @fail_msg, [value])
157
+ end
158
+
159
+ buffer.push(value)
160
+ idx += 1
161
+ end
162
+
163
+ ok(Parsanol::LazyResult.new(buffer, context))
164
+ end
165
+
166
+ # Merges adjacent string atoms using Rope for efficiency
167
+ def merge_adjacent_strings(components)
168
+ result = []
169
+ idx = 0
170
+
171
+ while idx < components.size
172
+ current = components[idx]
173
+
174
+ if current.is_a?(Parsanol::Atoms::Str)
175
+ rope = Parsanol::Rope.new.append(current.str)
176
+ next_idx = idx + 1
177
+
178
+ while next_idx < components.size && components[next_idx].is_a?(Parsanol::Atoms::Str)
179
+ rope.append(components[next_idx].str)
180
+ next_idx += 1
181
+ end
182
+
183
+ result << (next_idx > idx + 1 ? Parsanol::Atoms::Str.new(rope.to_s) : current)
184
+ idx = next_idx
185
+ else
186
+ result << current
187
+ idx += 1
188
+ end
189
+ end
190
+
191
+ result
192
+ end
193
+ end
194
+ end
195
+ end