parsanol 1.3.53-aarch64-linux → 1.3.56-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.
- checksums.yaml +4 -4
- data/lib/parsanol/3.2/parsanol_native.so +2 -2
- data/lib/parsanol/3.3/parsanol_native.so +2 -2
- data/lib/parsanol/3.4/parsanol_native.so +2 -2
- data/lib/parsanol/4.0/parsanol_native.so +2 -2
- data/lib/parsanol/atoms/base.rb +1 -1
- data/lib/parsanol/atoms/cut.rb +1 -1
- data/lib/parsanol/atoms/repetition.rb +13 -2
- data/lib/parsanol/cause.rb +1 -1
- data/lib/parsanol/error_reporter/contextual.rb +1 -1
- data/lib/parsanol/lazy_result.rb +12 -3
- data/lib/parsanol/native/libparsanol.so +0 -0
- data/lib/parsanol/native/transformer.rb +12 -8
- data/lib/parsanol/native.rb +4 -2
- data/lib/parsanol/parser.rb +1 -1
- data/lib/parsanol/pattern.rb +1 -1
- data/lib/parsanol/pool.rb +1 -1
- data/lib/parsanol/rope.rb +1 -1
- data/lib/parsanol/slice.rb +4 -6
- data/lib/parsanol/source/line_cache.rb +1 -1
- data/lib/parsanol/source.rb +2 -2
- data/lib/parsanol/version.rb +1 -1
- data/lib/parsanol/vm.rb +125 -12
- data/lib/parsanol.rb +6 -0
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 06407307b9391eecc790b0d6364e2643ee15a1cee425233af4e66c119cde1c5e
|
|
4
|
+
data.tar.gz: 39627457a04894edc05c20599906995061ec110a6153197c3dd7b03a05b9e327
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 20e45402ce4f15e0d0f711498627f2c6c0b3f78723806a84b3ac2be1e37868bdeb817c7c5c196622e4eb4f0f662971b9cd87b50d48ac04bfa57578d424ca11b7
|
|
7
|
+
data.tar.gz: b63db5788cce9c5ecb96a08b1c3301a97a5f1862039ff3c968b453b724088c055be887652f59933db9bd2c3499e6e46b9b8ec11666c3caa67c2fc6798791098a
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
[diffend] Oversized file quarantined before diffing.
|
|
2
2
|
name: data/lib/parsanol/3.2/parsanol_native.so
|
|
3
|
-
size:
|
|
4
|
-
sha256:
|
|
3
|
+
size: 34626704 bytes
|
|
4
|
+
sha256: 0976c692128d3f93b042159f46dc09f21e93d48e98e2004712c31f001ce6ab6b
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
[diffend] Oversized file quarantined before diffing.
|
|
2
2
|
name: data/lib/parsanol/3.3/parsanol_native.so
|
|
3
|
-
size:
|
|
4
|
-
sha256:
|
|
3
|
+
size: 34861544 bytes
|
|
4
|
+
sha256: c0e30ff6b7cdfd74a0d7dd07b2aaa0b4381585e02162bcb04ce786eee748726a
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
[diffend] Oversized file quarantined before diffing.
|
|
2
2
|
name: data/lib/parsanol/3.4/parsanol_native.so
|
|
3
|
-
size:
|
|
4
|
-
sha256:
|
|
3
|
+
size: 34857328 bytes
|
|
4
|
+
sha256: 2380c24bf2c89e71dcf7521934b5e5bc57366fe0c00106d8aed715eaddbabd99
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
[diffend] Oversized file quarantined before diffing.
|
|
2
2
|
name: data/lib/parsanol/4.0/parsanol_native.so
|
|
3
|
-
size:
|
|
4
|
-
sha256:
|
|
3
|
+
size: 34867336 bytes
|
|
4
|
+
sha256: f43381689aa3e99ff0c2bedd7611874e370344947576335c34a8a4cec2153cd7
|
data/lib/parsanol/atoms/base.rb
CHANGED
|
@@ -208,7 +208,7 @@ module Parsanol
|
|
|
208
208
|
|
|
209
209
|
# Converts raw input to Source if needed.
|
|
210
210
|
def normalize_input(source)
|
|
211
|
-
source.
|
|
211
|
+
source.is_a?(Parsanol::Source) ? source : Parsanol::Source.new(source)
|
|
212
212
|
end
|
|
213
213
|
|
|
214
214
|
# Detects if we're in a Parser context.
|
data/lib/parsanol/atoms/cut.rb
CHANGED
|
@@ -42,7 +42,7 @@ module Parsanol
|
|
|
42
42
|
# 1. Mark the current position as a cut point
|
|
43
43
|
# 2. Empty the backtrack stack (we won't backtrack past here)
|
|
44
44
|
# 3. Aggressively evict cache entries before this position
|
|
45
|
-
context.cut!(source.bytepos)
|
|
45
|
+
context.cut!(source.bytepos)
|
|
46
46
|
|
|
47
47
|
[success, value]
|
|
48
48
|
end
|
|
@@ -66,7 +66,7 @@ module Parsanol
|
|
|
66
66
|
# @return [Array(Boolean, Object)] result
|
|
67
67
|
def try(source, context, consume_all)
|
|
68
68
|
# Check for tree memoization support
|
|
69
|
-
if context.
|
|
69
|
+
if context.use_tree_memoization?
|
|
70
70
|
return with_tree_cache(source, context, consume_all)
|
|
71
71
|
end
|
|
72
72
|
|
|
@@ -183,6 +183,7 @@ module Parsanol
|
|
|
183
183
|
last_failure = nil
|
|
184
184
|
|
|
185
185
|
loop do
|
|
186
|
+
iteration_start = source.bytepos
|
|
186
187
|
success, value = @parslet.apply(source, context, false)
|
|
187
188
|
|
|
188
189
|
unless success
|
|
@@ -193,6 +194,11 @@ module Parsanol
|
|
|
193
194
|
occurrence += 1
|
|
194
195
|
result[occurrence] = value
|
|
195
196
|
|
|
197
|
+
# A body match that consumes nothing cannot make progress;
|
|
198
|
+
# looping again would re-match empty forever. Count the empty
|
|
199
|
+
# match, then stop — the min check below applies as usual.
|
|
200
|
+
break if source.bytepos == iteration_start
|
|
201
|
+
|
|
196
202
|
break if @max && occurrence >= @max
|
|
197
203
|
end
|
|
198
204
|
|
|
@@ -254,6 +260,7 @@ module Parsanol
|
|
|
254
260
|
last_failure = nil
|
|
255
261
|
|
|
256
262
|
loop do
|
|
263
|
+
iteration_start = source.bytepos
|
|
257
264
|
success, value = @parslet.apply(source, context, false)
|
|
258
265
|
|
|
259
266
|
unless success
|
|
@@ -265,6 +272,10 @@ module Parsanol
|
|
|
265
272
|
result[occurrence] = value
|
|
266
273
|
positions << source.bytepos
|
|
267
274
|
|
|
275
|
+
# Zero-width body match: count it, stop iterating (the min
|
|
276
|
+
# check below applies as usual) — mirrors try_general.
|
|
277
|
+
break if source.bytepos == iteration_start
|
|
278
|
+
|
|
268
279
|
break if @max && occurrence >= @max
|
|
269
280
|
end
|
|
270
281
|
|
|
@@ -287,7 +298,7 @@ module Parsanol
|
|
|
287
298
|
context.store_tree_memo(cache_key, start_pos,
|
|
288
299
|
result[1, occurrence], end_pos)
|
|
289
300
|
end
|
|
290
|
-
context.release_array(positions)
|
|
301
|
+
context.release_array(positions)
|
|
291
302
|
|
|
292
303
|
# Check minimum
|
|
293
304
|
if occurrence < @min
|
data/lib/parsanol/cause.rb
CHANGED
|
@@ -74,7 +74,7 @@ module Parsanol
|
|
|
74
74
|
line_num, col_num = @source.line_and_column(@position)
|
|
75
75
|
|
|
76
76
|
formatted_msg = @message.map do |msg|
|
|
77
|
-
msg.
|
|
77
|
+
msg.is_a?(Parsanol::Slice) ? msg.content.inspect : msg.to_s
|
|
78
78
|
end.join
|
|
79
79
|
|
|
80
80
|
"#{formatted_msg} at line #{line_num} char #{col_num}#{@parsing_label}."
|
|
@@ -72,7 +72,7 @@ module Parsanol
|
|
|
72
72
|
cause = super
|
|
73
73
|
|
|
74
74
|
# Apply label if the atom has one
|
|
75
|
-
if atom.
|
|
75
|
+
if atom.is_a?(Parsanol::Atoms::Base) && (lbl = atom.label)
|
|
76
76
|
maybe_update_label(lbl, src.pos.bytepos)
|
|
77
77
|
cause.set_label(@active_label_text)
|
|
78
78
|
end
|
data/lib/parsanol/lazy_result.rb
CHANGED
|
@@ -103,6 +103,15 @@ module Parsanol
|
|
|
103
103
|
|
|
104
104
|
alias kind_of? is_a?
|
|
105
105
|
|
|
106
|
+
# The delegated interface: Array's full class contract (including
|
|
107
|
+
# Enumerable), computed once against the type rather than probed on
|
|
108
|
+
# instances.
|
|
109
|
+
ARRAY_METHODS = (
|
|
110
|
+
Array.instance_methods(true) + Enumerable.instance_methods + [:to_ary]
|
|
111
|
+
).freeze
|
|
112
|
+
|
|
113
|
+
private_constant :ARRAY_METHODS
|
|
114
|
+
|
|
106
115
|
# Respond to array methods.
|
|
107
116
|
#
|
|
108
117
|
# @param method [Symbol] Method name
|
|
@@ -110,7 +119,7 @@ module Parsanol
|
|
|
110
119
|
# @return [Boolean] true if responds
|
|
111
120
|
#
|
|
112
121
|
def respond_to?(method, include_private = false)
|
|
113
|
-
super ||
|
|
122
|
+
super || ARRAY_METHODS.include?(method)
|
|
114
123
|
end
|
|
115
124
|
|
|
116
125
|
# Delegate unknown methods to materialized array.
|
|
@@ -121,7 +130,7 @@ module Parsanol
|
|
|
121
130
|
# @return [Object] Result of method call
|
|
122
131
|
#
|
|
123
132
|
def method_missing(method, ...)
|
|
124
|
-
if
|
|
133
|
+
if ARRAY_METHODS.include?(method)
|
|
125
134
|
to_a.public_send(method, ...)
|
|
126
135
|
else
|
|
127
136
|
super
|
|
@@ -135,7 +144,7 @@ module Parsanol
|
|
|
135
144
|
# @return [Boolean] true if method is supported
|
|
136
145
|
#
|
|
137
146
|
def respond_to_missing?(method, include_private = false)
|
|
138
|
-
|
|
147
|
+
ARRAY_METHODS.include?(method) || super
|
|
139
148
|
end
|
|
140
149
|
|
|
141
150
|
# Compare with another object.
|
|
Binary file
|
|
@@ -391,15 +391,19 @@ module Parsanol
|
|
|
391
391
|
return left.is_a?(Hash) ? left.merge(right) : left + right
|
|
392
392
|
end
|
|
393
393
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
394
|
+
# String-like = String or Slice (both implement to_str). When
|
|
395
|
+
# only ONE side is string-like, the OTHER side wins — the
|
|
396
|
+
# original duck-typed priority, preserved exactly.
|
|
397
|
+
left_stringlike = left.is_a?(String) || left.is_a?(Parsanol::Slice)
|
|
398
|
+
right_stringlike = right.is_a?(String) || right.is_a?(Parsanol::Slice)
|
|
399
|
+
if left_stringlike && right_stringlike
|
|
400
|
+
return right if right.is_a?(Parsanol::Slice)
|
|
401
|
+
return left if left.is_a?(Parsanol::Slice)
|
|
402
|
+
|
|
403
|
+
return left + right
|
|
399
404
|
end
|
|
400
|
-
|
|
401
|
-
return
|
|
402
|
-
return right if left.respond_to?(:to_str)
|
|
405
|
+
return left if right_stringlike
|
|
406
|
+
return right if left_stringlike
|
|
403
407
|
|
|
404
408
|
return left + [right] if right.is_a?(Hash)
|
|
405
409
|
return [left] + right if left.is_a?(Hash)
|
data/lib/parsanol/native.rb
CHANGED
|
@@ -216,7 +216,9 @@ module Parsanol
|
|
|
216
216
|
# cause from that directly — the failure path never needs a
|
|
217
217
|
# reporter reparse. The single interpreter pass below stays only
|
|
218
218
|
# to recover inputs from grammars native cannot express.
|
|
219
|
-
|
|
219
|
+
parsanol_grammar =
|
|
220
|
+
grammar.is_a?(Parsanol::Atoms::Base) || grammar.is_a?(Parsanol::Parser)
|
|
221
|
+
if parsanol_grammar && (m = error.message.match(NATIVE_POS_MARKER))
|
|
220
222
|
source = Parsanol::Source.new(input)
|
|
221
223
|
success, value = grammar.run_with_context(source, nil, true)
|
|
222
224
|
return grammar.finalize_result(value) if success
|
|
@@ -227,7 +229,7 @@ module Parsanol
|
|
|
227
229
|
raise Parsanol::ParseFailed.new(cause.to_s, cause)
|
|
228
230
|
end
|
|
229
231
|
|
|
230
|
-
if
|
|
232
|
+
if parsanol_grammar
|
|
231
233
|
# No native diagnostics (e.g. incomplete-input errors): one
|
|
232
234
|
# interpreter pass with the reporter attached — a success
|
|
233
235
|
# recovers the tree, a failure raises the cause tree.
|
data/lib/parsanol/parser.rb
CHANGED
|
@@ -255,7 +255,7 @@ module Parsanol
|
|
|
255
255
|
# already produced. The full per-atom event stream of the Ruby
|
|
256
256
|
# engine remains available via an explicit `mode: :ruby`.
|
|
257
257
|
def feed_native_reporter(reporter, input, error)
|
|
258
|
-
return unless reporter.
|
|
258
|
+
return unless reporter.is_a?(Parsanol::ErrorReporter::Base)
|
|
259
259
|
|
|
260
260
|
source = Parsanol::Source.new(input)
|
|
261
261
|
cause = error.parse_failure_cause
|
data/lib/parsanol/pattern.rb
CHANGED
|
@@ -90,7 +90,7 @@ module Parsanol
|
|
|
90
90
|
return true if pattern_val === target
|
|
91
91
|
|
|
92
92
|
# Check if pattern is a binding expression (like simple(:x))
|
|
93
|
-
if pattern_val.
|
|
93
|
+
if pattern_val.is_a?(Parsanol::Pattern::SubtreeBind) && pattern_val.can_bind?(target)
|
|
94
94
|
return capture_binding(target, pattern_val, captured)
|
|
95
95
|
end
|
|
96
96
|
|
data/lib/parsanol/pool.rb
CHANGED
data/lib/parsanol/rope.rb
CHANGED
data/lib/parsanol/slice.rb
CHANGED
|
@@ -142,18 +142,16 @@ module Parsanol
|
|
|
142
142
|
|
|
143
143
|
# Unified line/column computation:
|
|
144
144
|
# - String input: compute from input string
|
|
145
|
-
# - LineCache: delegate
|
|
145
|
+
# - Source / LineCache: delegate
|
|
146
146
|
def line_and_column_at(pos)
|
|
147
|
-
if @input.
|
|
148
|
-
# LineCache or duck-typed object
|
|
149
|
-
@input.line_and_column(pos)
|
|
150
|
-
else
|
|
151
|
-
# String input
|
|
147
|
+
if @input.is_a?(String)
|
|
152
148
|
prefix = @input.byteslice(0, pos) || ""
|
|
153
149
|
line = 1 + prefix.count("\n")
|
|
154
150
|
last_nl = prefix.rindex("\n")
|
|
155
151
|
column = last_nl ? pos - last_nl : pos + 1
|
|
156
152
|
[line, column]
|
|
153
|
+
else
|
|
154
|
+
@input.line_and_column(pos)
|
|
157
155
|
end
|
|
158
156
|
end
|
|
159
157
|
end
|
data/lib/parsanol/source.rb
CHANGED
|
@@ -33,9 +33,9 @@ module Parsanol
|
|
|
33
33
|
# @raise [ArgumentError] if input doesn't respond to to_str
|
|
34
34
|
#
|
|
35
35
|
def initialize(input)
|
|
36
|
-
unless input.
|
|
36
|
+
unless input.is_a?(String)
|
|
37
37
|
raise ArgumentError,
|
|
38
|
-
"Source requires a
|
|
38
|
+
"Source requires a String (got #{input.class})"
|
|
39
39
|
end
|
|
40
40
|
|
|
41
41
|
@raw_string = input.to_str
|
data/lib/parsanol/version.rb
CHANGED
data/lib/parsanol/vm.rb
CHANGED
|
@@ -149,6 +149,7 @@ module Parsanol
|
|
|
149
149
|
def compile_with_inline(atom, inline)
|
|
150
150
|
root = atom.is_a?(Parsanol::Parser) ? atom.root : atom
|
|
151
151
|
compiler = Compiler.new(inline: inline)
|
|
152
|
+
validate_alternatives(root)
|
|
152
153
|
return nil unless compiler.compile_atom(root, true)
|
|
153
154
|
|
|
154
155
|
# HALT terminates the MAIN program; subroutines follow it so the
|
|
@@ -156,9 +157,99 @@ module Parsanol
|
|
|
156
157
|
compiler.ops << [HALT, nil, nil, nil]
|
|
157
158
|
return nil unless compiler.append_subroutines
|
|
158
159
|
|
|
160
|
+
# Terminal failures jump to pc = FAIL without testing the pc
|
|
161
|
+
# register on the hot path; the dispatch case instead lands on
|
|
162
|
+
# this dedicated instruction (the last slot in the flat program),
|
|
163
|
+
# whose opcode is FAIL itself.
|
|
164
|
+
compiler.ops << [FAIL, nil, nil, nil]
|
|
165
|
+
|
|
166
|
+
# Structure-seeded memoization: a grammar containing repeat/maybe
|
|
167
|
+
# starts memoized on its very first parse (fresh parser instances
|
|
168
|
+
# included — the seed is recomputed at every compile), so the
|
|
169
|
+
# cold-start never pays the doomed unmemoized exploration.
|
|
170
|
+
if compiler.backtracking_prone
|
|
171
|
+
# rubocop:disable-next Lint/HashCompareByIdentity -- object_id keys match run_for's heavy flag; would otherwise pin every grammar alive
|
|
172
|
+
(@heavy ||= {})[root.object_id] = true
|
|
173
|
+
end
|
|
174
|
+
|
|
159
175
|
compiler.to_program
|
|
160
176
|
end
|
|
161
177
|
|
|
178
|
+
def validate_choice_branches(atom)
|
|
179
|
+
atom.alternatives.each_with_index do |branch, idx|
|
|
180
|
+
walk_branch = branch
|
|
181
|
+
next if idx == atom.alternatives.size - 1
|
|
182
|
+
|
|
183
|
+
# Entities resolve lazily and may be recursive; resolve
|
|
184
|
+
# through the wrapper for the nullability check only.
|
|
185
|
+
while walk_branch.is_a?(Parsanol::Atoms::Entity) ||
|
|
186
|
+
walk_branch.is_a?(Parsanol::Atoms::Named)
|
|
187
|
+
walk_branch = begin
|
|
188
|
+
walk_branch.parslet
|
|
189
|
+
rescue StandardError
|
|
190
|
+
nil
|
|
191
|
+
end
|
|
192
|
+
break if walk_branch.nil?
|
|
193
|
+
end
|
|
194
|
+
next if walk_branch.nil?
|
|
195
|
+
next unless Compiler.new.nullable?(walk_branch)
|
|
196
|
+
|
|
197
|
+
raise Parsanol::GrammarError,
|
|
198
|
+
"wrong grammar: alternative branch #{idx} " \
|
|
199
|
+
"(#{branch.inspect}) can match empty input and is not " \
|
|
200
|
+
"the last branch — ordered choice commits to its empty " \
|
|
201
|
+
"match, so #{atom.alternatives.size - idx - 1} later " \
|
|
202
|
+
"branch(es) can never match at positions where it " \
|
|
203
|
+
"matches empty. Reorder branches so empty-matchable " \
|
|
204
|
+
"ones come last, or make the branch non-empty."
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Grammar validation, run once per compile: an ordered-choice
|
|
209
|
+
# branch that can match empty input while not being last commits
|
|
210
|
+
# to its empty match and permanently shadows every later branch —
|
|
211
|
+
# the grammar is wrong, and inputs it would mis-parse fail with a
|
|
212
|
+
# baffling cause. Raise before any input is seen.
|
|
213
|
+
def validate_alternatives(root)
|
|
214
|
+
visited = {}.compare_by_identity
|
|
215
|
+
walk = lambda do |atom, depth|
|
|
216
|
+
next if atom.nil? || depth > 20
|
|
217
|
+
next if visited[atom]
|
|
218
|
+
|
|
219
|
+
visited[atom] = true
|
|
220
|
+
case atom
|
|
221
|
+
when Parsanol::Atoms::Alternative
|
|
222
|
+
validate_choice_branches(atom)
|
|
223
|
+
atom.alternatives.each { |c| walk.call(c, depth + 1) }
|
|
224
|
+
when Parsanol::Atoms::Sequence
|
|
225
|
+
atom.parslets.each { |c| walk.call(c, depth + 1) }
|
|
226
|
+
when Parsanol::Atoms::Repetition, Parsanol::Atoms::Named
|
|
227
|
+
walk.call(atom.parslet, depth + 1)
|
|
228
|
+
when Parsanol::Atoms::Lookahead
|
|
229
|
+
walk.call(atom.bound_parslet, depth + 1)
|
|
230
|
+
when Parsanol::Atoms::Ignored
|
|
231
|
+
walk.call(atom.wrapped_atom, depth + 1)
|
|
232
|
+
when Parsanol::Atoms::Entity
|
|
233
|
+
begin
|
|
234
|
+
walk.call(atom.parslet, depth + 1)
|
|
235
|
+
rescue Parsanol::GrammarError
|
|
236
|
+
raise
|
|
237
|
+
rescue StandardError
|
|
238
|
+
# Lazily resolved / recursive entity that cannot resolve
|
|
239
|
+
# outside a parse: skip this branch of the walk.
|
|
240
|
+
nil
|
|
241
|
+
end
|
|
242
|
+
when Parsanol::Atoms::Scope
|
|
243
|
+
begin
|
|
244
|
+
walk.call(atom.block.call, depth + 1)
|
|
245
|
+
rescue StandardError
|
|
246
|
+
nil
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
walk.call(root, 0)
|
|
251
|
+
end
|
|
252
|
+
|
|
162
253
|
# Executes a compiled program. Returns [true, value] on success;
|
|
163
254
|
# [false, nil] on failure or budget exhaustion — callers fall back
|
|
164
255
|
# to the interpreter, which reproduces exact diagnostics.
|
|
@@ -248,9 +339,10 @@ module Parsanol
|
|
|
248
339
|
@inline = inline
|
|
249
340
|
@subs = {} # [obj_id, flag] => pc or :pending
|
|
250
341
|
@pending = {} # [obj_id, flag] => [[call_idx, body], ...]
|
|
342
|
+
@backtracking_prone = false
|
|
251
343
|
end
|
|
252
344
|
|
|
253
|
-
attr_reader :ops
|
|
345
|
+
attr_reader :ops, :backtracking_prone
|
|
254
346
|
|
|
255
347
|
def to_program
|
|
256
348
|
return :oversize if @ops.size > MAX_PROGRAM
|
|
@@ -608,6 +700,11 @@ module Parsanol
|
|
|
608
700
|
# * unbounded-plus of a single terminal (non-spine sites) -> greedy
|
|
609
701
|
# RUN_* op
|
|
610
702
|
def compile_repetition(atom, consume_all)
|
|
703
|
+
# Any repetition (repeat, maybe, repeat(0)) can re-explore the
|
|
704
|
+
# same rule+position pairs when a later sibling fails; grammars
|
|
705
|
+
# containing one memoize from the first parse instead of waiting
|
|
706
|
+
# for the naive pass to prove the need.
|
|
707
|
+
@backtracking_prone = true
|
|
611
708
|
min = atom.min
|
|
612
709
|
max = atom.max
|
|
613
710
|
return nil if max&.zero?
|
|
@@ -737,6 +834,10 @@ module Parsanol
|
|
|
737
834
|
def execute # rubocop:disable Metrics/MethodLength, Metrics/BlockLength, Metrics/BlockNesting -- single dispatch loop; hot path
|
|
738
835
|
ops = @ops
|
|
739
836
|
input = @input
|
|
837
|
+
# Dedicated FAIL landing instruction appended after HALT and the
|
|
838
|
+
# subroutines: `pc = fail_pc` dispatches straight into when-FAIL
|
|
839
|
+
# on the next cycle, no sentinel comparison per step.
|
|
840
|
+
fail_pc = ops.size - 4
|
|
740
841
|
bytes = input.unpack("C*")
|
|
741
842
|
# Regexp#match?(str, pos) searches FORWARD from pos (unanchored);
|
|
742
843
|
# StringScanner#match? is position-anchored, matching the
|
|
@@ -800,7 +901,7 @@ module Parsanol
|
|
|
800
901
|
pos += ops[pc + 3]
|
|
801
902
|
pc += 4
|
|
802
903
|
else
|
|
803
|
-
pc =
|
|
904
|
+
pc = fail_pc
|
|
804
905
|
end
|
|
805
906
|
when RE
|
|
806
907
|
b = bytes[pos]
|
|
@@ -819,7 +920,7 @@ module Parsanol
|
|
|
819
920
|
pos += w
|
|
820
921
|
pc += 4
|
|
821
922
|
else
|
|
822
|
-
pc =
|
|
923
|
+
pc = fail_pc
|
|
823
924
|
end
|
|
824
925
|
when ANY
|
|
825
926
|
if pos < n
|
|
@@ -829,7 +930,7 @@ module Parsanol
|
|
|
829
930
|
pos += w
|
|
830
931
|
pc += 4
|
|
831
932
|
else
|
|
832
|
-
pc =
|
|
933
|
+
pc = fail_pc
|
|
833
934
|
end
|
|
834
935
|
when SEQ_BEGIN
|
|
835
936
|
rstack << SEQ_MARK
|
|
@@ -878,7 +979,7 @@ module Parsanol
|
|
|
878
979
|
# PENDING — re-entry at the same rule+position (left
|
|
879
980
|
# recursion); the interpreter loops forever here, so
|
|
880
981
|
# failing this path keeps the VM bounded.
|
|
881
|
-
pc =
|
|
982
|
+
pc = fail_pc
|
|
882
983
|
next
|
|
883
984
|
end
|
|
884
985
|
# Memo hit: replay the subroutine's stack effect without
|
|
@@ -926,7 +1027,7 @@ module Parsanol
|
|
|
926
1027
|
rlen = bt[-BT_STRIDE + 2]
|
|
927
1028
|
rstack.slice!(rlen..) if rstack.size > rlen
|
|
928
1029
|
bt.pop(BT_STRIDE)
|
|
929
|
-
pc =
|
|
1030
|
+
pc = fail_pc
|
|
930
1031
|
when DROP
|
|
931
1032
|
rstack.pop
|
|
932
1033
|
rstack << nil
|
|
@@ -952,12 +1053,24 @@ module Parsanol
|
|
|
952
1053
|
count = frames[cbase]
|
|
953
1054
|
return BAIL if count.nil?
|
|
954
1055
|
|
|
1056
|
+
old_end = frames[cbase + 1]
|
|
955
1057
|
count += 1
|
|
956
1058
|
frames[cbase] = count
|
|
957
1059
|
frames[cbase + 1] = pos
|
|
958
1060
|
max = ops[pc + 3]
|
|
959
1061
|
pc = if max && count >= max
|
|
960
1062
|
ops[pc + 2] # exit
|
|
1063
|
+
elsif pos == old_end
|
|
1064
|
+
# Zero-width body match: count it, stop iterating.
|
|
1065
|
+
# Mirrors Repetition#try_general: success with the
|
|
1066
|
+
# prefix when min is met, failure of the repetition
|
|
1067
|
+
# otherwise.
|
|
1068
|
+
if count >= frames[cbase + 3]
|
|
1069
|
+
ops[pc + 2] # exit
|
|
1070
|
+
else
|
|
1071
|
+
frames.slice!(cbase..)
|
|
1072
|
+
fail_pc
|
|
1073
|
+
end
|
|
961
1074
|
else
|
|
962
1075
|
ops[pc + 1] # test
|
|
963
1076
|
end
|
|
@@ -967,7 +1080,7 @@ module Parsanol
|
|
|
967
1080
|
|
|
968
1081
|
if ops[pc + 2] == 1 && pos != n
|
|
969
1082
|
frames.slice!(cbase..)
|
|
970
|
-
pc =
|
|
1083
|
+
pc = fail_pc
|
|
971
1084
|
else
|
|
972
1085
|
rbase = frames[cbase + 2]
|
|
973
1086
|
values = rstack.pop(rstack.size - rbase)
|
|
@@ -980,7 +1093,7 @@ module Parsanol
|
|
|
980
1093
|
table = ops[pc + 1]
|
|
981
1094
|
b = pos < n ? bytes[pos] : -1
|
|
982
1095
|
target = b == -1 ? -1 : table[b]
|
|
983
|
-
pc = target >= 0 ? target :
|
|
1096
|
+
pc = target >= 0 ? target : fail_pc
|
|
984
1097
|
when SEQ_SIMPLE
|
|
985
1098
|
kids = ops[pc + 1]
|
|
986
1099
|
kn = kids.size
|
|
@@ -1115,7 +1228,7 @@ module Parsanol
|
|
|
1115
1228
|
ki += 4
|
|
1116
1229
|
end
|
|
1117
1230
|
if failed
|
|
1118
|
-
pc =
|
|
1231
|
+
pc = fail_pc
|
|
1119
1232
|
else
|
|
1120
1233
|
rstack << values
|
|
1121
1234
|
pc += 4
|
|
@@ -1180,7 +1293,7 @@ module Parsanol
|
|
|
1180
1293
|
pos += w
|
|
1181
1294
|
end
|
|
1182
1295
|
if min && values.size - 1 < min
|
|
1183
|
-
pc =
|
|
1296
|
+
pc = fail_pc
|
|
1184
1297
|
else
|
|
1185
1298
|
rstack << values
|
|
1186
1299
|
pc += 4
|
|
@@ -1206,14 +1319,14 @@ module Parsanol
|
|
|
1206
1319
|
pos += ln
|
|
1207
1320
|
end
|
|
1208
1321
|
if min && values.size - 1 < min
|
|
1209
|
-
pc =
|
|
1322
|
+
pc = fail_pc
|
|
1210
1323
|
else
|
|
1211
1324
|
rstack << values
|
|
1212
1325
|
pc += 4
|
|
1213
1326
|
end
|
|
1214
1327
|
when HALT
|
|
1215
1328
|
if @consume_all && pos != n
|
|
1216
|
-
pc =
|
|
1329
|
+
pc = fail_pc
|
|
1217
1330
|
next
|
|
1218
1331
|
end
|
|
1219
1332
|
value = VM.materialize(rstack[0], input)
|
data/lib/parsanol.rb
CHANGED
|
@@ -44,6 +44,12 @@ module Parsanol
|
|
|
44
44
|
base.extend(ClassMethods)
|
|
45
45
|
end
|
|
46
46
|
|
|
47
|
+
# Raised when the GRAMMAR itself is malformed — detected before any
|
|
48
|
+
# input is parsed. The canonical case: an ordered-choice branch that
|
|
49
|
+
# can match empty input while not being last, which commits to its
|
|
50
|
+
# empty match and permanently shadows every later branch.
|
|
51
|
+
class GrammarError < StandardError; end
|
|
52
|
+
|
|
47
53
|
# Exception raised when parsing fails. Contains detailed error information
|
|
48
54
|
# in the #parse_failure_cause attribute.
|
|
49
55
|
class ParseFailed < StandardError
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: parsanol
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.3.
|
|
4
|
+
version: 1.3.56
|
|
5
5
|
platform: aarch64-linux
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-09-
|
|
11
|
+
date: 2026-09-24 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description: A small Ruby library for constructing parsers in the PEG (Parsing Expression
|
|
14
14
|
Grammar) fashion. Parsanol provides Parslet-compatible API with additional features
|