parsanol 1.3.92-aarch64-linux → 1.3.94-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 +4 -4
- data/lib/parsanol/3.2/parsanol_native.so +2 -2
- data/lib/parsanol/3.3/parsanol_native.so +2 -2
- data/lib/parsanol/3.4/parsanol_native.so +2 -2
- data/lib/parsanol/4.0/parsanol_native.so +2 -2
- data/lib/parsanol/native/libparsanol.so +2 -2
- data/lib/parsanol/native/parser.rb +46 -47
- data/lib/parsanol/parg/artifact.rb +26 -5
- data/lib/parsanol/parg/bindings.rb +13 -0
- data/lib/parsanol/parg/compiler.rb +64 -5
- data/lib/parsanol/parg/document.rb +9 -1
- data/lib/parsanol/parg/parser.rb +1 -1
- data/lib/parsanol/version.rb +1 -1
- data/lib/parsanol/vm.rb +6 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9e0c1efcdfb93af513ae908d45c90b6f2d6c42015fb643e474bffbb37ae39316
|
|
4
|
+
data.tar.gz: 86ad51bce683affc8b5b94426151bf56694c41fa87b91c9e56c98b7e801c1efc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1b5aea575e016166e39822daa41c36c9ef80da76c121610231c6c359071d31362f4e5ed0efd24b4278d8db896ff70f7702e71cbfdf5d4ff4e2f36469222e5085
|
|
7
|
+
data.tar.gz: b89fe36dabaa496b63c7bb220bd5c40c5698fbc3ecac8329359d53d2319ec56df283accc410e15fcfe14d8e0729cbfd876e58c6de382079ce716f1f94e795725
|
|
@@ -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:
|
|
4
|
-
sha256:
|
|
3
|
+
size: 40550656 bytes
|
|
4
|
+
sha256: cff224d02b36baf6455b961827d069556a7fed497e0a8e778009065f12613f00
|
|
@@ -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:
|
|
4
|
-
sha256:
|
|
3
|
+
size: 40785744 bytes
|
|
4
|
+
sha256: ae67525c0f460813aed0b9fccb58809b517da84d72dae383bc232bd3c0c5d4f0
|
|
@@ -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:
|
|
4
|
-
sha256:
|
|
3
|
+
size: 40781360 bytes
|
|
4
|
+
sha256: e30f6696ef7f343f3edf49240039e9287277f6973d4e0bfee918939d82684056
|
|
@@ -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:
|
|
4
|
-
sha256:
|
|
3
|
+
size: 40791392 bytes
|
|
4
|
+
sha256: 2f8c4e6eb46bc01f1271aeb19e5b3beb3b1c81ce1deb99b500094fabf3b11517
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
[diffend] Oversized file quarantined before diffing.
|
|
2
2
|
name: data/lib/parsanol/native/libparsanol.so
|
|
3
|
-
size:
|
|
4
|
-
sha256:
|
|
3
|
+
size: 36352992 bytes
|
|
4
|
+
sha256: fc4e4581f2ff40db6d747bd6b6377e34da0d7389eccc7b1ecb30f485f2e59e25
|
|
@@ -167,59 +167,58 @@ module Parsanol
|
|
|
167
167
|
end
|
|
168
168
|
|
|
169
169
|
def grammar_structure_hash(atom)
|
|
170
|
-
Digest::MD5.hexdigest(
|
|
170
|
+
Digest::MD5.hexdigest(append_atom_structure(atom))
|
|
171
171
|
end
|
|
172
172
|
|
|
173
|
-
def
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
if visited[obj_id]
|
|
177
|
-
return [:cycle, atom.class.name]
|
|
173
|
+
def append_atom_structure(atom, visited = {}.compare_by_identity, output = +"")
|
|
174
|
+
if visited[atom]
|
|
175
|
+
return output << "[:cycle, " << atom.class.name.inspect << "]"
|
|
178
176
|
end
|
|
179
177
|
|
|
180
|
-
visited[
|
|
181
|
-
|
|
178
|
+
visited[atom] = true
|
|
182
179
|
case atom
|
|
183
|
-
when
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
[:seq,
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
[:trivia_capture, atom.capturers.to_a.sort,
|
|
216
|
-
|
|
217
|
-
when
|
|
218
|
-
[:trivia,
|
|
219
|
-
|
|
220
|
-
|
|
180
|
+
when Parsanol::Atoms::Entity
|
|
181
|
+
append_atom_structure(atom.parslet, visited, output)
|
|
182
|
+
when Parsanol::Atoms::Str
|
|
183
|
+
output << "[:str, " << atom.str.inspect << "]"
|
|
184
|
+
when Parsanol::Atoms::Re
|
|
185
|
+
output << "[:re, " << atom.match.inspect << "]"
|
|
186
|
+
when Parsanol::Atoms::Sequence, Parsanol::Atoms::Alternative
|
|
187
|
+
children = atom.is_a?(Parsanol::Atoms::Sequence) ? atom.parslets : atom.alternatives
|
|
188
|
+
output << (atom.is_a?(Parsanol::Atoms::Sequence) ? "[:seq, [" : "[:alt, [")
|
|
189
|
+
index = 0
|
|
190
|
+
while index < children.length
|
|
191
|
+
output << ", " unless index.zero?
|
|
192
|
+
append_atom_structure(children[index], visited, output)
|
|
193
|
+
index += 1
|
|
194
|
+
end
|
|
195
|
+
output << "]]"
|
|
196
|
+
when Parsanol::Atoms::Repetition
|
|
197
|
+
output << "[:rep, " << atom.min.inspect << ", " << atom.max.inspect << ", "
|
|
198
|
+
append_atom_structure(atom.parslet, visited, output) << "]"
|
|
199
|
+
when Parsanol::Atoms::Named
|
|
200
|
+
output << "[:named, " << atom.name.to_s.inspect << ", "
|
|
201
|
+
append_atom_structure(atom.parslet, visited, output) << "]"
|
|
202
|
+
when Parsanol::Atoms::Lookahead
|
|
203
|
+
output << "[:lookahead, " << atom.positive.inspect << ", "
|
|
204
|
+
append_atom_structure(atom.bound_parslet, visited, output) << "]"
|
|
205
|
+
when Parsanol::Atoms::Constant
|
|
206
|
+
# The inspected constant remains a string inside the fingerprint.
|
|
207
|
+
output << "[:constant, " << atom.value.inspect.inspect << "]"
|
|
208
|
+
when Parsanol::Atoms::Lookbehind
|
|
209
|
+
output << "[:lookbehind, " << atom.count.inspect << ", " << atom.pattern.inspect << ", " << atom.positive.inspect << "]"
|
|
210
|
+
# Capturing trivia must precede its Trivia and Ignored parents.
|
|
211
|
+
when Parsanol::Atoms::TriviaCapture
|
|
212
|
+
output << "[:trivia_capture, " << atom.capturers.to_a.sort.inspect << ", "
|
|
213
|
+
append_atom_structure(atom.wrapped_atom, visited, output) << "]"
|
|
214
|
+
when Parsanol::Atoms::Trivia
|
|
215
|
+
output << "[:trivia, "
|
|
216
|
+
append_atom_structure(atom.wrapped_atom, visited, output) << "]"
|
|
217
|
+
when Parsanol::Atoms::Ignored
|
|
218
|
+
output << "[:ignored, "
|
|
219
|
+
append_atom_structure(atom.wrapped_atom, visited, output) << "]"
|
|
221
220
|
else
|
|
222
|
-
[:unknown, atom.class.name]
|
|
221
|
+
output << "[:unknown, " << atom.class.name.inspect << "]"
|
|
223
222
|
end
|
|
224
223
|
end
|
|
225
224
|
end
|
|
@@ -152,21 +152,42 @@ module Parsanol
|
|
|
152
152
|
|
|
153
153
|
# Run an external test list (suite files yield Document::Test
|
|
154
154
|
# structs; embedded tests are artifact-shaped hashes).
|
|
155
|
-
|
|
155
|
+
# parse_with overrides how the input is turned into a shape
|
|
156
|
+
# (the envelope self-test drives the baked grammar JSON natively
|
|
157
|
+
# instead of root_atom's source rebuild); bindings and
|
|
158
|
+
# expectations still flow through the standard surface.
|
|
159
|
+
def run_test_list(tests, mode: :native, parse_with: nil)
|
|
156
160
|
tests.filter_map do |test|
|
|
157
161
|
hash = test.is_a?(Document::Test) ? test_to_hash(test) : test
|
|
158
162
|
entry_name = hash["entry"] || entries.first
|
|
159
163
|
begin
|
|
160
|
-
|
|
161
|
-
|
|
164
|
+
shape = parse_with ? parse_with.call(hash["input"]) : parse(entry_name, hash["input"], mode: mode)
|
|
165
|
+
case hash["kind"]
|
|
166
|
+
when "reject"
|
|
162
167
|
"test #{hash['input'].inspect}: expected the input to be rejected"
|
|
163
|
-
|
|
168
|
+
when "example"
|
|
169
|
+
bound = apply_bindings(entry_name, shape)
|
|
164
170
|
expect = hash["expect"].to_h { |key, value| [key.to_sym, value] }
|
|
165
|
-
mismatched = expect.reject
|
|
171
|
+
mismatched = expect.reject do |key, value|
|
|
172
|
+
bound.key?(key) && Bindings.canonical(bound[key]) == Bindings.canonical(value)
|
|
173
|
+
end
|
|
166
174
|
next if mismatched.empty?
|
|
167
175
|
|
|
168
176
|
"test #{hash['input'].inspect}: expected captures " \
|
|
169
177
|
"#{mismatched.transform_values(&:inspect).inspect}, got #{bound.inspect}"
|
|
178
|
+
when "capture"
|
|
179
|
+
captures = Bindings.collect_captures(shape)
|
|
180
|
+
mismatched = hash["expect"].reject do |key, value|
|
|
181
|
+
actual = captures[key.to_sym]&.first
|
|
182
|
+
!actual.nil? && Bindings.canonical(actual) == Bindings.canonical(value)
|
|
183
|
+
end
|
|
184
|
+
next if mismatched.empty?
|
|
185
|
+
|
|
186
|
+
actuals = mismatched.keys.to_h do |key|
|
|
187
|
+
[key, Bindings.canonical(captures[key.to_sym]&.first)]
|
|
188
|
+
end
|
|
189
|
+
"test #{hash['input'].inspect}: expected captures " \
|
|
190
|
+
"#{mismatched.transform_values(&:inspect).inspect}, got #{actuals.inspect}"
|
|
170
191
|
end
|
|
171
192
|
rescue Parsanol::ParseFailed
|
|
172
193
|
unless hash["kind"] == "reject"
|
|
@@ -23,6 +23,19 @@ module Parsanol
|
|
|
23
23
|
out
|
|
24
24
|
end
|
|
25
25
|
|
|
26
|
+
# Canonical test-value form: one equality surface for capture and
|
|
27
|
+
# example expectations on both sides of the comparison (Slice vs
|
|
28
|
+
# source literal was comparing heterogeneous types). Terminals
|
|
29
|
+
# reduce to their text; structure recurses; keys stringify.
|
|
30
|
+
def canonical(value)
|
|
31
|
+
case value
|
|
32
|
+
when Hash then value.to_h { |key, item| [key.to_s, canonical(item)] }
|
|
33
|
+
when Array then value.map { |item| canonical(item) }
|
|
34
|
+
when nil, true, false, Integer then value
|
|
35
|
+
else String(value)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
26
39
|
def collect_captures(shape)
|
|
27
40
|
found = Hash.new { |hash, key| hash[key] = [] }
|
|
28
41
|
queue = [shape]
|
|
@@ -76,9 +76,40 @@ module Parsanol
|
|
|
76
76
|
raise CompileError, failures.join("\n") unless failures.empty?
|
|
77
77
|
|
|
78
78
|
envelope = build_envelope
|
|
79
|
+
failures = self.class.self_test(envelope, tables_dir: @tables_dir)
|
|
80
|
+
raise CompileError, failures.join("\n") unless failures.empty?
|
|
81
|
+
|
|
79
82
|
Result.new(@atom_cache, @warnings, envelope)
|
|
80
83
|
end
|
|
81
84
|
|
|
85
|
+
# Bake-time self-test (parsanol-ruby#223): replay the grammar's
|
|
86
|
+
# embedded tests against the BAKED ENVELOPE atom JSON, not the
|
|
87
|
+
# source this process just compiled. A bake that drops or
|
|
88
|
+
# rewrites a member (the 1.3.91 ordered-choice class) makes the
|
|
89
|
+
# envelope disagree with its own tests, and every Ruby-side
|
|
90
|
+
# parse path would mask it — root_atom rebuilds from the
|
|
91
|
+
# embedded source, so only the native engine's JSON registration
|
|
92
|
+
# executes the atom graph a wasm/rs consumer actually runs.
|
|
93
|
+
# Skips (empty) when the native engine is unavailable; the CI
|
|
94
|
+
# and release bakes always have it. Returns failure descriptions.
|
|
95
|
+
def self.self_test(envelope, tables_dir: nil)
|
|
96
|
+
tests = envelope["tests"].to_a
|
|
97
|
+
return [] if tests.empty?
|
|
98
|
+
return [] unless Native.available?
|
|
99
|
+
|
|
100
|
+
artifact = Artifact.from_json(JSON.generate(envelope), tables_dir: tables_dir)
|
|
101
|
+
default = envelope["default_entry"] || artifact.entries.first
|
|
102
|
+
grammars = {}
|
|
103
|
+
tests.filter_map do |test|
|
|
104
|
+
entry = test["entry"] || default
|
|
105
|
+
grammar_json = (grammars[entry] ||= JSON.generate(artifact.entry(entry)["grammar"]))
|
|
106
|
+
artifact.run_test_list(
|
|
107
|
+
[test],
|
|
108
|
+
parse_with: ->(input) { Native.parse_json_grammar(grammar_json, input) },
|
|
109
|
+
)
|
|
110
|
+
end.flatten
|
|
111
|
+
end
|
|
112
|
+
|
|
82
113
|
def resolve_custom(name)
|
|
83
114
|
class_name = @document.customs.fetch(name)
|
|
84
115
|
klass = Object.const_get(class_name)
|
|
@@ -314,22 +345,43 @@ module Parsanol
|
|
|
314
345
|
def run_tests
|
|
315
346
|
return [] if @document.tests.empty?
|
|
316
347
|
|
|
317
|
-
default_entry = resolve_default_entry
|
|
318
348
|
view = test_view
|
|
319
349
|
@document.tests.filter_map do |test|
|
|
320
350
|
entry_name = test.entry || default_entry
|
|
321
351
|
rule = @document.entries.fetch(entry_name)
|
|
322
352
|
begin
|
|
323
353
|
shape = atom_for(rule).parse(test.input)
|
|
324
|
-
|
|
354
|
+
case test.kind
|
|
355
|
+
when :reject
|
|
325
356
|
"test #{test.input.inspect}: expected the input to be rejected"
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
357
|
+
when :example
|
|
358
|
+
bindings = binding_list(rule)
|
|
359
|
+
if bindings.empty?
|
|
360
|
+
raise CompileError,
|
|
361
|
+
"example tests need a bindings section (use `capture` " \
|
|
362
|
+
"for raw-capture assertions)"
|
|
363
|
+
end
|
|
364
|
+
bound = Bindings.apply(view, { "bindings" => bindings }, shape)
|
|
365
|
+
mismatched = test.expect.reject do |key, value|
|
|
366
|
+
bound.key?(key) && Bindings.canonical(bound[key]) == Bindings.canonical(value)
|
|
367
|
+
end
|
|
329
368
|
next if mismatched.empty?
|
|
330
369
|
|
|
331
370
|
"test #{test.input.inspect}: expected captures " \
|
|
332
371
|
"#{mismatched.transform_values(&:inspect).inspect}, got #{bound.inspect}"
|
|
372
|
+
when :capture
|
|
373
|
+
captures = Bindings.collect_captures(shape)
|
|
374
|
+
mismatched = test.expect.reject do |key, value|
|
|
375
|
+
actual = captures[key.to_sym]&.first
|
|
376
|
+
!actual.nil? && Bindings.canonical(actual) == Bindings.canonical(value)
|
|
377
|
+
end
|
|
378
|
+
next if mismatched.empty?
|
|
379
|
+
|
|
380
|
+
actuals = mismatched.keys.to_h do |key|
|
|
381
|
+
[key, Bindings.canonical(captures[key.to_sym]&.first)]
|
|
382
|
+
end
|
|
383
|
+
"test #{test.input.inspect}: expected captures " \
|
|
384
|
+
"#{mismatched.transform_values(&:inspect).inspect}, got #{actuals.inspect}"
|
|
333
385
|
end
|
|
334
386
|
rescue Parsanol::ParseFailed
|
|
335
387
|
unless test.kind == :reject
|
|
@@ -339,6 +391,13 @@ module Parsanol
|
|
|
339
391
|
end
|
|
340
392
|
end
|
|
341
393
|
|
|
394
|
+
# Only consulted when a test omits its entry: a multi-entry
|
|
395
|
+
# grammar whose tests are all explicit must compile (parsanol-
|
|
396
|
+
# ruby#216). Memoized so the raise happens once, on first need.
|
|
397
|
+
def default_entry
|
|
398
|
+
@default_entry ||= resolve_default_entry
|
|
399
|
+
end
|
|
400
|
+
|
|
342
401
|
def resolve_default_entry
|
|
343
402
|
own = @document.own_entries
|
|
344
403
|
return own.first if own && own.size == 1
|
|
@@ -5,7 +5,15 @@ module Parsanol
|
|
|
5
5
|
# Parsed PARG document: grammar rules plus the binding sections.
|
|
6
6
|
class Document
|
|
7
7
|
Binding = Struct.new(:capture, :path, :type, :card, :preprocess)
|
|
8
|
-
|
|
8
|
+
# `capture` = phase 1 (grammar layer): asserts the raw capture tree,
|
|
9
|
+
# no bindings section needed. `example` = phase 2 (bindings layer):
|
|
10
|
+
# asserts the bound, preprocessed attribute hash.
|
|
11
|
+
# `capture` = phase 1 (grammar layer): asserts the raw capture tree,
|
|
12
|
+
# no bindings section needed. `example` = phase 2 (bindings layer):
|
|
13
|
+
# asserts the bound, preprocessed attribute hash.
|
|
14
|
+
TEST_KINDS = %i[accept reject example capture].freeze
|
|
15
|
+
# Test kinds whose expectations are `{ key: value }` pairs.
|
|
16
|
+
PAIR_KINDS = %w[example capture].freeze
|
|
9
17
|
|
|
10
18
|
Test = Struct.new(:entry, :kind, :input, :expect)
|
|
11
19
|
|
data/lib/parsanol/parg/parser.rb
CHANGED
|
@@ -83,7 +83,7 @@ module Parsanol
|
|
|
83
83
|
kind = ident.value
|
|
84
84
|
input = unquote(expect(:str).value)
|
|
85
85
|
expect_pairs = {}
|
|
86
|
-
if kind
|
|
86
|
+
if Document::PAIR_KINDS.include?(kind) && peek&.type == :punct && peek.value == "{"
|
|
87
87
|
advance
|
|
88
88
|
skip_newlines
|
|
89
89
|
until peek&.type == :punct && peek.value == "}"
|
data/lib/parsanol/version.rb
CHANGED
data/lib/parsanol/vm.rb
CHANGED
|
@@ -146,7 +146,12 @@ module Parsanol
|
|
|
146
146
|
# The shared program's compile-time analysis (heavy-memo
|
|
147
147
|
# seeding) covered this structure; the fresh root inherits
|
|
148
148
|
# it so its first parse memoizes like the original's.
|
|
149
|
-
|
|
149
|
+
if @heavy_by_hash&.[](hash_key)
|
|
150
|
+
(@heavy ||= {}.compare_by_identity)[root] = true
|
|
151
|
+
trim_cache(@heavy)
|
|
152
|
+
end
|
|
153
|
+
cache[root] = shared
|
|
154
|
+
trim_cache(cache)
|
|
150
155
|
return shared
|
|
151
156
|
end
|
|
152
157
|
|
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.
|
|
4
|
+
version: 1.3.94
|
|
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-
|
|
11
|
+
date: 2026-10-18 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
|