parsanol 1.3.121 → 1.3.122

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: 6b2308bfe0b4b5787cbfac5725e10e2afd01ade41f3ae373dd09df250dbaebd3
4
- data.tar.gz: b49dc9c6b767be5c8ec34492b3e1a63683aa8bba12f8a4d194ddd107169b51da
3
+ metadata.gz: 4a2162ac62e96201b3ab45cba32e5b69f8162ec37b266a52fa5be0b16e85823f
4
+ data.tar.gz: db0e6ef317d68d558eb06e36b0fcbe50b1f556786065d87dd599154671d8df73
5
5
  SHA512:
6
- metadata.gz: 280e47f635e0319aa01a2b5b4936ddc3aa62c9eae8e6f3c13f07600a5904bafb2e7bbd7521f984e5689f5e550eb964c09ac23c8ca36ab7913d4c776f83fafea4
7
- data.tar.gz: bab0183da6fcf3b5c837ba9f46e675269b5b567e77a3333ea8b5c41c5c85f5390a3f82cc8d9778b63cd57308fa7b0053d67da12d055f9a70c18e0c7c397b1c7f
6
+ metadata.gz: 7bdb62867da115b8372fd37b2f4532d44ad48e47571c1aab897845313b4f6bf585aa25e1efcf386aba83305c610256f871c7a5052c631e0c085a03ab1b85560b
7
+ data.tar.gz: dbfdf3281e65e86abba50653461bb572c4246499bf8a629b269c4898fc824f8b951714db3bb3a48e5609cecb72d9d10230ac571c73575be195a66d95105ec595
@@ -13,6 +13,10 @@ 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.
19
+ present = captures.keys
16
20
  out = {}
17
21
  scalar, arrays = bindings.partition { |binding| !binding["path"].to_s.include?("[]") }
18
22
  scalar.each do |binding|
@@ -22,9 +26,38 @@ module Parsanol
22
26
  arrays.group_by { |binding| binding["path"].split("[]").first }.each do |prefix, group|
23
27
  assign_array(out, artifact, prefix, group, captures, shape)
24
28
  end
29
+ # Every declared capture that the tree carries must reach the
30
+ # bound map (parsanol-ruby#413): a capture present in the parse
31
+ # but bound nowhere is silent data loss, the one case the
32
+ # binding layer's other checks miss. Absence in the TREE is
33
+ # legitimate optionality and never raises.
34
+ lost = bindings.select do |binding|
35
+ capture = binding["capture"].to_sym
36
+ present.include?(capture) && !bound?(out, binding)
37
+ end
38
+ unless lost.empty?
39
+ raise BindError,
40
+ "entry #{entry['name'].inspect}: capture #{lost.map { |b| b['capture'].inspect }.join(', ')} " \
41
+ "is present in the parse tree but bound nowhere"
42
+ end
25
43
  out
26
44
  end
27
45
 
46
+ # Did this binding contribute anything to the bound map? Scalars
47
+ # bind under their leaf key; array groups bind under their prefix.
48
+ def bound?(out, binding)
49
+ path = binding["path"].to_s
50
+ if path.include?("[]")
51
+ prefix = path.split("[]").first.to_sym
52
+ rows = out[prefix]
53
+ rows.is_a?(Array) && rows.any? do |row|
54
+ row.is_a?(Hash) && row.key?(leaf_key(binding))
55
+ end
56
+ else
57
+ out.key?(leaf_key(binding))
58
+ end
59
+ end
60
+
28
61
  # Canonical test-value form: one equality surface for capture and
29
62
  # example expectations on both sides of the comparison (Slice vs
30
63
  # source literal was comparing heterogeneous types). Terminals
@@ -65,24 +98,30 @@ module Parsanol
65
98
 
66
99
  def assign_array(out, artifact, prefix, group, captures, shape = nil)
67
100
  # Position-aware pairing (parsanol-ruby#381): when the shape
68
- # nests the group's instances under the prefix key, each
101
+ # nests the group's instances under the PREFIX KEY, each
69
102
  # instance's subtree owns its captures — pairing by OCCURRENCE
70
103
  # index landed a sparse capture (one that fires in only some
71
104
  # instances) on the wrong instance. Per-instance collection
