parsanol 1.3.97-aarch64-linux → 1.3.99-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: f259ea55092d5673e2d9b3604df6fb9adba3807dba48500b765562de3b1b6aee
4
- data.tar.gz: d6c1ba491efa08d1ccc551d79899558577c72138909a17d46a39560ca818b80a
3
+ metadata.gz: e06421f8e5c544c7b5075efef008185c9fa67abaa6aa30e74c1f829f3778bd5b
4
+ data.tar.gz: 56f37f201e95b55f636feb91bde17721f83d7b5f2ea83e25305654a568cf2344
5
5
  SHA512:
6
- metadata.gz: 066d798f2dc0b057a46db9f077aee2e0d4a424b076c836cce9152ef1d969b2f3d587b0071ed9f1b3524a0534c223aa9cd58b2d43d8511ec58eb8d867438a8fd9
7
- data.tar.gz: 8bce87d1b44af2a4ec860b5a1072837bda1ac171a6aee8d1fd1dd505cac84a3ae84f7529f413430014de1dd902ce69197d2c8f9ae6cb16c465591d520a36af37
6
+ metadata.gz: 917dbbb4c00e3ac7e3ee1ba9bdf223200f5f4bec1f926e9019ce952b3e70b2063793c12afc71c846b38ba5441b526629acd50db20e8b93b93f6de1a7fa6da06c
7
+ data.tar.gz: 434bc70187d37e1c927652952ff88030b0db72485f8e90c9baa837725a2bb75fa0a804a7423bf40cbe9f8eacfde19045bdadb669492f302eb16cf492a23af375
@@ -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: 40739336 bytes
4
- sha256: 4cad7bf4894a1cea08ee2b319696f166f2e7cc38ed6eef38dee79057f9fd684a
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: 40974392 bytes
4
- sha256: 25df57a50ccd92bd51baaf86008559487f4259b81a899c30c61e8e023ff7ec23
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: 40961728 bytes
4
- sha256: e6e8cae20b2ffc4754570fd4e4e19081e24e4b5c37ccc0e122619ada30ca7d97
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: 40971784 bytes
4
- sha256: 4c426cf8bfd77fb270f32e171664208d9b3fd3414bcf22b2d677fddc2ec14e9f
3
+ size: 41064400 bytes
4
+ sha256: 3921a83255cd10483c05b9d8c65d170d4dce6379aee2415adc56b85c3dabdf77
@@ -1,4 +1,4 @@
1
1
  [diffend] Oversized file quarantined before diffing.
2
2
  name: data/lib/parsanol/native/libparsanol.so
3
- size: 36519512 bytes
4
- sha256: e6eab7ae66da64b0abf1e86985786423cce6eba79524e64771a52fd53d541530
3
+ size: 36611864 bytes
4
+ sha256: db77c7d151957854ca658a95d3edbf03bbf0df6032a9f16779b57816b2c83068
@@ -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] }
@@ -178,8 +185,9 @@ module Parsanol
178
185
  when "capture"
179
186
  captures = Bindings.collect_captures(shape)
180
187
  mismatched = hash["expect"].reject do |key, value|
181
- actual = captures[key.to_sym]&.first
182
- !actual.nil? && Bindings.canonical(actual) == Bindings.canonical(value)
188
+ values = captures[key.to_sym]
189
+ values && !values.empty? &&
190
+ Bindings.canonical(values.first) == Bindings.canonical(value)
183
191
  end
184
192
  next if mismatched.empty?
185
193
 
@@ -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
@@ -64,7 +67,9 @@ module Parsanol
64
67
  count = group.map { |binding| captures[binding["capture"].to_sym].length }.max || 0
65
68
  return if count.zero?
66
69
 
67
- list = (out[prefix] ||= [])
70
+ # Field names are symbols everywhere else (scalar bindings, element
71
+ # fields); the array prefix must match or `example` lookups miss it.
72
+ list = (out[prefix.to_sym] ||= [])
68
73
  count.times do |index|
69
74
  element = {}
70
75
  group.each do |binding|
@@ -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?
@@ -378,8 +492,9 @@ module Parsanol
378
492
  when :capture
379
493
  captures = Bindings.collect_captures(shape)
380
494
  mismatched = test.expect.reject do |key, value|
381
- actual = captures[key.to_sym]&.first
382
- !actual.nil? && Bindings.canonical(actual) == Bindings.canonical(value)
495
+ values = captures[key.to_sym]
496
+ values && !values.empty? &&
497
+ Bindings.canonical(values.first) == Bindings.canonical(value)
383
498
  end
384
499
  next if mismatched.empty?
385
500
 
@@ -703,19 +818,24 @@ module Parsanol
703
818
  nil
704
819
  end
705
820
 
706
- 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)
707
825
  case node.kind
708
826
  when :ref then [node.a]
709
827
  when :seq
710
828
  refs = []
711
829
  node.a.each do |item|
712
- refs.concat(leftmost_refs(item))
830
+ refs.concat(leftmost_refs(item, foldable: foldable))
713
831
  break unless nullable?(item)
714
832
  end
