parsanol 1.3.105 → 1.3.106
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/Cargo.lock +4 -4
- data/lib/parsanol/parg/artifact.rb +63 -25
- data/lib/parsanol/parg/compiler.rb +27 -1
- data/lib/parsanol/parg/document.rb +7 -0
- data/lib/parsanol/parg/parser.rb +40 -0
- data/lib/parsanol/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cb8829be859854aad36e7ed1a0b43675af3295f714fb86ef9ee0af9cabf491ec
|
|
4
|
+
data.tar.gz: 73137433d9c3676ca771f696cccda01d8f87770a638fcc4eb0ea92332ff974f9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 28d0a98e32e8e46bcc645a2e8f13782518470192ce96c27a733edcca920025c6546fc1f2041acb4b069fd1ced2430fe8ef3b186ade399c9df68b0cfaac345018
|
|
7
|
+
data.tar.gz: 192af56c8f394b99de8fcc0b0ba4683e807bc19f4448900df53b38afe51821f57f2c17dc76f1884f6d12d201c3edb72bc2e745ccce9ac84a1f7eac7ec2825faa
|
data/Cargo.lock
CHANGED
|
@@ -307,9 +307,9 @@ checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
|
|
307
307
|
|
|
308
308
|
[[package]]
|
|
309
309
|
name = "parsanol"
|
|
310
|
-
version = "0.18.
|
|
310
|
+
version = "0.18.17"
|
|
311
311
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
312
|
-
checksum = "
|
|
312
|
+
checksum = "6dcea9d4de103b4ecba2f58de7634ec009f809c93150f658f056b7ad65ee3d97"
|
|
313
313
|
dependencies = [
|
|
314
314
|
"ahash",
|
|
315
315
|
"getrandom 0.3.4",
|
|
@@ -326,9 +326,9 @@ dependencies = [
|
|
|
326
326
|
|
|
327
327
|
[[package]]
|
|
328
328
|
name = "parsanol-derive"
|
|
329
|
-
version = "0.18.
|
|
329
|
+
version = "0.18.17"
|
|
330
330
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
331
|
-
checksum = "
|
|
331
|
+
checksum = "6e222a36678bd337b7d8484c81e731796cc5b2bb6c3fd654d29563380d4ba883"
|
|
332
332
|
dependencies = [
|
|
333
333
|
"proc-macro2",
|
|
334
334
|
"quote",
|
|
@@ -250,44 +250,82 @@ module Parsanol
|
|
|
250
250
|
return [[], []] unless body_rule
|
|
251
251
|
|
|
252
252
|
body_atom = compiler.atom_for(body_rule)
|
|
253
|
+
# R2 declared form: the baked resync pattern owns the skip
|
|
254
|
+
# spans (author-deterministic — no trial scanning). Falls back
|
|
255
|
+
# to the inferred trial scan when the rule declares none.
|
|
256
|
+
pattern_atom = compiler.resync_atom_for(body_rule) ||
|
|
257
|
+
compiler.resync_atom_for(entry(entry_name)["root"])
|
|
253
258
|
max = input.length
|
|
254
259
|
resyncs = []
|
|
255
260
|
items = []
|
|
256
261
|
rounds = 0
|
|
257
262
|
while rounds < 8
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
break
|
|
263
|
+
attempt = try_iteration(body_atom, input, cursor)
|
|
264
|
+
if attempt
|
|
265
|
+
items << root_atom(entry_name).flatten(attempt[0])
|
|
266
|
+
cursor = attempt[1]
|
|
267
|
+
until cursor >= max
|
|
268
|
+
attempt = try_iteration(body_atom, input, cursor)
|
|
269
|
+
break unless attempt
|
|
270
|
+
|
|
271
|
+
items << root_atom(entry_name).flatten(attempt[0])
|
|
272
|
+
cursor = attempt[1]
|
|
265
273
|
end
|
|
266
|
-
|
|
274
|
+
break if cursor >= max
|
|
275
|
+
end
|
|
276
|
+
if pattern_atom
|
|
277
|
+
# Declared: the pattern match (gap + delimiter) is the
|
|
278
|
+
# annotated error span.
|
|
279
|
+
span = scan_pattern(pattern_atom, input, cursor)
|
|
280
|
+
break unless span
|
|
281
|
+
|
|
282
|
+
resume = span[1]
|
|
283
|
+
resyncs << {
|
|
284
|
+
"offset" => cursor,
|
|
285
|
+
"skipped" => input[cursor...resume],
|
|
286
|
+
"resumed_at" => resume,
|
|
287
|
+
}
|
|
288
|
+
else
|
|
289
|
+
# Inferred: scan for the next viable iteration start.
|
|
290
|
+
found = nil
|
|
291
|
+
scan = cursor
|
|
292
|
+
while scan < max
|
|
293
|
+
attempt = try_iteration(body_atom, input, scan)
|
|
294
|
+
if attempt && attempt[1] > scan
|
|
295
|
+
found = [scan, attempt[1], attempt[0]]
|
|
296
|
+
break
|
|
297
|
+
end
|
|
298
|
+
scan += 1
|
|
299
|
+
end
|
|
300
|
+
break unless found
|
|
301
|
+
|
|
302
|
+
skip_start, resume, value = found
|
|
303
|
+
resyncs << {
|
|
304
|
+
"offset" => cursor,
|
|
305
|
+
"skipped" => input[cursor...skip_start],
|
|
306
|
+
"resumed_at" => skip_start,
|
|
307
|
+
}
|
|
308
|
+
items << root_atom(entry_name).flatten(value)
|
|
267
309
|
end
|
|
268
|
-
break unless found
|
|
269
|
-
|
|
270
|
-
skip_start, resume, value = found
|
|
271
|
-
resyncs << {
|
|
272
|
-
"offset" => cursor,
|
|
273
|
-
"skipped" => input[cursor...skip_start],
|
|
274
|
-
"resumed_at" => skip_start,
|
|
275
|
-
}
|
|
276
|
-
items << root_atom(entry_name).flatten(value)
|
|
277
310
|
cursor = resume
|
|
278
311
|
rounds += 1
|
|
279
|
-
until cursor >= max
|
|
280
|
-
attempt = try_iteration(body_atom, input, cursor)
|
|
281
|
-
break unless attempt && attempt[1] > cursor
|
|
282
|
-
|
|
283
|
-
items << root_atom(entry_name).flatten(attempt[0])
|
|
284
|
-
cursor = attempt[1]
|
|
285
|
-
end
|
|
286
|
-
break if cursor >= max
|
|
287
312
|
end
|
|
288
313
|
[items, resyncs]
|
|
289
314
|
end
|
|
290
315
|
|
|
316
|
+
# Scan forward from `from` for the first position where the
|
|
317
|
+
# declared pattern matches with progress: [match_start, match_end].
|
|
318
|
+
def scan_pattern(pattern_atom, input, from)
|
|
319
|
+
scan = from
|
|
320
|
+
while scan < input.length
|
|
321
|
+
attempt = try_iteration(pattern_atom, input, scan)
|
|
322
|
+
return [scan, attempt[1]] if attempt
|
|
323
|
+
|
|
324
|
+
scan += 1
|
|
325
|
+
end
|
|
326
|
+
nil
|
|
327
|
+
end
|
|
328
|
+
|
|
291
329
|
# Apply the body atom at one position: [value, end_pos] on a
|
|
292
330
|
# progressing match, nil otherwise.
|
|
293
331
|
def try_iteration(atom, input, pos)
|
|
@@ -130,6 +130,17 @@ module Parsanol
|
|
|
130
130
|
"(bind the class before compiling, or fix the custom declaration)"
|
|
131
131
|
end
|
|
132
132
|
|
|
133
|
+
# The declared resync skip pattern for a rule, built as a raw
|
|
134
|
+
# atom (no trivia injection). Nil when the rule declares none.
|
|
135
|
+
def resync_atom_for(rule)
|
|
136
|
+
pattern = @document.resyncs[rule.to_sym] || @document.resyncs[rule.to_s]
|
|
137
|
+
return nil unless pattern
|
|
138
|
+
|
|
139
|
+
expand_left_recursion! unless @expanded
|
|
140
|
+
@resync_cache ||= {}
|
|
141
|
+
@resync_cache[rule.to_sym] ||= build_atom(pattern)
|
|
142
|
+
end
|
|
143
|
+
|
|
133
144
|
def atom_for(name)
|
|
134
145
|
expand_left_recursion! unless @expanded
|
|
135
146
|
@atom_cache[name] ||= begin
|
|
@@ -925,6 +936,12 @@ module Parsanol
|
|
|
925
936
|
end
|
|
926
937
|
|
|
927
938
|
def build_envelope
|
|
939
|
+
@document.resyncs.each_key do |target|
|
|
940
|
+
unless @document.rules.key?(target.to_sym) ||
|
|
941
|
+
@document.rules.key?(target.to_s)
|
|
942
|
+
raise CompileError, "resync declared for unknown rule #{target.inspect}"
|
|
943
|
+
end
|
|
944
|
+
end
|
|
928
945
|
entries = @document.entries.to_h do |name, rule|
|
|
929
946
|
[name, entry_envelope(rule)]
|
|
930
947
|
end
|
|
@@ -980,11 +997,20 @@ module Parsanol
|
|
|
980
997
|
end
|
|
981
998
|
|
|
982
999
|
def entry_envelope(rule)
|
|
983
|
-
{
|
|
1000
|
+
envelope = {
|
|
984
1001
|
"root" => rule,
|
|
985
1002
|
"grammar" => JSON.parse(portable_json(rule)),
|
|
986
1003
|
"bindings" => binding_list(rule),
|
|
987
1004
|
}
|
|
1005
|
+
# R2 declared resync: the skip pattern bakes as its own
|
|
1006
|
+
# mini-grammar (same portable atoms shape) so every engine
|
|
1007
|
+
# matches identical error spans. Raw match — no trivia
|
|
1008
|
+
# injection: the pattern delimits junk, it is not syntax.
|
|
1009
|
+
if (pattern = @document.resyncs[rule.to_sym] || @document.resyncs[rule.to_s])
|
|
1010
|
+
envelope["resync"] =
|
|
1011
|
+
JSON.parse(Parsanol::Native::Parser.serialize_grammar(build_atom(pattern)))
|
|
1012
|
+
end
|
|
1013
|
+
envelope
|
|
988
1014
|
end
|
|
989
1015
|
|
|
990
1016
|
def portable_json(rule)
|
|
@@ -19,6 +19,9 @@ module Parsanol
|
|
|
19
19
|
|
|
20
20
|
attr_accessor :grammar_name, :version, :source, :uses, :skip_declaration, :skip_source,
|
|
21
21
|
:atomic_rules, :skip_capture_key
|
|
22
|
+
# R2 declared resync: rule name -> skip pattern (the span matched
|
|
23
|
+
# at a recovery point is consumed as annotated error text).
|
|
24
|
+
attr_reader :resyncs
|
|
22
25
|
attr_reader :rules, :bindings, :preprocess, :entries, :docs, :tests, :render, :derive,
|
|
23
26
|
:customs, :states
|
|
24
27
|
# Entries authored in this file (pre-import); nil-entry tests resolve
|
|
@@ -45,6 +48,10 @@ module Parsanol
|
|
|
45
48
|
# suppressed inside their bodies (token-shaped rules match
|
|
46
49
|
# contiguously).
|
|
47
50
|
@atomic_rules = []
|
|
51
|
+
# R2 declared resync: rule -> skip pattern. Recovery consumes
|
|
52
|
+
# each pattern match at a failure point as annotated error text
|
|
53
|
+
# and resumes the iteration after it.
|
|
54
|
+
@resyncs = {}
|
|
48
55
|
end
|
|
49
56
|
|
|
50
57
|
def validate!
|
data/lib/parsanol/parg/parser.rb
CHANGED
|
@@ -257,6 +257,22 @@ module Parsanol
|
|
|
257
257
|
skip_newlines
|
|
258
258
|
next
|
|
259
259
|
end
|
|
260
|
+
if peek&.type == :ident && peek.value == "resync"
|
|
261
|
+
advance
|
|
262
|
+
target = rule_name
|
|
263
|
+
punct("=")
|
|
264
|
+
node = parse_choice
|
|
265
|
+
if document.resyncs.key?(target.to_sym)
|
|
266
|
+
raise ParseError, "duplicate resync declaration for #{target.inspect}"
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
# The target rule may be declared later in the block
|
|
270
|
+
# (forward reference, like entry roots); the compiler
|
|
271
|
+
# rejects unknown targets at compile time.
|
|
272
|
+
document.resyncs[target.to_sym] = validate_resync_node(node)
|
|
273
|
+
skip_newlines
|
|
274
|
+
next
|
|
275
|
+
end
|
|
260
276
|
if peek&.type == :ident && STATE_KEYWORDS.include?(peek.value)
|
|
261
277
|
keyword = advance.value
|
|
262
278
|
slot = ident.value
|
|
@@ -300,6 +316,30 @@ module Parsanol
|
|
|
300
316
|
end
|
|
301
317
|
end
|
|
302
318
|
|
|
319
|
+
# A resync pattern may be any expression EXCEPT captures and
|
|
320
|
+
# state operations: the span it matches is reported as skipped
|
|
321
|
+
# error text, so bindings and slots have no meaning inside it,
|
|
322
|
+
# and a capture would silently drop its data.
|
|
323
|
+
def validate_resync_node(node)
|
|
324
|
+
case node.kind
|
|
325
|
+
when :cap
|
|
326
|
+
raise ParseError,
|
|
327
|
+
"resync pattern may not contain captures (the matched " \
|
|
328
|
+
"span is annotated error text), got :cap"
|
|
329
|
+
when :seq, :alt
|
|
330
|
+
node.a.each { |child| validate_resync_node(child) }
|
|
331
|
+
node
|
|
332
|
+
when :rep, :opt
|
|
333
|
+
validate_resync_node(node.a)
|
|
334
|
+
node
|
|
335
|
+
when :pred
|
|
336
|
+
validate_resync_node(node.b)
|
|
337
|
+
node
|
|
338
|
+
else
|
|
339
|
+
node
|
|
340
|
+
end
|
|
341
|
+
end
|
|
342
|
+
|
|
303
343
|
def validate_skip_node(node)
|
|
304
344
|
case node.kind
|
|
305
345
|
when :ref
|
data/lib/parsanol/version.rb
CHANGED