72
105
  # 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.
106
+ # when every instance fires every capture.
107
+ #
108
+ # Identification is strictly keyed: the bare top array of a
109
+ # repetition-root entry is NOT claimed as instances
110
+ # (parsanol-ruby#408/#412 — that fallback claimed per-token rule
111
+ # siblings as group instances, splitting paired captures into
112
+ # single-field rows and sprouting phantom rows for unrelated
113
+ # siblings). A capture outside a keyed instance array keeps the
114
+ # legacy occurrence pairing. Phantom rows materialize as {}
115
+ # (the #408 contract: the engines agree by always emitting the
116
+ # row; the contracted phantom representation is {} , never nil).
75
117
  instances = shape && group_instances(shape, prefix)
76
118
  if instances
77
- # The occurrence path's `return if count.zero?`: a repetition
78
- # that fired zero iterations (the tree carries `parts: []`)
119
+ # parsanol-rs#370: a repetition that fired zero iterations
79
120
  # 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).
121
+ # pubid builders rely on the absent reading.
83
122
  return if instances.empty?
84
123
 
85
- list = (out[prefix.to_sym] ||= [])
124
+ list = []
86
125
  instances.each do |instance|
87
126
  found = collect_captures(instance)
88
127
  element = {}
@@ -94,6 +133,7 @@ module Parsanol
94
133
  end
95
134
  list << element
96
135
  end
136
+ out[prefix.to_sym] = list
97
137
  return
98
138
  end
99
139
 
@@ -115,10 +155,12 @@ module Parsanol
115
155
  end
116
156
  end
117
157
 
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.
158
+ # The `prefix[]` group's instance subtrees: the array under a key
159
+ # naming the prefix, walked in collect_captures' BFS order. There
160
+ # is deliberately NO fallback for a repetition-root entry — a bare
161
+ # top array cannot be told apart from per-token rule siblings
162
+ # (#408/#412), so claiming it is magic; those shapes pair by
163
+ # occurrence instead.
122
164
  def group_instances(shape, prefix)
123
165
  queue = [shape]
124
166
  until queue.empty?
@@ -133,7 +175,7 @@ module Parsanol
133
175
  when Array then queue.concat(node)
134
176
  end
135
177
  end
136
- shape if shape.is_a?(Array) && shape.any?(Hash)
178
+ nil
137
179
  end
138
180
 
139
181
  def validate_path!(binding)
@@ -199,10 +241,42 @@ module Parsanol
199
241
  # string-keyed hashes, repeats as arrays — so (any)-bound keys
200
242
  # survive a cross-engine gate without consumer shims.
201
243
  plain(value)
244
+ when "text"
245
+ # The matched span as text (ruby#382): a capture whose body
246
+ # carries inner captures binds a hash of them; (text) returns
247
+ # the span those captures jointly matched while the inner
248
+ # captures still fire as their own bindings.
249
+ span_text(value)
202
250
  else value
203
251
  end
204
252
  end
205
253
 
254
+ # The matched span of a capture value: the byte range from the
255
+ # first to the last slice in the subtree, cut from the input the
256
+ # slices carry. A plain Slice or String is already its span.
257
+ def span_text(value)
258
+ return value.to_s if value.is_a?(Parsanol::Slice) || value.is_a?(String)
259
+
260
+ slices = []
261
+ walk = lambda do |node|
262
+ case node
263
+ when Parsanol::Slice then slices << node
264
+ when Hash then node.each_value { |item| Array(item).each(&walk) }
265
+ when Array then node.each(&walk)
266
+ end
267
+ end
268
+ walk.call(value)
269
+ return plain(value) if slices.empty?
270
+
271
+ start = slices.map(&:offset).min
272
+ stop = slices.map { |slice| slice.offset + slice.content.bytesize }.max
273
+ input = slices.first.input
274
+ return plain(value) unless input
275
+
276
+ span = input.byteslice(start, stop - start)
277
+ span&.force_encoding(Encoding::UTF_8)
278
+ end
279
+
206
280
  # The engine-neutral reduction behind (any): capture leaves
207
281
  # (Parsanol::Slice, native wire objects) and scalars render as
208
282
  # 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.122"
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.121
4
+ version: 1.3.122
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.