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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3954375840e755bd28df5e62048c4ec4dc94ab56a5917c839e04901e95290c2f
4
- data.tar.gz: ec2e66f50dab4a8e6cc30651d9b4fe6ec468e4441a1c62b697f3a28d61e86167
3
+ metadata.gz: cb8829be859854aad36e7ed1a0b43675af3295f714fb86ef9ee0af9cabf491ec
4
+ data.tar.gz: 73137433d9c3676ca771f696cccda01d8f87770a638fcc4eb0ea92332ff974f9
5
5
  SHA512:
6
- metadata.gz: 465b6dfa7765cf143cc78bd7eab75bf8c8c7bf9d20a859930a868be5563de33e44f50e2844a519f9179d4d1dbe9836a4392042c573cf58dbd106e110bf67a26c
7
- data.tar.gz: 06c14f1c1111606323b4ec1e3c895a88fbb9109440158ed68eab0094840c9ce38e366bc2e049653790257873f720755196087d16abdc29aad3afbd8d07b3af54
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.16"
310
+ version = "0.18.17"
311
311
  source = "registry+https://github.com/rust-lang/crates.io-index"
312
- checksum = "597e0971251e10f0d9fac99a9f217dc2ce8e5607df9f93d2a12a5e4d823d0ee9"
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.16"
329
+ version = "0.18.17"
330
330
  source = "registry+https://github.com/rust-lang/crates.io-index"
331
- checksum = "342e298363272c74478384205a3d6def7905d8277945d65ad203606596de7fe6"
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
- found = nil
259
- scan = cursor
260
- while scan < max
261
- attempt = try_iteration(body_atom, input, scan)
262
- if attempt && attempt[1] > scan
263
- found = [scan, attempt[1], attempt[0]]
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
- scan += 1
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!
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Parsanol
4
- VERSION = "1.3.105"
4
+ VERSION = "1.3.106"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: parsanol
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.105
4
+ version: 1.3.106
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.