parsanol 1.3.98-aarch64-linux → 1.3.100-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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 59eaaf30f6cc627ef3dc9e80b8e33ac3b2c3d8855e699848a7b730fef8569016
4
- data.tar.gz: 45ed8be2284f602b832849f17ac00ee8f4fa19b8e6cf7d7bd081c3aa2b68a954
3
+ metadata.gz: ffe9cf736fae27a11f941c38eaa1ec8f5ad6a67af74a81952d6a2a39478cd69d
4
+ data.tar.gz: 5a35dd607a8ca943fb12da09ce8f90a6e9c15efaddb589e3861ab6b71ed646f6
5
5
  SHA512:
6
- metadata.gz: 9bed617e17bee7ef49a9abbe7df5640dc1c7cdea910fbcaf8da7c1ea215b47f7f9a4939e080484b4ed403c474723a4750ec4240d1e80e5eae225dad464869559
7
- data.tar.gz: c31194ecf89c3d809a1166a92b497103da9ce0ebe06d86a39bb6736a09eb7626681a2aa643d3ee47232c407602410cbe1314cb516a6b2eacc56b1a9a2a36491c
6
+ metadata.gz: 4426f5e8c9a6073f95582e6ffbaec7d3efdd4b2cea1525db7cadc6baf1b64b4394ff1da62758b72b171c86a4d9134c09b0b104f408f35ec871b7eeaead0853d6
7
+ data.tar.gz: 8196495a9b452106dd4f1ef500b16e5dedadb06984b605629ae473051af4a4fb18a79fbe53b04f8c325f6c6f9142b904d6ee34b51d5e10e2c0bb1546d2841d11
@@ -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: 40739648 bytes
4
- sha256: 25301e0ded598dd4a79bdce7a3b30ec4f4666b4d14850d15ec33fb03437d925f
3
+ size: 40823664 bytes
4
+ sha256: 74eadd193ef8d65d50ee57835773ce7c815f6ce9ce795ffebb039c2db1fb1292
@@ -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: 40974736 bytes
4
- sha256: 713b7c4b9a55b2da5e65f05c40a5f0fa132de7ad23cf2db1ef4b8b2efba56c11
3
+ size: 41058696 bytes
4
+ sha256: b6a5f7b526422c4766de9b1d45eaa82c99cf59c7fa330aa2af832b607a2f71f8
@@ -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: 40966176 bytes
4
- sha256: 84d2b4d7e0700e410ad8d95f68d4c2bf29354de1e28faf55a38bbb20ae0b5668
3
+ size: 41054344 bytes
4
+ sha256: 1c95bcd0337a6961284fed89522e1a446cc90e21369a2994a5f62414c98ccf85
@@ -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: 40976224 bytes
4
- sha256: 30615ee206ca278c00f2cb8ffb4deae4e74e3f4aed2afb3dcae40a84a22af494
3
+ size: 41064400 bytes
4
+ sha256: 3921a83255cd10483c05b9d8c65d170d4dce6379aee2415adc56b85c3dabdf77
@@ -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
@@ -1,4 +1,4 @@
1
1
  [diffend] Oversized file quarantined before diffing.
2
2
  name: data/lib/parsanol/native/libparsanol.so
3
- size: 36519752 bytes
4
- sha256: 88291bd6f470adb9d6057e726f14c108c3336b5a053597bb51b444a9327fc7ba
3
+ size: 36611864 bytes
4
+ sha256: db77c7d151957854ca658a95d3edbf03bbf0df6032a9f16779b57816b2c83068
@@ -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,14 +1,14 @@
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: aarch64-linux
6
6
  authors:
7
7
  - Ribose Inc.
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-10-20 00:00:00.000000000 Z
11
+ date: 2026-10-21 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