parsanol 1.3.98 → 1.3.100

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: 141dfea6902c1018accd9962fc346017dbb8d070303323df0d98e5f16bf12516
4
- data.tar.gz: ec3ae0172d80d7c6c84743376958fb2ceb7dffd9072afbfe641fcf4e0267e079
3
+ metadata.gz: 6006d5eac0900781aae6db62350af96b3635055c0ad96f5509b448a4e6e37bcb
4
+ data.tar.gz: cb3b2196ae881b7e1ca07b99911428fcdaeeab9e70eb159b217ffe436130e44a
5
5
  SHA512:
6
- metadata.gz: 40e41398c910b644fe3be0a7042d0a2ed763e2e622d61e98d3efa01d5bf74301c2adc61dbfdd689f99987c6677a7737402d97f013050b4cd6080c3706ee656b9
7
- data.tar.gz: 46e233a13a96163779eb6eca5d07687cc6fe9c7fbd3e88c81e20d2af9aa43939ddca682e1210b3d2e5cc5b1555c2660aad8cecc01d9ff45f0adc25b6d0fc9ff7
6
+ metadata.gz: 3b9117bf8dd677c3b7350100a7f49d8f6ec0196c07f7bffbe2115f6e6e5d5cc03d0d01418d4d1cf271a5738f08f1978336ef3282f2bcd72fc41958b125de70ac
7
+ data.tar.gz: 92426461df5f4825154c0afbaa75315c5b3fe25d0fa4fbe544c445bbecd69dc2d96b973e23a59263e6f055d4b636bb89034134e11c31a6db0094aebce68c2e6d
data/Cargo.lock CHANGED
@@ -307,9 +307,9 @@ checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
307
307
 
308
308
  [[package]]
309
309
  name = "parsanol"
310
- version = "0.18.10"
310
+ version = "0.18.12"
311
311
  source = "registry+https://github.com/rust-lang/crates.io-index"
312
- checksum = "54bbd6459639572bc085c504112bc9a4acbca47719736aebad19d9113549bdad"
312
+ checksum = "d237b2a3835d553b0e069245741b264fd21e89c509d6c4c2c017a2013ea142e5"
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.10"
329
+ version = "0.18.12"
330
330
  source = "registry+https://github.com/rust-lang/crates.io-index"
