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,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
|