parsanol 1.3.120 → 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 +4 -4
- data/Cargo.lock +4 -4
- data/lib/parsanol/native/parser.rb +15 -1
- data/lib/parsanol/parg/bindings.rb +88 -14
- data/lib/parsanol/parg/compiler.rb +60 -1
- data/lib/parsanol/parg/error.rb +4 -0
- data/lib/parsanol/parg.rb +1 -0
- data/lib/parsanol/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4a2162ac62e96201b3ab45cba32e5b69f8162ec37b266a52fa5be0b16e85823f
|
|
4
|
+
data.tar.gz: db0e6ef317d68d558eb06e36b0fcbe50b1f556786065d87dd599154671d8df73
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7bdb62867da115b8372fd37b2f4532d44ad48e47571c1aab897845313b4f6bf585aa25e1efcf386aba83305c610256f871c7a5052c631e0c085a03ab1b85560b
|
|
7
|
+
data.tar.gz: dbfdf3281e65e86abba50653461bb572c4246499bf8a629b269c4898fc824f8b951714db3bb3a48e5609cecb72d9d10230ac571c73575be195a66d95105ec595
|
data/Cargo.lock
CHANGED
|
@@ -307,9 +307,9 @@ checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
|
|
307
307
|
|
|
308
308
|
[[package]]
|
|
309
309
|
name = "parsanol"
|
|
310
|
-
version = "0.19.
|
|
310
|
+
version = "0.19.19"
|
|
311
311
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
312
|
-
checksum = "
|
|
312
|
+
checksum = "7ec17007676a476ef23db64b69c3bf5b36d741763ac11ced3089b90a96c9a716"
|
|
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.19.
|
|
329
|
+
version = "0.19.19"
|
|
330
330
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
331
|
-
checksum = "
|
|
331
|
+
checksum = "fc342af122d8854b8916eaacc6a6c2ad911419c605ea47b74b575c2ca24ab23d"
|
|
332
332
|
dependencies = [
|
|
333
333
|
"proc-macro2",
|
|
334
334
|
"quote",
|
|
@@ -178,13 +178,27 @@ module Parsanol
|
|
|
178
178
|
|
|
179
179
|
def append_atom_structure(atom, visited = {}.compare_by_identity, output = +"")
|
|
180
180
|
if visited[atom]
|
|
181
|
+
# An Entity's self-reference keeps its name in the
|
|
182
|
+
# fingerprint, mirroring the non-visited branch (ruby#404).
|
|
183
|
+
if atom.is_a?(Parsanol::Atoms::Entity)
|
|
184
|
+
return output << "[:entity, " << atom.rule_name.to_s.inspect << ", [:cycle, " << atom.class.name.inspect << "]]"
|
|
185
|
+
end
|
|
186
|
+
|
|
181
187
|
return output << "[:cycle, " << atom.class.name.inspect << "]"
|
|
182
188
|
end
|
|
183
189
|
|
|
184
190
|
visited[atom] = true
|
|
185
191
|
case atom
|
|
186
192
|
when Parsanol::Atoms::Entity
|
|
187
|
-
|
|
193
|
+
# The rule name belongs in the fingerprint (rs#404-class
|
|
194
|
+
# collision): Entity is transparent in the serialized graph's
|
|
195
|
+
# STRUCTURE only when the target is spelled out, but the JSON
|
|
196
|
+
# keeps the Entity node itself — two webs whose Entities point
|
|
197
|
+
# at different rules share a transparent fingerprint while
|
|
198
|
+
# serializing differently, and the serializer cache then
|
|
199
|
+
# serves one grammar's JSON for the other.
|
|
200
|
+
output << "[:entity, " << atom.rule_name.to_s.inspect << ", "
|
|
201
|
+
append_atom_structure(atom.parslet, visited, output) << "]"
|
|
188
202
|
when Parsanol::Atoms::Str
|
|
189
203
|
output << "[:str, " << atom.str.inspect << "]"
|
|
190
204
|
when Parsanol::Atoms::Re
|
|
@@ -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
|
|
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.
|
|
74
|
-
#
|
|
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
|
-
#
|
|
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
|
|
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 =
|
|
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
|
|
119
|
-
#
|
|
120
|
-
#
|
|
121
|
-
#
|
|
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
|
-
|
|
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
|
|
@@ -261,6 +261,10 @@ module Parsanol
|
|
|
261
261
|
# the declaration means the tree never sees this subtree named,
|
|
262
262
|
# inside or out.
|
|
263
263
|
return atom if glue_rule?(rule_name)
|
|
264
|
+
# A capture spans the rules it references (ruby#400): their
|
|
265
|
+
# bodies compile unwrapped so the capture's value equals its
|
|
266
|
+
# edition-1 value.
|
|
267
|
+
return atom if captured_reachable?(rule_name)
|
|
264
268
|
|
|
265
269
|
wrap_uncaptured(atom)
|
|
266
270
|
end
|
|
@@ -273,6 +277,57 @@ module Parsanol
|
|
|
273
277
|
@document.glue_rules.include?(name.to_s)
|
|
274
278
|
end
|
|
275
279
|
|
|
280
|
+
# The rules a text capture transitively spans (ruby#400): a
|
|
281
|
+
# capture on a reference captures the referenced rule's whole
|
|
282
|
+
# match, so every rule under a capture atom — the direct
|
|
283
|
+
# reference and everything its body transitively references —
|
|
284
|
+
# compiles unwrapped. Edition 2's Named leaves then attach only
|
|
285
|
+
# to non-captured structure, and a capture's value equals its
|
|
286
|
+
# edition-1 value (`{num: "123"}`, never a leaf array).
|
|
287
|
+
def captured_reachable?(name)
|
|
288
|
+
@capture_reachable ||= begin
|
|
289
|
+
seeds = []
|
|
290
|
+
@document.rules.each_value do |node|
|
|
291
|
+
collect_refs_under_captures(node, seeds)
|
|
292
|
+
end
|
|
293
|
+
reachable = []
|
|
294
|
+
until seeds.empty?
|
|
295
|
+
rule = seeds.pop
|
|
296
|
+
next if reachable.include?(rule) || !@document.rules.key?(rule)
|
|
297
|
+
|
|
298
|
+
reachable << rule
|
|
299
|
+
collect_all_refs(@document.rules[rule], seeds)
|
|
300
|
+
end
|
|
301
|
+
reachable
|
|
302
|
+
end
|
|
303
|
+
@capture_reachable.include?(name.to_s)
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
def collect_refs_under_captures(node, out)
|
|
307
|
+
walk = lambda do |n, in_capture|
|
|
308
|
+
case n.kind
|
|
309
|
+
when :ref then out << n.a if in_capture
|
|
310
|
+
when :cap then walk.call(n.b, true)
|
|
311
|
+
when :pred then walk.call(n.b, in_capture)
|
|
312
|
+
when :alt, :seq then n.a.each { |child| walk.call(child, in_capture) }
|
|
313
|
+
when :rep, :opt then walk.call(n.a, in_capture)
|
|
314
|
+
end
|
|
315
|
+
end
|
|
316
|
+
walk.call(node, false)
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
def collect_all_refs(node, out)
|
|
320
|
+
walk = lambda do |n|
|
|
321
|
+
case n.kind
|
|
322
|
+
when :ref then out << n.a
|
|
323
|
+
when :alt, :seq then n.a.each { |child| walk.call(child) }
|
|
324
|
+
when :cap, :pred then walk.call(n.b)
|
|
325
|
+
when :rep, :opt then walk.call(n.a)
|
|
326
|
+
end
|
|
327
|
+
end
|
|
328
|
+
walk.call(node)
|
|
329
|
+
end
|
|
330
|
+
|
|
276
331
|
def wrap_uncaptured(atom)
|
|
277
332
|
case atom
|
|
278
333
|
when Atoms::Named
|
|
@@ -289,6 +344,7 @@ module Parsanol
|
|
|
289
344
|
# agree by design.
|
|
290
345
|
if trivia_only?(atom.rule_name.to_s, { atom.rule_name.to_s => true }) ||
|
|
291
346
|
glue_rule?(atom.rule_name.to_s) ||
|
|
347
|
+
captured_reachable?(atom.rule_name.to_s) ||
|
|
292
348
|
produces_capture?(atom.parslet)
|
|
293
349
|
atom
|
|
294
350
|
else
|
|
@@ -476,9 +532,12 @@ module Parsanol
|
|
|
476
532
|
when Atoms::Named then nil
|
|
477
533
|
when Atoms::Entity
|
|
478
534
|
# Mirror wrap_uncaptured's verdicts: trivia glue, declared
|
|
479
|
-
# glue,
|
|
535
|
+
# glue, capture-reachable refs (ruby#400 — a capture spans
|
|
536
|
+
# them, so modern mode never names them), and
|
|
537
|
+
# capture-producing refs all stay bare.
|
|
480
538
|
unless trivia_only?(atom.rule_name.to_s, { atom.rule_name.to_s => true }) ||
|
|
481
539
|
glue_rule?(atom.rule_name.to_s) ||
|
|
540
|
+
captured_reachable?(atom.rule_name.to_s) ||
|
|
482
541
|
produces_capture?(atom.parslet)
|
|
483
542
|
lines << "rule #{site}: reference to #{atom.rule_name} produces no capture -- " \
|
|
484
543
|
"modern mode would name it #{atom.rule_name}"
|
data/lib/parsanol/parg/error.rb
CHANGED
|
@@ -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"
|
data/lib/parsanol/version.rb
CHANGED