331
- checksum = "40adc63c6d2afcd1b095518d55f0095c5c04445f46994592161f0fa60cf6331b"
331
+ checksum = "3125de14cee1ab1dfd0daf9307d02899f9d23c824e64b9179e4c26fc56ade9b0"
332
332
  dependencies = [
333
333
  "proc-macro2",
334
334
  "quote",
@@ -236,9 +236,15 @@ module Parsanol
236
236
  end
237
237
 
238
238
  # Reports detailed error by reparsing with reporter.
239
+ #
240
+ # The default reporter is Deepest (parsanol-ruby#265): the native
241
+ # engine's failure diagnostics report the furthest failure, and a
242
+ # consumer's file:line:col degrades to "line 1 char 1" when the
243
+ # interpreter raises the outermost sequence instead. Explicit
244
+ # reporters (Tree for full cause trees) are honored unchanged.
239
245
  def report_detailed_error(input, consume_all, reporter, _initial_error)
240
246
  input.bytepos = 0
241
- error_reporter = reporter || Parsanol::ErrorReporter::Tree.new
247
+ error_reporter = reporter || Parsanol::ErrorReporter::Deepest.new
242
248
  success, cause = run_with_context(input, error_reporter, consume_all)
243
249
 
244
250
  # Second parse should also fail
@@ -98,12 +98,16 @@ module Parsanol
98
98
  # @return [Array<Integer, Parsanol::Cause>] [depth, deepest_leaf]
99
99
  #
100
100
  def find_deepest_leaf(node, rank = 0)
101
+ return [rank, node] if node.nil?
102
+
101
103
  best_node = node
102
104
  best_rank = rank
103
105
 
104
106
  kids = node.children
105
107
  if kids && !kids.empty?
106
108
  kids.each do |kid|
109
+ next if kid.nil?
110
+
107
111
  kid_rank, kid_node = find_deepest_leaf(kid, rank + 1)
108
112
 
109
113
  if kid_rank > best_rank
@@ -282,8 +282,10 @@ module Parsanol
282
282
  if parsanol_grammar
283
283
  # No native diagnostics (e.g. incomplete-input errors): one
284
284
  # interpreter pass with the reporter attached — a success
285
- # recovers the tree, a failure raises the cause tree.
286
- reporter = Parsanol::ErrorReporter::Tree.new
285
+ # recovers the tree, a failure raises the cause tree. Deepest
286
+ # keeps the fallback's diagnostics at parity with the direct
287
+ # interpreter path (parsanol-ruby#265).
288
+ reporter = Parsanol::ErrorReporter::Deepest.new
287
289
  source = Parsanol::Source.new(input)
288
290
  success, value = grammar.run_with_context(source, reporter, true)
289
291
  return grammar.finalize_result(value) if success
@@ -165,6 +165,13 @@ module Parsanol
165
165
  case hash["kind"]
166
166
  when "reject"
167
167
  "test #{hash['input'].inspect}: expected the input to be rejected"
168
+ when "shape"
169
+ actual = Bindings.canonical(shape)
170
+ expected = Bindings.canonical(hash["expect"])
171
+ next if actual == expected
172
+
173
+ "test #{hash['input'].inspect}: expected shape #{expected.inspect}, " \
174
+ "got #{actual.inspect}"
168
175
  when "example"
169
176
  bound = apply_bindings(entry_name, shape)
170
177
  expect = hash["expect"].to_h { |key, value| [key.to_sym, value] }
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "json"
4
+
3
5
  module Parsanol
4
6
  module PARG
5
7
  # Applies an entry's artifact bindings to a parsanol-shape parse tree,
@@ -32,6 +34,7 @@ module Parsanol
32
34
  when Hash then value.to_h { |key, item| [key.to_s, canonical(item)] }
33
35
  when Array then value.map { |item| canonical(item) }
34
36
  when nil, true, false, Integer then value
37
+ when Float then JSON.generate(value) # B6: the JSON number text
35
38
  else String(value)
36
39
  end
37
40
  end
@@ -53,8 +53,13 @@ module Parsanol
53
53
  @dynamic = false
54
54
  @tables = {}
55
55
  @warnings = []
56
+ @expanded = false
56
57
  end
57
58
 
59
+ # Reserved prefix for the tier rules synthesized by the direct
60
+ # left-recursion expansion (see expand_left_recursion!).
61
+ LEFT_RECURSION_PREFIX = "__left_"
62
+
58
63
  # QoL shorthands (ABNF core rules): materialized on reference when
59
64
  # the grammar does not define its own rule of that name.
60
65
  BUILTIN_CLASSES = {
@@ -67,6 +72,7 @@ module Parsanol
67
72
  }.freeze
68
73
 
69
74
  def compile
75
+ expand_left_recursion!
70
76
  @document.rules.each_key { |name| atom_for(name) }
71
77
  errors = Lints.errors(@document, self)
72
78
  raise CompileError, errors.join("\n") unless errors.empty?
@@ -121,6 +127,7 @@ module Parsanol
121
127
  end
122
128
 
123
129
  def atom_for(name)
130
+ expand_left_recursion! unless @expanded
124
131
  @atom_cache[name] ||= begin
125
132
  atom = build_atom(fetch_rule(name))
126
133
  # Atomic rules build without injection entirely: token-shaped
@@ -348,6 +355,106 @@ module Parsanol
348
355
  # In-file grammar tests run at compile time: the build fails when an
349
356
  # accept/example test does not parse or an example's expected capture
350
357
  # pairs do not match the bound result.
358
+ # Direct left recursion (owner decision 2026-10-21, parsanol-ruby#251):
359
+ # rewrite `r = r op1 r / r op2 r / ... / primary` into a chain of
360
+ # ordinary left-folding tier rules — the ANTLR 4 model. Branches in
361
+ # source order are precedence tiers, earlier binding tighter; the
362
+ # non-recursive alternatives are the primary (loosest operand). The
363
+ # rewritten tiers are plain rules, so every engine inherits the
364
+ # feature through the unchanged artifact format and the fold value is
365
+ # exactly the flattening of the equivalent loop grammar (V1/V3).
366
+ # Indirect left recursion keeps its compile error.
367
+ def expand_left_recursion!
368
+ return if @expanded
369
+
370
+ @expanded = true
371
+ if @document.rules.any? { |name, _| name.start_with?(LEFT_RECURSION_PREFIX) }
372
+ raise CompileError,
373
+ "rule names beginning with #{LEFT_RECURSION_PREFIX.inspect} are reserved " \
374
+ "for the compiler's left-recursion expansion"
375
+ end
376
+
377
+ # Snapshot: expansion adds tier rules to @document.rules.
378
+ @document.rules.dup.each_key do |name|
379
+ body = @document.rules[name]
380
+ next unless left_recursive_body?(name, body)
381
+
382
+ expand_left_recursive_rule(name, body)
383
+ end
384
+ end
385
+
386
+ def left_recursive_body?(name, body)
387
+ branches = body.kind == :alt ? body.a : [body]
388
+ branches.any? { |branch| leftmost_refs(branch, foldable: true).include?(name) }
389
+ end
390
+
391
+ def expand_left_recursive_rule(name, body)
392
+ branches = body.kind == :alt ? body.a : [body]
393
+ left, primary =
394
+ branches.partition { |branch| leftmost_refs(branch, foldable: true).include?(name) }
395
+ if primary.empty?
396
+ raise CompileError,
397
+ "rule #{name.inspect} is left-recursive and has no " \
398
+ "non-recursive alternative to anchor the rewrite"
399
+ end
400
+
401
+ rules = @document.rules
402
+ prev = "#{LEFT_RECURSION_PREFIX}#{name}_primary"
403
+ rules[prev] = primary.length == 1 ? primary.first : Node.new(:alt, primary)
404
+ left.each_with_index do |branch, index|
405
+ tier = "#{LEFT_RECURSION_PREFIX}#{name}_#{index + 1}"
406
+ suffix = left_recursive_suffix(name, branch)
407
+ suffix = substitute_refs(suffix, name, prev)
408
+ rules[tier] = Node.new(:seq, [Node.new(:ref, prev), Node.new(:rep, suffix, 0, nil)])
409
+ prev = tier
410
+ end
411
+ rules[name] = Node.new(:ref, prev)
412
+ end
413
+
414
+ # A left-recursive alternative must BEGIN with a bare reference to the
415
+ # rule itself; everything after that reference is the per-iteration
416
+ # suffix, which must consume input (a nullable suffix would loop
417
+ # forever at the fold).
418
+ def left_recursive_suffix(name, branch)
419
+ if branch.kind == :ref
420
+ raise CompileError,
421
+ "left-recursive alternative in rule #{name.inspect} consists only of the " \
422
+ "recursive reference and would loop forever"
423
+ end
424
+ unless branch.kind == :seq && (head = branch.a.first)&.kind == :ref && head.a == name
425
+ raise CompileError,
426
+ "left-recursive alternative in rule #{name.inspect} must begin with a " \
427
+ "bare reference to #{name.inspect} (write `#{name} <rest>`; captures and " \
428
+ "predicates around the leading reference are not supported)"
429
+ end
430
+
431
+ rest = branch.a[1..]
432
+ rest_node = rest.length == 1 ? rest.first : Node.new(:seq, rest)
433
+ if nullable?(rest_node)
434
+ raise CompileError,
435
+ "left-recursive alternative in rule #{name.inspect} can match empty after " \
436
+ "the leading reference and would loop forever at the fold"
437
+ end
438
+ rest_node
439
+ end
440
+
441
+ def substitute_refs(node, from, to)
442
+ if node.kind == :ref
443
+ return Node.new(:ref, to) if node.a == from
444
+
445
+ return node
446
+ end
447
+ case node.kind
448
+ when :seq then Node.new(:seq, node.a.map { |child| substitute_refs(child, from, to) })
449
+ when :alt then Node.new(:alt, node.a.map { |child| substitute_refs(child, from, to) })
450
+ when :rep then Node.new(:rep, substitute_refs(node.a, from, to), node.b, node.c)
451
+ when :opt then Node.new(:opt, substitute_refs(node.a, from, to))
452
+ when :pred then Node.new(:pred, node.a, substitute_refs(node.b, from, to), node.c)
453
+ when :cap then Node.new(:cap, node.a, substitute_refs(node.b, from, to))
454
+ else node
455
+ end
456
+ end
457
+
351
458
  def run_tests
352
459
  return [] if @document.tests.empty?
353
460
 
@@ -360,6 +467,13 @@ module Parsanol
360
467
  case test.kind
361
468
  when :reject
362
469
  "test #{test.input.inspect}: expected the input to be rejected"
470
+ when :shape
471
+ actual = Bindings.canonical(shape)
472
+ expected = Bindings.canonical(test.expect)
473
+ next if actual == expected
474
+
475
+ "test #{test.input.inspect}: expected shape #{expected.inspect}, " \
476
+ "got #{actual.inspect}"
363
477
  when :example
364
478
  bindings = binding_list(rule)
365
479
  if bindings.empty?
@@ -704,19 +818,24 @@ module Parsanol
704
818
  nil
705
819
  end
706
820
 
707
- def leftmost_refs(node)
821
+ # A self-reference under a predicate cannot anchor a fold (and the
822
+ # lookbehind validation rejects rule bodies outright), so the
823
+ # rewrite's scan stops there; cycle detection still walks through.
824
+ def leftmost_refs(node, foldable: false)
708
825
  case node.kind
709
826
  when :ref then [node.a]
710
827
  when :seq
711
828
  refs = []
712
829
  node.a.each do |item|
713
- refs.concat(leftmost_refs(item))
830
+ refs.concat(leftmost_refs(item, foldable: foldable))
714
831
  break unless nullable?(item)
715
832
  end
716
833
  refs.uniq
717
- when :alt then node.a.flat_map { |branch| leftmost_refs(branch) }.uniq
718
- when :rep, :opt then leftmost_refs(node.a)
719
- when :pred, :cap then leftmost_refs(node.b)
834
+ when :alt then node.a.flat_map { |branch| leftmost_refs(branch, foldable: foldable) }.uniq
835
+ when :rep, :opt then leftmost_refs(node.a, foldable: foldable)
836
+ when :pred
837
+ foldable ? [] : leftmost_refs(node.b)
838
+ when :cap then leftmost_refs(node.b, foldable: foldable)
720
839
  else []
721
840
  end
722
841
  end
@@ -790,6 +909,17 @@ module Parsanol
790
909
 
791
910
  # ---- artifact envelope ----------------------------------------------
792
911
 
912
+ # Test expectations are JSON-shaped: pair expectations are maps with
913
+ # symbol keys; `shape` expectations may be any value. Normalize keys
914
+ # recursively so the envelope is plain JSON.
915
+ def normalize_expect(value)
916
+ case value
917
+ when Hash then value.to_h { |key, item| [key.to_s, normalize_expect(item)] }
918
+ when Array then value.map { |item| normalize_expect(item) }
919
+ else value
920
+ end
921
+ end
922
+
793
923
  def build_envelope
794
924
  entries = @document.entries.to_h do |name, rule|
795
925
  [name, entry_envelope(rule)]
@@ -819,7 +949,7 @@ module Parsanol
819
949
  "entry" => test.entry || (@document.own_entries.first if @document.own_entries.size == 1),
820
950
  "kind" => test.kind.to_s,
821
951
  "input" => test.input,
822
- "expect" => test.expect.transform_keys(&:to_s),
952
+ "expect" => normalize_expect(test.expect),
823
953
  }
