parsanol 1.3.105-arm-linux → 1.3.107-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 +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/native/libparsanol.so +0 -0
- data/lib/parsanol/native.rb +44 -0
- data/lib/parsanol/parg/artifact.rb +75 -26
- 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: 82264b2e58bdcd642b09e1b7d39e5848685758e4be7b5d04c3d1ccbf8891e1f0
|
|
4
|
+
data.tar.gz: 4abb4bd1c4195f5efb724a23f14be6e24451c284f25748eec7e914129280d4a8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 31541ed28f1e205cb5abcbb0d90f59dbd18ebc9734026f458a2458d8c1cdf97b6a17686c7dc1c9bf6f3fccc5342dde8d078561d403f32dadae225b11edef0171
|
|
7
|
+
data.tar.gz: 499beed8e7e159aaf36223cc318f4fea3f40f69456b3fb48c5ce1278bd592cf323470905d165dfb22777ca5e607cb96a17de16c039b56395185a42dfac8a1a08
|
|
@@ -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: 34490424 bytes
|
|
4
|
+
sha256: a3cdb4f287af7a777278dc7ef4db062d9c3aa6b30e6667ffb26542d25215dec2
|
|
@@ -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: 34675616 bytes
|
|
4
|
+
sha256: 8ccc77dbd45591ffcd1cceabed8a2324593f3cdfbdd3acc463fbec81702381a8
|
|
@@ -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: 34669796 bytes
|
|
4
|
+
sha256: 5b8b78e7c01dca71d2abe00446475964e6bf894dacc0aadb427910ab80eebf38
|
|
@@ -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: 34679512 bytes
|
|
4
|
+
sha256: 05dea26c9937b15e9f7f9ad8800d4cb29fc365a056640cecea6ac992d03092a3
|
|
Binary file
|
data/lib/parsanol/native.rb
CHANGED
|
@@ -29,6 +29,16 @@ module Parsanol
|
|
|
29
29
|
Parser.available?
|
|
30
30
|
end
|
|
31
31
|
|
|
32
|
+
# Whether the reference walker is available (TODO.sota/8 B1): the
|
|
33
|
+
# `_parse_handle_reference` export ships with parsanol >= 0.18.18.
|
|
34
|
+
# Probed rather than assumed so a checkout whose extension predates
|
|
35
|
+
# the export can report the missing fifth fuzz path instead of
|
|
36
|
+
# raising NoMethodError mid-run.
|
|
37
|
+
def reference_available?
|
|
38
|
+
available? && Parser.extension_loaded? &&
|
|
39
|
+
respond_to?(:_parse_handle_reference)
|
|
40
|
+
end
|
|
41
|
+
|
|
32
42
|
# Parse input with a Ruby grammar, returning clean AST with lazy line/column.
|
|
33
43
|
#
|
|
34
44
|
# Uses batch FFI format for maximum performance (3-5x faster than object-by-object).
|
|
@@ -355,6 +365,40 @@ module Parsanol
|
|
|
355
365
|
raise_native_parse_error(e, grammar, input)
|
|
356
366
|
end
|
|
357
367
|
end
|
|
368
|
+
|
|
369
|
+
# Reference-walker path (TODO.sota/8 B1): the plain recursive
|
|
370
|
+
# relation — no VM, no memo tables, no fused scans, no terminal
|
|
371
|
+
# fast paths. Deliberately slow; the cross-engine differential
|
|
372
|
+
# fuzz runs it as an independent path, so a native failure here is
|
|
373
|
+
# a real disagreement and must not be rescued into a different
|
|
374
|
+
# engine's answer.
|
|
375
|
+
def parse_reference(grammar, input)
|
|
376
|
+
unless reference_available?
|
|
377
|
+
raise NotImplementedError,
|
|
378
|
+
"reference mode requires the native extension with " \
|
|
379
|
+
"parsanol >= 0.18.18"
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
handle = Parser.grammar_handle(grammar)
|
|
383
|
+
|
|
384
|
+
begin
|
|
385
|
+
BatchDecoder.decode_and_flatten(_parse_handle_reference(handle, input),
|
|
386
|
+
input, Parsanol::Slice)
|
|
387
|
+
rescue ArgumentError
|
|
388
|
+
# Handle dropped Rust-side (e.g. cache cleared): re-register once.
|
|
389
|
+
Parser.invalidate_handle(handle)
|
|
390
|
+
begin
|
|
391
|
+
BatchDecoder.decode_and_flatten(
|
|
392
|
+
_parse_handle_reference(Parser.grammar_handle(grammar), input),
|
|
393
|
+
input, Parsanol::Slice
|
|
394
|
+
)
|
|
395
|
+
rescue RuntimeError => e
|
|
396
|
+
raise_native_parse_error(e, grammar, input)
|
|
397
|
+
end
|
|
398
|
+
rescue RuntimeError => e
|
|
399
|
+
raise_native_parse_error(e, grammar, input)
|
|
400
|
+
end
|
|
401
|
+
end
|
|
358
402
|
end
|
|
359
403
|
end
|
|
360
404
|
end
|
|
@@ -108,13 +108,24 @@ module Parsanol
|
|
|
108
108
|
end
|
|
109
109
|
|
|
110
110
|
root_atom(entry_name).parse(input)
|
|
111
|
+
when :reference
|
|
112
|
+
# The plain recursive relation (TODO.sota/8 B1): same shaping
|
|
113
|
+
# pipeline, no VM, no memo tables, no fused scans. Never falls
|
|
114
|
+
# back to :ruby — a reference result produced by another
|
|
115
|
+
# engine would defeat the differential. Deliberately slow.
|
|
116
|
+
unless Native.reference_available?
|
|
117
|
+
raise NotImplementedError, "reference mode requires the native " \
|
|
118
|
+
"extension with parsanol >= 0.18.18"
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
Native.parse_reference(root_atom(entry_name), input)
|
|
111
122
|
when :ruby
|
|
112
123
|
# Deepest-failure reporting parity with the native engine: the
|
|
113
124
|
# default Tree reporter keeps the most recent (root) failure,
|
|
114
125
|
# which reports position 1:1 for grammars with entry wrappers.
|
|
115
126
|
root_atom(entry_name).parse(input, reporter: ErrorReporter::Deepest.new)
|
|
116
127
|
else
|
|
117
|
-
raise ArgumentError, "unknown mode #{mode.inspect} (use :native or :ruby)"
|
|
128
|
+
raise ArgumentError, "unknown mode #{mode.inspect} (use :native, :reference or :ruby)"
|
|
118
129
|
end
|
|
119
130
|
end
|
|
120
131
|
|
|
@@ -250,44 +261,82 @@ module Parsanol
|
|
|
250
261
|
return [[], []] unless body_rule
|
|
251
262
|
|
|
252
263
|
body_atom = compiler.atom_for(body_rule)
|
|
264
|
+
# R2 declared form: the baked resync pattern owns the skip
|
|
265
|
+
# spans (author-deterministic — no trial scanning). Falls back
|
|
266
|
+
# to the inferred trial scan when the rule declares none.
|
|
267
|
+
pattern_atom = compiler.resync_atom_for(body_rule) ||
|
|
268
|
+
compiler.resync_atom_for(entry(entry_name)["root"])
|
|
253
269
|
max = input.length
|
|
254
270
|
resyncs = []
|
|
255
271
|
items = []
|
|
256
272
|
rounds = 0
|
|
257
273
|
while rounds < 8
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
break
|
|
274
|
+
attempt = try_iteration(body_atom, input, cursor)
|
|
275
|
+
if attempt
|
|
276
|
+
items << root_atom(entry_name).flatten(attempt[0])
|
|
277
|
+
cursor = attempt[1]
|
|
278
|
+
until cursor >= max
|
|
279
|
+
attempt = try_iteration(body_atom, input, cursor)
|
|
280
|
+
break unless attempt
|
|
281
|
+
|
|
282
|
+
items << root_atom(entry_name).flatten(attempt[0])
|
|
283
|
+
cursor = attempt[1]
|
|
284
|
+
end
|
|
285
|
+
break if cursor >= max
|
|
286
|
+
end
|
|
287
|
+
if pattern_atom
|
|
288
|
+
# Declared: the pattern match (gap + delimiter) is the
|
|
289
|
+
# annotated error span.
|
|
290
|
+
span = scan_pattern(pattern_atom, input, cursor)
|
|
291
|
+
break unless span
|
|
292
|
+
|
|
293
|
+
resume = span[1]
|
|
294
|
+
resyncs << {
|
|
295
|
+
"offset" => cursor,
|
|
296
|
+
"skipped" => input[cursor...resume],
|
|
297
|
+
"resumed_at" => resume,
|
|
298
|
+
}
|
|
299
|
+
else
|
|
300
|
+
# Inferred: scan for the next viable iteration start.
|
|
301
|
+
found = nil
|
|
302
|
+
scan = cursor
|
|
303
|
+
while scan < max
|
|
304
|
+
attempt = try_iteration(body_atom, input, scan)
|
|
305
|
+
if attempt && attempt[1] > scan
|
|
306
|
+
found = [scan, attempt[1], attempt[0]]
|
|
307
|
+
break
|
|
308
|
+
end
|
|
309
|
+
scan += 1
|
|
265
310
|
end
|
|
266
|
-
|
|
311
|
+
break unless found
|
|
312
|
+
|
|
313
|
+
skip_start, resume, value = found
|
|
314
|
+
resyncs << {
|
|
315
|
+
"offset" => cursor,
|
|
316
|
+
"skipped" => input[cursor...skip_start],
|
|
317
|
+
"resumed_at" => skip_start,
|
|
318
|
+
}
|
|
319
|
+
items << root_atom(entry_name).flatten(value)
|
|
267
320
|
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
321
|
cursor = resume
|
|
278
322
|
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
323
|
end
|
|
288
324
|
[items, resyncs]
|
|
289
325
|
end
|
|
290
326
|
|
|
327
|
+
# Scan forward from `from` for the first position where the
|
|
328
|
+
# declared pattern matches with progress: [match_start, match_end].
|
|
329
|
+
def scan_pattern(pattern_atom, input, from)
|
|
330
|
+
scan = from
|
|
331
|
+
while scan < input.length
|
|
332
|
+
attempt = try_iteration(pattern_atom, input, scan)
|
|
333
|
+
return [scan, attempt[1]] if attempt
|
|
334
|
+
|
|
335
|
+
scan += 1
|
|
336
|
+
end
|
|
337
|
+
nil
|
|
338
|
+
end
|
|
339
|
+
|
|
291
340
|
# Apply the body atom at one position: [value, end_pos] on a
|
|
292
341
|
# progressing match, nil otherwise.
|
|
293
342
|
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