parsanol 1.3.91-arm-linux → 1.3.93-arm-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 +0 -0
- data/lib/parsanol/native/parser.rb +46 -47
- data/lib/parsanol/parg/artifact.rb +51 -12
- data/lib/parsanol/parg/bindings.rb +13 -0
- data/lib/parsanol/parg/cli.rb +36 -2
- data/lib/parsanol/parg/compiler.rb +39 -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: 38e6903eee4c564df8586705d284a07671cbf0935ed4c7641e8b44015c4c510e
|
|
4
|
+
data.tar.gz: c6f1c81c282193fd596386c717e756aa35d87ffd22d5c3c1554ba3b09d4e0e02
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 94c5b1eb30a297566ca23877a53b1d63be7cff2fa04a92084fb1e9ff0102b238723fba755469daadb5d38b2113638cf0a02697f06a65671d04e631b2bd8647bf
|
|
7
|
+
data.tar.gz: ce29e977d953b08817a2f425f86090d4272cde2fc41e29d0d68b90571765e01951cdd3b90242671d0e9de018bc36f3aa8e52fc5359e5578a7b0ce8bd14554cab
|
|
@@ -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: 34333284 bytes
|
|
4
|
+
sha256: 46c18a5ba0af8926c012eb98c6417dfca64931a837629b317fb7c3cc430b9085
|
|
@@ -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: 34518452 bytes
|
|
4
|
+
sha256: 0752c79e340c4c722b106443ff1faf6ff6d6d7c5800570906d1c548585e06a58
|
|
@@ -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: 34512620 bytes
|
|
4
|
+
sha256: 9b0744875d02d5c1957b82cdf9249129ce62214696606b84886609243342e23f
|
|
@@ -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: 34522276 bytes
|
|
4
|
+
sha256: 834bad07a21ef4ccaeb47fbfaf85c567866142fb141f6d93205d5d9958e0c2f2
|
|
Binary file
|
|
@@ -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
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "digest"
|
|
3
4
|
require "json"
|
|
4
5
|
require "yaml"
|
|
5
6
|
|
|
@@ -11,20 +12,21 @@ module Parsanol
|
|
|
11
12
|
# consume; the embedded PARG source lets a Ruby runtime recompile the atom
|
|
12
13
|
# tree locally without any other dependency.
|
|
13
14
|
class Artifact
|
|
14
|
-
attr_reader :envelope, :path, :tables_dir
|
|
15
|
+
attr_reader :envelope, :path, :tables_dir, :imports_dir
|
|
15
16
|
|
|
16
|
-
def self.load(path, tables_dir: nil)
|
|
17
|
+
def self.load(path, tables_dir: nil, imports_dir: nil)
|
|
17
18
|
envelope = JSON.parse(PARG.read_utf8(path))
|
|
18
|
-
new(envelope, path, tables_dir || File.dirname(path)
|
|
19
|
+
new(envelope, path, tables_dir || File.dirname(path),
|
|
20
|
+
imports_dir: imports_dir)
|
|
19
21
|
end
|
|
20
22
|
|
|
21
|
-
def self.from_json(text, tables_dir: nil)
|
|
22
|
-
new(JSON.parse(text), nil, tables_dir)
|
|
23
|
+
def self.from_json(text, tables_dir: nil, imports_dir: nil)
|
|
24
|
+
new(JSON.parse(text), nil, tables_dir, imports_dir: imports_dir)
|
|
23
25
|
end
|
|
24
26
|
|
|
25
27
|
SUPPORTED_SHAPE = "parsanol-tree/v2"
|
|
26
28
|
|
|
27
|
-
def initialize(envelope, path, tables_dir)
|
|
29
|
+
def initialize(envelope, path, tables_dir, imports_dir: nil)
|
|
28
30
|
unless envelope["shape"] == SUPPORTED_SHAPE
|
|
29
31
|
raise ArtifactError,
|
|
30
32
|
"unsupported artifact shape #{envelope['shape'].inspect} " \
|
|
@@ -38,12 +40,30 @@ module Parsanol
|
|
|
38
40
|
@envelope = envelope
|
|
39
41
|
@path = path
|
|
40
42
|
@tables_dir = tables_dir
|
|
43
|
+
# parsanol-ruby#208: `use`-importing grammars need the imported
|
|
44
|
+
# sources when the pure-Ruby path recompiles the embedded source;
|
|
45
|
+
# in-memory artifacts (path nil) pass the directory explicitly.
|
|
46
|
+
@imports_dir = imports_dir
|
|
41
47
|
verify_checksum!
|
|
42
48
|
@compiler = nil
|
|
43
49
|
end
|
|
44
50
|
|
|
45
51
|
def version = envelope["version"]
|
|
46
52
|
|
|
53
|
+
# Freshness (parsanol-ruby#209): what source revision and compiler
|
|
54
|
+
# produced this artifact. `source_hash` is the SHA-256 of the .parg
|
|
55
|
+
# text at compile time; nil on artifacts baked before the field
|
|
56
|
+
# existed, so `fresh_against?` answers false rather than guessing.
|
|
57
|
+
def source_hash = envelope["source_hash"]
|
|
58
|
+
|
|
59
|
+
def compiler_version = envelope["compiler_version"]
|
|
60
|
+
|
|
61
|
+
def fresh_against?(source_text)
|
|
62
|
+
return false if source_hash.nil?
|
|
63
|
+
|
|
64
|
+
source_hash == Digest::SHA256.hexdigest(source_text.to_s)
|
|
65
|
+
end
|
|
66
|
+
|
|
47
67
|
def entries = envelope["entries"].keys
|
|
48
68
|
|
|
49
69
|
def entry(name)
|
|
@@ -137,16 +157,33 @@ module Parsanol
|
|
|
137
157
|
hash = test.is_a?(Document::Test) ? test_to_hash(test) : test
|
|
138
158
|
entry_name = hash["entry"] || entries.first
|
|
139
159
|
begin
|
|
140
|
-
|
|
141
|
-
|
|
160
|
+
shape = parse(entry_name, hash["input"], mode: mode)
|
|
161
|
+
case hash["kind"]
|
|
162
|
+
when "reject"
|
|
142
163
|
"test #{hash['input'].inspect}: expected the input to be rejected"
|
|
143
|
-
|
|
164
|
+
when "example"
|
|
165
|
+
bound = apply_bindings(entry_name, shape)
|
|
144
166
|
expect = hash["expect"].to_h { |key, value| [key.to_sym, value] }
|
|
145
|
-
mismatched = expect.reject
|
|
167
|
+
mismatched = expect.reject do |key, value|
|
|
168
|
+
bound.key?(key) && Bindings.canonical(bound[key]) == Bindings.canonical(value)
|
|
169
|
+
end
|
|
146
170
|
next if mismatched.empty?
|
|
147
171
|
|
|
148
172
|
"test #{hash['input'].inspect}: expected captures " \
|
|
149
173
|
"#{mismatched.transform_values(&:inspect).inspect}, got #{bound.inspect}"
|
|
174
|
+
when "capture"
|
|
175
|
+
captures = Bindings.collect_captures(shape)
|
|
176
|
+
mismatched = hash["expect"].reject do |key, value|
|
|
177
|
+
actual = captures[key.to_sym]&.first
|
|
178
|
+
!actual.nil? && Bindings.canonical(actual) == Bindings.canonical(value)
|
|
179
|
+
end
|
|
180
|
+
next if mismatched.empty?
|
|
181
|
+
|
|
182
|
+
actuals = mismatched.keys.to_h do |key|
|
|
183
|
+
[key, Bindings.canonical(captures[key.to_sym]&.first)]
|
|
184
|
+
end
|
|
185
|
+
"test #{hash['input'].inspect}: expected captures " \
|
|
186
|
+
"#{mismatched.transform_values(&:inspect).inspect}, got #{actuals.inspect}"
|
|
150
187
|
end
|
|
151
188
|
rescue Parsanol::ParseFailed
|
|
152
189
|
unless hash["kind"] == "reject"
|
|
@@ -263,8 +300,10 @@ module Parsanol
|
|
|
263
300
|
# without this the pure-Ruby path fails on artifacts whose
|
|
264
301
|
# native atom graphs inline everything. The imported sources
|
|
265
302
|
# sit beside the artifact (vendors ship them together).
|
|
266
|
-
|
|
267
|
-
|
|
303
|
+
dirs = [path ? File.dirname(path) : nil, @imports_dir]
|
|
304
|
+
.compact
|
|
305
|
+
.select { |dir| File.directory?(dir) }
|
|
306
|
+
Imports.merge!(document, dirs) unless dirs.empty?
|
|
268
307
|
Compiler.new(document, @tables_dir)
|
|
269
308
|
end
|
|
270
309
|
end
|
|
@@ -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]
|
data/lib/parsanol/parg/cli.rb
CHANGED
|
@@ -22,6 +22,10 @@ module Parsanol
|
|
|
22
22
|
test <file.parg|artifact.json> run the grammar's test section
|
|
23
23
|
parse <file> [--entry E] INPUT parse one input; prints the tree
|
|
24
24
|
and the bound captures
|
|
25
|
+
check --source x.parg --artifact y.json
|
|
26
|
+
verify the artifact was compiled
|
|
27
|
+
from that source (source hash +
|
|
28
|
+
compiler version); exit 1 stale
|
|
25
29
|
repl <file> [--entry E] interactive parse loop (blank
|
|
26
30
|
line quits, :quit exits)
|
|
27
31
|
doc <file> [rule] print rule documentation
|
|
@@ -53,6 +57,7 @@ module Parsanol
|
|
|
53
57
|
when "test" then test
|
|
54
58
|
when "schema" then schema
|
|
55
59
|
when "parse" then parse
|
|
60
|
+
when "check" then check
|
|
56
61
|
when "repl" then repl
|
|
57
62
|
when "doc" then doc
|
|
58
63
|
when "lsp" then Lsp.new.run && 0
|
|
@@ -88,6 +93,33 @@ module Parsanol
|
|
|
88
93
|
0
|
|
89
94
|
end
|
|
90
95
|
|
|
96
|
+
# Freshness (parsanol-ruby#209): verify an artifact was compiled
|
|
97
|
+
# from the given source. The stale-shipped-artifact incident class
|
|
98
|
+
# (an old artifact silently taking precedence over the corrected
|
|
99
|
+
# grammar) is undetectable without the recorded source hash.
|
|
100
|
+
# Exit 0 fresh, 1 stale or unrecorded.
|
|
101
|
+
def check
|
|
102
|
+
source = flag_value("--source")
|
|
103
|
+
artifact_path = flag_value("--artifact")
|
|
104
|
+
unless source && artifact_path && File.file?(source) && File.file?(artifact_path)
|
|
105
|
+
warn USAGE
|
|
106
|
+
return 1
|
|
107
|
+
end
|
|
108
|
+
artifact = Artifact.load(artifact_path)
|
|
109
|
+
if artifact.source_hash.nil?
|
|
110
|
+
puts "STALE? #{artifact_path}: no source hash recorded (pre-freshness artifact)"
|
|
111
|
+
return 1
|
|
112
|
+
end
|
|
113
|
+
if artifact.fresh_against?(PARG.read_utf8(source))
|
|
114
|
+
puts "FRESH #{artifact_path} == #{source} (compiler #{artifact.compiler_version})"
|
|
115
|
+
0
|
|
116
|
+
else
|
|
117
|
+
puts "STALE #{artifact_path} does not match #{source} " \
|
|
118
|
+
"(built from #{artifact.source_hash[0, 12]}… by #{artifact.compiler_version})"
|
|
119
|
+
1
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
91
123
|
def test
|
|
92
124
|
target = @argv.shift
|
|
93
125
|
if File.directory?(target)
|
|
@@ -200,11 +232,13 @@ module Parsanol
|
|
|
200
232
|
|
|
201
233
|
def artifact_for(file)
|
|
202
234
|
if file.end_with?(".json")
|
|
203
|
-
Parsanol::PARG::Artifact.load(file, tables_dir: @tables_dir || default_tables_dir(file)
|
|
235
|
+
Parsanol::PARG::Artifact.load(file, tables_dir: @tables_dir || default_tables_dir(file),
|
|
236
|
+
imports_dir: File.dirname(file))
|
|
204
237
|
else
|
|
205
238
|
artifact = compile_source(file)
|
|
206
239
|
Parsanol::PARG::Artifact.from_json(JSON.generate(artifact.envelope),
|
|
207
|
-
tables_dir: @tables_dir || default_tables_dir(file)
|
|
240
|
+
tables_dir: @tables_dir || default_tables_dir(file),
|
|
241
|
+
imports_dir: File.dirname(file))
|
|
208
242
|
end
|
|
209
243
|
end
|
|
210
244
|
|
|
@@ -314,22 +314,43 @@ module Parsanol
|
|
|
314
314
|
def run_tests
|
|
315
315
|
return [] if @document.tests.empty?
|
|
316
316
|
|
|
317
|
-
default_entry = resolve_default_entry
|
|
318
317
|
view = test_view
|
|
319
318
|
@document.tests.filter_map do |test|
|
|
320
319
|
entry_name = test.entry || default_entry
|
|
321
320
|
rule = @document.entries.fetch(entry_name)
|
|
322
321
|
begin
|
|
323
322
|
shape = atom_for(rule).parse(test.input)
|
|
324
|
-
|
|
323
|
+
case test.kind
|
|
324
|
+
when :reject
|
|
325
325
|
"test #{test.input.inspect}: expected the input to be rejected"
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
326
|
+
when :example
|
|
327
|
+
bindings = binding_list(rule)
|
|
328
|
+
if bindings.empty?
|
|
329
|
+
raise CompileError,
|
|
330
|
+
"example tests need a bindings section (use `capture` " \
|
|
331
|
+
"for raw-capture assertions)"
|
|
332
|
+
end
|
|
333
|
+
bound = Bindings.apply(view, { "bindings" => bindings }, shape)
|
|
334
|
+
mismatched = test.expect.reject do |key, value|
|
|
335
|
+
bound.key?(key) && Bindings.canonical(bound[key]) == Bindings.canonical(value)
|
|
336
|
+
end
|
|
329
337
|
next if mismatched.empty?
|
|
330
338
|
|
|
331
339
|
"test #{test.input.inspect}: expected captures " \
|
|
332
340
|
"#{mismatched.transform_values(&:inspect).inspect}, got #{bound.inspect}"
|
|
341
|
+
when :capture
|
|
342
|
+
captures = Bindings.collect_captures(shape)
|
|
343
|
+
mismatched = test.expect.reject do |key, value|
|
|
344
|
+
actual = captures[key.to_sym]&.first
|
|
345
|
+
!actual.nil? && Bindings.canonical(actual) == Bindings.canonical(value)
|
|
346
|
+
end
|
|
347
|
+
next if mismatched.empty?
|
|
348
|
+
|
|
349
|
+
actuals = mismatched.keys.to_h do |key|
|
|
350
|
+
[key, Bindings.canonical(captures[key.to_sym]&.first)]
|
|
351
|
+
end
|
|
352
|
+
"test #{test.input.inspect}: expected captures " \
|
|
353
|
+
"#{mismatched.transform_values(&:inspect).inspect}, got #{actuals.inspect}"
|
|
333
354
|
end
|
|
334
355
|
rescue Parsanol::ParseFailed
|
|
335
356
|
unless test.kind == :reject
|
|
@@ -339,6 +360,13 @@ module Parsanol
|
|
|
339
360
|
end
|
|
340
361
|
end
|
|
341
362
|
|
|
363
|
+
# Only consulted when a test omits its entry: a multi-entry
|
|
364
|
+
# grammar whose tests are all explicit must compile (parsanol-
|
|
365
|
+
# ruby#216). Memoized so the raise happens once, on first need.
|
|
366
|
+
def default_entry
|
|
367
|
+
@default_entry ||= resolve_default_entry
|
|
368
|
+
end
|
|
369
|
+
|
|
342
370
|
def resolve_default_entry
|
|
343
371
|
own = @document.own_entries
|
|
344
372
|
return own.first if own && own.size == 1
|
|
@@ -758,6 +786,12 @@ module Parsanol
|
|
|
758
786
|
end,
|
|
759
787
|
"docs" => @document.docs,
|
|
760
788
|
"source" => @document.source,
|
|
789
|
+
# Freshness (parsanol-ruby#209): what the artifact was compiled
|
|
790
|
+
# from and with, so loaders can detect a stale artifact and fall
|
|
791
|
+
# back to a reference implementation instead of failing obscurely
|
|
792
|
+
# at parse time. Checksummed like the rest of the envelope.
|
|
793
|
+
"source_hash" => Digest::SHA256.hexdigest(@document.source.to_s),
|
|
794
|
+
"compiler_version" => Parsanol::VERSION,
|
|
761
795
|
}
|
|
762
796
|
if @document.skip_declaration
|
|
763
797
|
# parsanol-ruby#134: declared trivia rule. Injection is baked
|
|
@@ -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.93
|
|
5
5
|
platform: arm-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-16 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
|