715
833
  refs.uniq
716
- when :alt then node.a.flat_map { |branch| leftmost_refs(branch) }.uniq
717
- when :rep, :opt then leftmost_refs(node.a)
718
- 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)
719
839
  else []
720
840
  end
721
841
  end
@@ -789,6 +909,17 @@ module Parsanol
789
909
 
790
910
  # ---- artifact envelope ----------------------------------------------
791
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
+
792
923
  def build_envelope
793
924
  entries = @document.entries.to_h do |name, rule|
794
925
  [name, entry_envelope(rule)]
@@ -818,7 +949,7 @@ module Parsanol
818
949
  "entry" => test.entry || (@document.own_entries.first if @document.own_entries.size == 1),
819
950
  "kind" => test.kind.to_s,
820
951
  "input" => test.input,
821
- "expect" => test.expect.transform_keys(&:to_s),
952
+ "expect" => normalize_expect(test.expect),
822
953
  }
823
954
  end,
824
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
 
@@ -23,7 +23,7 @@ module Parsanol
23
23
  | (?<arrow>->)
24
24
  | (?<num>\d+)
25
25
  | (?<ident>[A-Za-z_][A-Za-z0-9_]*)
26
- | (?<punct>[=\/()\[\]{}*!&:,.<])
26
+ | (?<punct>[=\/()\[\]{}*!&:,.<-])
27
27
  /x
28
28
 
29
29
  def initialize(text)
@@ -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 == "}"
@@ -91,6 +96,8 @@ module Parsanol
91
96
  punct(":")
92
97
  expect_pairs[key.to_sym] = parse_test_value
93
98
  skip_newlines
99
+ advance if peek&.type == :punct && peek.value == ","
100
+ skip_newlines
94
101
  end
95
102
  punct("}")
96
103
  end
@@ -101,18 +108,84 @@ module Parsanol
101
108
  skip_newlines
102
109
  end
103
110
 
111
+ # Expectation values: strings, numbers, true/false/null, arrays, and
112
+ # nested objects. Nested structure is what pins array bindings,
113
+ # absent-optional nil, and flatten/merge results in the corpus;
114
+ # before this the test grammar only accepted scalars.
104
115
  def parse_test_value
105
- token = advance
116
+ token = peek
106
117
  case token&.type
107
- when :str then unquote(token.value)
108
- when :num then token.value.to_i
109
- when :ident
110
- raise ParseError, "expected true or false" unless %w[true false].include?(token.value)
118
+ when :str then unquote(advance.value)
119
+ when :num then parse_test_number
120
+ when :punct
121
+ case token.value
122
+ when "[" then parse_test_array
123
+ when "{" then parse_test_object
124
+ when "-"
125
+ advance
126
+ unless peek&.type == :num
127
+ raise ParseError, "expected a number after '-' in a test value"
128
+ end
111
129
 
112
- token.value == "true"
130
+ -parse_test_number
131
+ else
132
+ raise ParseError,
133
+ "expected a test value (string, number, true, false, null, array, object)"
134
+ end
135
+ when :ident
136
+ advance
137
+ case token.value
138
+ when "true" then true
139
+ when "false" then false
140
+ when "null" then nil
141
+ else
142
+ raise ParseError,
143
+ "expected true, false, or null, got #{token.value.inspect}"
144
+ end
113
145
  else
114
- raise ParseError, "expected a string, number, true or false"
146
+ raise ParseError,
147
+ "expected a test value (string, number, true, false, null, array, object)"
148
+ end
149
+ end
150
+
151
+ # The lexer emits integers only; a decimal is num "." num.
152
+ def parse_test_number
153
+ text = advance.value
154
+ if peek&.type == :punct && peek.value == "." && @tokens[@pos + 1]&.type == :num
155
+ advance
156
+ text = "#{text}.#{advance.value}"
115
157
  end
158
+ text.include?(".") ? Float(text) : Integer(text, 10)
159
+ end
160
+
161
+ def parse_test_array
162
+ punct("[")
163
+ values = []
164
+ skip_newlines
165
+ until peek&.type == :punct && peek.value == "]"
166
+ values << parse_test_value
167
+ skip_newlines
168
+ advance if peek&.type == :punct && peek.value == ","
169
+ skip_newlines
170
+ end
171
+ punct("]")
172
+ values
173
+ end
174
+
175
+ def parse_test_object
176
+ punct("{")
177
+ value = {}
178
+ skip_newlines
179
+ until peek&.type == :punct && peek.value == "}"
180
+ key = ident.value
181
+ punct(":")
182
+ value[key.to_sym] = parse_test_value
183
+ skip_newlines
184
+ advance if peek&.type == :punct && peek.value == ","
185
+ skip_newlines
186
+ end
187
+ punct("}")
188
+ value
116
189
  end
117
190
 
118
191
  def eof? = @pos >= @tokens.length
@@ -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
  # F10 phase 2 (gate slice): every .parg source must parse under the parg
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Parsanol
4
- VERSION = "1.3.97"
4
+ VERSION = "1.3.99"
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.97
4
+ version: 1.3.99
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