parsanol 1.3.121-aarch64-linux → 1.3.123-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: b59325cb49756928a21a99177042caeb687bf5720f02ca3c4992efaf259b3975
4
- data.tar.gz: 6049d358d43d93dd8b7146a293c6bf424343871778b99be986ed54bda2a81699
3
+ metadata.gz: 6a05aa3803a0a35921ce270f05e7375af46c1c008c9b2afb98ed7e63701fe014
4
+ data.tar.gz: bb80e21f834f580e0a21f604fde26208d49679dcd889730da0f8f670d703eb82
5
5
  SHA512:
6
- metadata.gz: 1aaf59d820edb6f97326c61053cd06acb40036d86d5da669925718710f01ebd70310371fb72aef0f102e983efdd178d9adf96ff9528692ef101a0cc88421b66a
7
- data.tar.gz: 422219a69dcb76b014fbd317bfd58ade7721acf55c12ffcd5d126c682547d37d94395c4c23b8c8b59445bcafec936d977ef52605cb90e4b33474ad7f7a094713
6
+ metadata.gz: 5c307c932d5e3aa2927b03c090e23b50cc1de8cf1979100a3e120ac928b38b88dd16af55eee668e491467b70b33f250f77e673356cdc10f67c67222679abb773
7
+ data.tar.gz: bb2b3e5fe203e6e97594209576e7651e62d38b61f86f3c1a4860c6a06a1369a0e7b4784b6cb82ef29812766d77a98347482f9e38b5c61c6691b94e039cf3d8f9
@@ -13,6 +13,16 @@ module Parsanol
13
13
  def apply(artifact, entry, shape)
14
14
  bindings = entry["bindings"] || []
15
15
  captures = collect_captures(shape)
16
+ # Snapshot before the assignment pass: reading a missing key off
17
+ # `captures` inserts it (default-proc hash), which would make the
18
+ # #413 lost-check below see phantom presence. Presence means a
19
+ # NON-NIL value somewhere in the tree: a fired-but-empty optional
20
+ # capture binds null in the parslet shape, and binding nothing for
21
+ # null is legitimate optionality, not loss (parsanol-ruby#418 —
22
+ # CIE trailing_lang, JIS reaffirmed).
23
+ present = captures.keys.select do |key|
24
+ captures[key].any? { |value| !value.nil? }
25
+ end
16
26
  out = {}
17
27
  scalar, arrays = bindings.partition { |binding| !binding["path"].to_s.include?("[]") }
18
28
  scalar.each do |binding|
@@ -22,9 +32,38 @@ module Parsanol
22
32
  arrays.group_by { |binding| binding["path"].split("[]").first }.each do |prefix, group|
23
33
  assign_array(out, artifact, prefix, group, captures, shape)
24
34
  end
35
+ # Every declared capture that the tree carries must reach the
36
+ # bound map (parsanol-ruby#413): a capture present in the parse
37
+ # but bound nowhere is silent data loss, the one case the
38
+ # binding layer's other checks miss. Absence in the TREE is
39
+ # legitimate optionality and never raises.
40
+ lost = bindings.select do |binding|
41
+ capture = binding["capture"].to_sym
42
+ present.include?(capture) && !bound?(out, binding)
43
+ end
44
+ unless lost.empty?
45
+ raise BindError,
46
+ "entry #{entry['name'].inspect}: capture #{lost.map { |b| b['capture'].inspect }.join(', ')} " \
47
+ "is present in the parse tree but bound nowhere"
48
+ end
25
49
  out
26
50
  end
27
51
 
52
+ # Did this binding contribute anything to the bound map? Scalars
53
+ # bind under their leaf key; array groups bind under their prefix.
54
+ def bound?(out, binding)
55
+ path = binding["path"].to_s
56
+ if path.include?("[]")
57
+ prefix = path.split("[]").first.to_sym
58
+ rows = out[prefix]
59
+ rows.is_a?(Array) && rows.any? do |row|
60
+ row.is_a?(Hash) && row.key?(leaf_key(binding))
61
+ end
62
+ else
63
+ out.key?(leaf_key(binding))
64
+ end
65
+ end
66
+
28
67
  # Canonical test-value form: one equality surface for capture and
29
68
  # example expectations on both sides of the comparison (Slice vs
30
69
  # source literal was comparing heterogeneous types). Terminals
@@ -65,24 +104,30 @@ module Parsanol
65
104
 
66
105
  def assign_array(out, artifact, prefix, group, captures, shape = nil)
67
106
  # Position-aware pairing (parsanol-ruby#381): when the shape
68
- # nests the group's instances under the prefix key, each
107
+ # nests the group's instances under the PREFIX KEY, each
69
108
  # instance's subtree owns its captures — pairing by OCCURRENCE
70
109
  # index landed a sparse capture (one that fires in only some
71
110
  # instances) on the wrong instance. Per-instance collection
72
111
  # fixes the pairing and is identical to the old flat pairing
73
- # when every instance fires every capture. Captures outside
74
- # any instance keep the legacy occurrence pairing.
112
+ # when every instance fires every capture.
113
+ #
114
+ # Identification is strictly keyed: the bare top array of a
115
+ # repetition-root entry is NOT claimed as instances
116
+ # (parsanol-ruby#408/#412 — that fallback claimed per-token rule
117
+ # siblings as group instances, splitting paired captures into
118
+ # single-field rows and sprouting phantom rows for unrelated
119
+ # siblings). A capture outside a keyed instance array keeps the
120
+ # legacy occurrence pairing. Phantom rows materialize as {}
121
+ # (the #408 contract: the engines agree by always emitting the
122
+ # row; the contracted phantom representation is {} , never nil).
75
123
  instances = shape && group_instances(shape, prefix)