824
954
  end,
825
955
  "docs" => @document.docs,
@@ -11,7 +11,7 @@ module Parsanol
11
11
  # `capture` = phase 1 (grammar layer): asserts the raw capture tree,
12
12
  # no bindings section needed. `example` = phase 2 (bindings layer):
13
13
  # asserts the bound, preprocessed attribute hash.
14
- TEST_KINDS = %i[accept reject example capture].freeze
14
+ TEST_KINDS = %i[accept reject example capture shape].freeze
15
15
  # Test kinds whose expectations are `{ key: value }` pairs.
16
16
  PAIR_KINDS = %w[example capture].freeze
17
17
 
@@ -83,7 +83,12 @@ module Parsanol
83
83
  kind = ident.value
84
84
  input = unquote(expect(:str).value)
85
85
  expect_pairs = {}
86
- if Document::PAIR_KINDS.include?(kind) && peek&.type == :punct && peek.value == "{"
86
+ if kind == "shape"
87
+ # Whole-tree assertion: the expectation is an arbitrary value
88
+ # (text, array, object), not a capture-name map.
89
+ expect_pairs = parse_test_value
90
+ elsif Document::PAIR_KINDS.include?(kind) &&
91
+ peek&.type == :punct && peek.value == "{"
87
92
  advance
88
93
  skip_newlines
89
94
  until peek&.type == :punct && peek.value == "}"
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Parsanol
4
- VERSION = "1.3.98"
4
+ VERSION = "1.3.100"
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.98
4
+ version: 1.3.100
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.