76
124
  if instances
77
- # The occurrence path's `return if count.zero?`: a repetition
78
- # that fired zero iterations (the tree carries `parts: []`)
125
+ # parsanol-rs#370: a repetition that fired zero iterations
79
126
  # binds no key — the recorded native expectations and the
80
- # pubid builders rely on the absent reading (the empty-array
81
- # binding regression, caught by the pubid corpus gate; see
82
- # parsanol-rs#370).
127
+ # pubid builders rely on the absent reading.
83
128
  return if instances.empty?
84
129
 
85
- list = (out[prefix.to_sym] ||= [])
130
+ list = []
86
131
  instances.each do |instance|
87
132
  found = collect_captures(instance)
88
133
  element = {}
@@ -94,6 +139,7 @@ module Parsanol
94
139
  end
95
140
  list << element
96
141
  end
142
+ out[prefix.to_sym] = list
97
143
  return
98
144
  end
99
145
 
@@ -115,10 +161,12 @@ module Parsanol
115
161
  end
116
162
  end
117
163
 
118
- # The `prefix[]` group's instance subtrees: the first array under
119
- # a key naming the prefix, walked in collect_captures' BFS order.
120
- # A repetition-root entry's shape IS the instances array (the wire
121
- # has no key for it) — that is the fallback when no key matches.
164
+ # The `prefix[]` group's instance subtrees: the array under a key
165
+ # naming the prefix, walked in collect_captures' BFS order. There
166
+ # is deliberately NO fallback for a repetition-root entry — a bare
167
+ # top array cannot be told apart from per-token rule siblings
168
+ # (#408/#412), so claiming it is magic; those shapes pair by
169
+ # occurrence instead.
122
170
  def group_instances(shape, prefix)
123
171
  queue = [shape]
124
172
  until queue.empty?
@@ -133,7 +181,7 @@ module Parsanol
133
181
  when Array then queue.concat(node)
134
182
  end
135
183
  end
136
- shape if shape.is_a?(Array) && shape.any?(Hash)
184
+ nil
137
185
  end
138
186
 
139
187
  def validate_path!(binding)
@@ -199,10 +247,42 @@ module Parsanol
199
247
  # string-keyed hashes, repeats as arrays — so (any)-bound keys
200
248
  # survive a cross-engine gate without consumer shims.
201
249
  plain(value)
250
+ when "text"
251
+ # The matched span as text (ruby#382): a capture whose body
252
+ # carries inner captures binds a hash of them; (text) returns
253
+ # the span those captures jointly matched while the inner
254
+ # captures still fire as their own bindings.
255
+ span_text(value)
202
256
  else value
203
257
  end
204
258
  end
205
259
 
260
+ # The matched span of a capture value: the byte range from the
261
+ # first to the last slice in the subtree, cut from the input the
262
+ # slices carry. A plain Slice or String is already its span.
263
+ def span_text(value)
264
+ return value.to_s if value.is_a?(Parsanol::Slice) || value.is_a?(String)
265
+
266
+ slices = []
267
+ walk = lambda do |node|
268
+ case node
269
+ when Parsanol::Slice then slices << node
270
+ when Hash then node.each_value { |item| Array(item).each(&walk) }
271
+ when Array then node.each(&walk)
272
+ end
273
+ end
274
+ walk.call(value)
275
+ return plain(value) if slices.empty?
276
+
277
+ start = slices.map(&:offset).min
278
+ stop = slices.map { |slice| slice.offset + slice.content.bytesize }.max
279
+ input = slices.first.input
280
+ return plain(value) unless input
281
+
282
+ span = input.byteslice(start, stop - start)
283
+ span&.force_encoding(Encoding::UTF_8)
284
+ end
285
+
206
286
  # The engine-neutral reduction behind (any): capture leaves
207
287
  # (Parsanol::Slice, native wire objects) and scalars render as
208
288
  # their text, hashes as string-keyed plain hashes, arrays
@@ -14,5 +14,9 @@ module Parsanol
14
14
 
15
15
  # Raised when an artifact envelope is corrupt or its checksum mismatches.
16
16
  class ArtifactError < Error; end
17
+
18
+ # A declared capture is present in the parse tree but reached the
19
+ # bound map nowhere (parsanol-ruby#413): silent data loss made loud.
20
+ class BindError < ArtifactError; end
17
21
  end
18
22
  end
data/lib/parsanol/parg.rb CHANGED
@@ -17,6 +17,7 @@ module Parsanol
17
17
  autoload :ParseError, "parsanol/parg/error"
18
18
  autoload :CompileError, "parsanol/parg/error"
19
19
  autoload :ArtifactError, "parsanol/parg/error"
20
+ autoload :BindError, "parsanol/parg/error"
20
21
  autoload :Node, "parsanol/parg/node"
21
22
  autoload :Export, "parsanol/parg/export"
22
23
  autoload :Lexer, "parsanol/parg/lexer"
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Parsanol
4
- VERSION = "1.3.121"
4
+ VERSION = "1.3.123"
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.121
4
+ version: 1.3.123
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-11-01 00:00:00.000000000 Z
11
+ date: 2026-11-02 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