parsanol 1.3.56-arm-linux → 1.3.57-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/exe/parsanol +7 -0
- data/lib/parsanol/3.2/parsanol_native.so +0 -0
- data/lib/parsanol/3.3/parsanol_native.so +0 -0
- data/lib/parsanol/3.4/parsanol_native.so +0 -0
- data/lib/parsanol/4.0/parsanol_native.so +0 -0
- data/lib/parsanol/native/libparsanol.so +0 -0
- data/lib/parsanol/native/serializer.rb +5 -2
- data/lib/parsanol/parg/artifact.rb +213 -0
- data/lib/parsanol/parg/authoring.rb +110 -0
- data/lib/parsanol/parg/bindings.rb +129 -0
- data/lib/parsanol/parg/cli.rb +251 -0
- data/lib/parsanol/parg/compiler.rb +465 -0
- data/lib/parsanol/parg/derive.rb +28 -0
- data/lib/parsanol/parg/document.rb +69 -0
- data/lib/parsanol/parg/error.rb +18 -0
- data/lib/parsanol/parg/frontend.rb +173 -0
- data/lib/parsanol/parg/import.rb +34 -0
- data/lib/parsanol/parg/importers/abnf.rb +299 -0
- data/lib/parsanol/parg/importers/ebnf.rb +201 -0
- data/lib/parsanol/parg/importers/pest.rb +314 -0
- data/lib/parsanol/parg/imports.rb +87 -0
- data/lib/parsanol/parg/lexer.rb +72 -0
- data/lib/parsanol/parg/lints.rb +171 -0
- data/lib/parsanol/parg/lsp.rb +208 -0
- data/lib/parsanol/parg/lutaml.rb +67 -0
- data/lib/parsanol/parg/node.rb +20 -0
- data/lib/parsanol/parg/parser.rb +458 -0
- data/lib/parsanol/parg/preprocess.rb +36 -0
- data/lib/parsanol/parg/render.rb +44 -0
- data/lib/parsanol/parg/selfhost.rb +49 -0
- data/lib/parsanol/parg/visitor.rb +64 -0
- data/lib/parsanol/parg.rb +38 -0
- data/lib/parsanol/version.rb +1 -1
- data/lib/parsanol/vm.rb +7 -5
- data/lib/parsanol.rb +3 -0
- data/parsanol.gemspec +3 -1
- metadata +31 -4
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
require "json"
|
|
5
|
+
require "yaml"
|
|
6
|
+
|
|
7
|
+
module Parsanol
|
|
8
|
+
module PARG
|
|
9
|
+
# Compiles a Document to executable atoms and an artifact envelope.
|
|
10
|
+
#
|
|
11
|
+
# Responsibilities:
|
|
12
|
+
# - build the parsanol atom tree (lazy Entity web; recursive grammars OK)
|
|
13
|
+
# - reject left recursion at compile time (would hang every PEG engine)
|
|
14
|
+
# - reject shadowed alternatives; warn on order-dependent first-set overlap
|
|
15
|
+
# - expand `alt from_table` data-driven alternatives (longest-first)
|
|
16
|
+
# - emit the artifact envelope: portable Grammar JSON (the same format the
|
|
17
|
+
# native engines register), bindings, preprocess steps, checksum
|
|
18
|
+
class Compiler
|
|
19
|
+
EPS = :eps
|
|
20
|
+
ANY = :any
|
|
21
|
+
|
|
22
|
+
Result = Struct.new(:atoms, :warnings, :envelope)
|
|
23
|
+
|
|
24
|
+
def self.compile(document, tables_dir: nil)
|
|
25
|
+
new(document, tables_dir).compile
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Canonical checksum over an envelope (everything except the checksum
|
|
29
|
+
# itself). Used both when building an artifact and when verifying one.
|
|
30
|
+
def self.checksum(envelope)
|
|
31
|
+
payload = envelope.except("checksum")
|
|
32
|
+
"sha256:#{Digest::SHA256.hexdigest(JSON.generate(canonicalize(payload)))}"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def self.canonicalize(obj)
|
|
36
|
+
case obj
|
|
37
|
+
when Hash then obj.sort.to_h.transform_values { |value| canonicalize(value) }
|
|
38
|
+
when Array then obj.map { |value| canonicalize(value) }
|
|
39
|
+
else obj
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def initialize(document, tables_dir)
|
|
44
|
+
@document = document
|
|
45
|
+
@tables_dir = tables_dir
|
|
46
|
+
@atom_cache = {}
|
|
47
|
+
@first_cache = {}.compare_by_identity
|
|
48
|
+
@ref_stack = []
|
|
49
|
+
@tables = {}
|
|
50
|
+
@warnings = []
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def compile
|
|
54
|
+
@document.rules.each_key { |name| atom_for(name) }
|
|
55
|
+
errors = Lints.errors(@document, self)
|
|
56
|
+
raise CompileError, errors.join("\n") unless errors.empty?
|
|
57
|
+
|
|
58
|
+
@warnings = Lints.warnings(@document, self)
|
|
59
|
+
failures = run_tests
|
|
60
|
+
raise CompileError, failures.join("\n") unless failures.empty?
|
|
61
|
+
|
|
62
|
+
envelope = build_envelope
|
|
63
|
+
Result.new(@atom_cache, @warnings, envelope)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def atom_for(name)
|
|
67
|
+
@atom_cache[name] ||= build_atom(fetch_rule(name))
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def record_warning(message)
|
|
71
|
+
@warnings << message
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def nullable?(node)
|
|
75
|
+
first_set(node).include?(EPS)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def first_set(node)
|
|
79
|
+
@first_cache[node] ||= compute_first(node)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def find_left_cycle(name)
|
|
83
|
+
find_left_cycle_from(name, [], {})
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
private
|
|
87
|
+
|
|
88
|
+
# In-file grammar tests run at compile time: the build fails when an
|
|
89
|
+
# accept/example test does not parse or an example's expected capture
|
|
90
|
+
# pairs do not match the bound result.
|
|
91
|
+
def run_tests
|
|
92
|
+
return [] if @document.tests.empty?
|
|
93
|
+
|
|
94
|
+
default_entry = resolve_default_entry
|
|
95
|
+
view = test_view
|
|
96
|
+
@document.tests.filter_map do |test|
|
|
97
|
+
entry_name = test.entry || default_entry
|
|
98
|
+
rule = @document.entries.fetch(entry_name)
|
|
99
|
+
begin
|
|
100
|
+
shape = atom_for(rule).parse(test.input)
|
|
101
|
+
if test.kind == :reject
|
|
102
|
+
"test #{test.input.inspect}: expected the input to be rejected"
|
|
103
|
+
elsif test.kind == :example
|
|
104
|
+
bound = Bindings.apply(view, { "bindings" => binding_list(rule) }, shape)
|
|
105
|
+
mismatched = test.expect.reject { |key, value| bound.key?(key) && bound[key] == value }
|
|
106
|
+
next if mismatched.empty?
|
|
107
|
+
|
|
108
|
+
"test #{test.input.inspect}: expected captures " \
|
|
109
|
+
"#{mismatched.transform_values(&:inspect).inspect}, got #{bound.inspect}"
|
|
110
|
+
end
|
|
111
|
+
rescue Parsanol::ParseFailed
|
|
112
|
+
unless test.kind == :reject
|
|
113
|
+
"test #{test.input.inspect}: expected the input to parse"
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def resolve_default_entry
|
|
120
|
+
own = @document.own_entries
|
|
121
|
+
return own.first if own && own.size == 1
|
|
122
|
+
|
|
123
|
+
entries = @document.entries.keys
|
|
124
|
+
return entries.first if entries.size == 1
|
|
125
|
+
|
|
126
|
+
raise CompileError,
|
|
127
|
+
"tests need an explicit entry (grammar has #{entries.size})"
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Bindings.apply needs the envelope-shaped preprocess/tables surface;
|
|
131
|
+
# at compile time the document and loaded tables play that role.
|
|
132
|
+
def test_view
|
|
133
|
+
document = @document
|
|
134
|
+
rows = ->(name) { table_rows(name) }
|
|
135
|
+
@test_view ||= Object.new.tap do |view|
|
|
136
|
+
view.define_singleton_method(:envelope) do
|
|
137
|
+
{ "preprocess" => document.preprocess }
|
|
138
|
+
end
|
|
139
|
+
view.define_singleton_method(:table_rows) { |name| rows.call(name) }
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def binding_list(rule)
|
|
144
|
+
@document.bindings[rule].to_a.map do |binding|
|
|
145
|
+
{
|
|
146
|
+
"capture" => binding.capture,
|
|
147
|
+
"path" => binding.path,
|
|
148
|
+
"type" => binding.type,
|
|
149
|
+
"card" => binding.card,
|
|
150
|
+
"preprocess" => binding.preprocess,
|
|
151
|
+
}
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def fetch_rule(name)
|
|
156
|
+
@document.rules[name] || raise(CompileError, "unknown rule #{name.inspect}")
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def build_atom(node)
|
|
160
|
+
case node.kind
|
|
161
|
+
when :lit
|
|
162
|
+
if node.a.empty?
|
|
163
|
+
raise CompileError,
|
|
164
|
+
"empty string literal: zero-width matches are not " \
|
|
165
|
+
"expressible — capture it (\"\" as name) to use it as " \
|
|
166
|
+
"a marker, or use [ … ] for optional content"
|
|
167
|
+
end
|
|
168
|
+
if node.b
|
|
169
|
+
Atoms::Re.new("(?i:#{Regexp.escape(node.a)})")
|
|
170
|
+
else
|
|
171
|
+
Atoms::Str.new(node.a)
|
|
172
|
+
end
|
|
173
|
+
when :class then Atoms::Re.new(class_pattern(node.a))
|
|
174
|
+
when :seq
|
|
175
|
+
atoms = node.a.map { |child| build_atom(child) }
|
|
176
|
+
atoms.length == 1 ? atoms.first : Atoms::Sequence.new(*atoms)
|
|
177
|
+
when :alt
|
|
178
|
+
Atoms::Alternative.new(*node.a.map { |child| build_atom(child) })
|
|
179
|
+
when :rep
|
|
180
|
+
Atoms::Repetition.new(build_atom(node.a), node.b, node.c)
|
|
181
|
+
when :opt then Atoms::Repetition.new(build_atom(node.a), 0, 1, :maybe)
|
|
182
|
+
when :pred then Atoms::Lookahead.new(build_atom(node.b), node.a)
|
|
183
|
+
when :cap
|
|
184
|
+
# "" as name is the PARG spelling of Ruby parslet's
|
|
185
|
+
# str("").as(:name): an always-succeeding zero-width marker
|
|
186
|
+
# that records presence in the tree (oiml space_before_lang).
|
|
187
|
+
inner = node.b
|
|
188
|
+
if inner.kind == :lit && inner.a.empty?
|
|
189
|
+
return Atoms::Named.new(Atoms::Str.new(""), node.a.to_sym)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
Atoms::Named.new(build_atom(node.b), node.a.to_sym)
|
|
193
|
+
when :ref then Atoms::Entity.new(node.a) { atom_for(node.a) }
|
|
194
|
+
when :table
|
|
195
|
+
values = table_column(node.a, node.b)
|
|
196
|
+
Atoms::Alternative.new(*values.map { |value| Atoms::Str.new(value) })
|
|
197
|
+
else
|
|
198
|
+
raise CompileError, "unknown node kind #{node.kind.inspect}"
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def class_pattern(ranges)
|
|
203
|
+
body = ranges.map do |lo, hi|
|
|
204
|
+
if lo == hi
|
|
205
|
+
escape_codepoint(lo)
|
|
206
|
+
elsif hi - lo + 1 > 2
|
|
207
|
+
"#{endpoint(lo)}-#{endpoint(hi)}"
|
|
208
|
+
else
|
|
209
|
+
lo.upto(hi).map { |code| escape_codepoint(code) }.join
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
"[#{body.join}]"
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# Range endpoints always use explicit escapes so the range operator
|
|
216
|
+
# can never be ambiguous with an escaped literal.
|
|
217
|
+
def endpoint(code)
|
|
218
|
+
# Above the BMP the four-hex \uXXXX form cannot express the
|
|
219
|
+
# codepoint; both engines accept the braced \u{...} form.
|
|
220
|
+
return format("\\u{%x}", code) if code > 0xFFFF
|
|
221
|
+
|
|
222
|
+
code < 0x7F ? format("\\x%02x", code) : format("\\u%04x", code)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def escape_codepoint(code)
|
|
226
|
+
return format("\\x%02x", code) if code < 0x20 || code == 0x7F
|
|
227
|
+
return format("\\u{%x}", code) if code > 0xFFFF
|
|
228
|
+
return format("\\u%04x", code) if code > 0x7E
|
|
229
|
+
return "\\#{code.chr}" if ["\\", "]", "[", "^", "-"].include?(code.chr)
|
|
230
|
+
|
|
231
|
+
code.chr
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def table_column(table, column)
|
|
235
|
+
rows = table_rows(table)
|
|
236
|
+
values = rows.filter_map { |row| row[column] }.map(&:to_s).reject(&:empty?)
|
|
237
|
+
if values.empty?
|
|
238
|
+
raise CompileError,
|
|
239
|
+
"table #{table.inspect} has no column #{column.inspect}"
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
values.uniq.sort_by { |value| [-value.length, value] }
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def table_rows(name)
|
|
246
|
+
return @tables[name] if @tables.key?(name)
|
|
247
|
+
|
|
248
|
+
path = %w[.yaml .yml .json]
|
|
249
|
+
.map { |ext| File.join(@tables_dir.to_s, "#{name}#{ext}") }
|
|
250
|
+
.find { |candidate| File.file?(candidate) }
|
|
251
|
+
if path.nil?
|
|
252
|
+
raise CompileError,
|
|
253
|
+
"table #{name.inspect} not found under #{@tables_dir.inspect}"
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
raw = if path.end_with?(".json")
|
|
257
|
+
JSON.parse(File.read(path))
|
|
258
|
+
else
|
|
259
|
+
YAML.safe_load_file(path, aliases: true)
|
|
260
|
+
end
|
|
261
|
+
rows = case raw
|
|
262
|
+
when Hash
|
|
263
|
+
raw.map do |key, value|
|
|
264
|
+
{ "name" => key.to_s }.merge(value.to_h.transform_keys(&:to_s))
|
|
265
|
+
end
|
|
266
|
+
when Array then raw.map { |row| row.to_h.transform_keys(&:to_s) }
|
|
267
|
+
else
|
|
268
|
+
raise CompileError, "table #{name.inspect} must be a map or array"
|
|
269
|
+
end
|
|
270
|
+
@tables[name] = rows
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# ---- analysis ------------------------------------------------------
|
|
274
|
+
|
|
275
|
+
def lint
|
|
276
|
+
errors = []
|
|
277
|
+
@document.rules.each do |name, node|
|
|
278
|
+
if (cycle = find_left_cycle(name))
|
|
279
|
+
errors << "left recursion detected: #{cycle.join(' -> ')}"
|
|
280
|
+
end
|
|
281
|
+
errors.concat(check_alternatives(name, node)) if node.kind == :alt
|
|
282
|
+
end
|
|
283
|
+
errors
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def check_alternatives(name, node)
|
|
287
|
+
errors = []
|
|
288
|
+
branches = node.a
|
|
289
|
+
branches.each_with_index do |branch, index|
|
|
290
|
+
if nullable?(branch) && index < branches.length - 1
|
|
291
|
+
errors << "rule #{name}: branch #{index + 1} can match empty input " \
|
|
292
|
+
"and shadows all later branches"
|
|
293
|
+
end
|
|
294
|
+
branches[(index + 1)..].each_with_index do |later, j|
|
|
295
|
+
compare_branches(name, branch, index + 1, later, index + j + 2, errors)
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
errors
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
def find_left_cycle_from(name, path, visiting)
|
|
302
|
+
return path[(path.index(name))..] if path.include?(name)
|
|
303
|
+
return nil if visiting[name] == :done
|
|
304
|
+
|
|
305
|
+
visiting[name] = :active
|
|
306
|
+
leftmost_refs(fetch_rule(name)).each do |dep|
|
|
307
|
+
cycle = find_left_cycle_from(dep, path + [name], visiting)
|
|
308
|
+
return cycle if cycle
|
|
309
|
+
end
|
|
310
|
+
visiting[name] = :done
|
|
311
|
+
nil
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def leftmost_refs(node)
|
|
315
|
+
case node.kind
|
|
316
|
+
when :ref then [node.a]
|
|
317
|
+
when :seq
|
|
318
|
+
refs = []
|
|
319
|
+
node.a.each do |item|
|
|
320
|
+
refs.concat(leftmost_refs(item))
|
|
321
|
+
break unless nullable?(item)
|
|
322
|
+
end
|
|
323
|
+
refs.uniq
|
|
324
|
+
when :alt then node.a.flat_map { |branch| leftmost_refs(branch) }.uniq
|
|
325
|
+
when :rep, :opt then leftmost_refs(node.a)
|
|
326
|
+
when :pred, :cap then leftmost_refs(node.b)
|
|
327
|
+
else []
|
|
328
|
+
end
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
def compute_first(node)
|
|
332
|
+
case node.kind
|
|
333
|
+
when :lit
|
|
334
|
+
# The zero-width marker ("" as name) consumes nothing: its
|
|
335
|
+
# first set is empty, which also marks the node nullable.
|
|
336
|
+
return Set.new if node.a.empty?
|
|
337
|
+
|
|
338
|
+
set = Set.new(node.a[0].bytes)
|
|
339
|
+
set += node.a[0].upcase.bytes + node.a[0].downcase.bytes if node.b
|
|
340
|
+
set
|
|
341
|
+
when :class
|
|
342
|
+
total = node.a.inject(0) { |acc, (lo, hi)| acc + (hi - lo + 1) }
|
|
343
|
+
if total > 256
|
|
344
|
+
Set.new([ANY])
|
|
345
|
+
else
|
|
346
|
+
node.a.inject(Set.new) do |acc, (lo, hi)|
|
|
347
|
+
acc + lo.upto(hi).to_set
|
|
348
|
+
end
|
|
349
|
+
end
|
|
350
|
+
when :seq
|
|
351
|
+
set = Set.new
|
|
352
|
+
node.a.each do |item|
|
|
353
|
+
item_first = first_set(item)
|
|
354
|
+
set += item_first - [EPS]
|
|
355
|
+
break if item_first.include?(ANY) || !item_first.include?(EPS)
|
|
356
|
+
end
|
|
357
|
+
set << EPS if node.a.all? { |item| first_set(item).include?(EPS) }
|
|
358
|
+
set
|
|
359
|
+
when :alt
|
|
360
|
+
node.a.inject(Set.new) { |acc, branch| acc + first_set(branch) }
|
|
361
|
+
when :rep
|
|
362
|
+
set = first_set(node.a).dup
|
|
363
|
+
set << EPS if node.b.zero?
|
|
364
|
+
set
|
|
365
|
+
when :opt then first_set(node.a) + [EPS]
|
|
366
|
+
when :pred
|
|
367
|
+
node.a ? first_set(node.b) : Set.new([ANY])
|
|
368
|
+
when :cap then first_set(node.b)
|
|
369
|
+
when :ref
|
|
370
|
+
if @ref_stack.include?(node.a)
|
|
371
|
+
Set.new([ANY, EPS])
|
|
372
|
+
else
|
|
373
|
+
@ref_stack << node.a
|
|
374
|
+
begin
|
|
375
|
+
first_set(fetch_rule(node.a))
|
|
376
|
+
ensure
|
|
377
|
+
@ref_stack.pop
|
|
378
|
+
end
|
|
379
|
+
end
|
|
380
|
+
when :table
|
|
381
|
+
values = begin
|
|
382
|
+
table_column(node.a, node.b)
|
|
383
|
+
rescue CompileError
|
|
384
|
+
[]
|
|
385
|
+
end
|
|
386
|
+
values.inject(Set.new) { |acc, value| acc + value[0].bytes }
|
|
387
|
+
else
|
|
388
|
+
Set.new([ANY])
|
|
389
|
+
end
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
# ---- artifact envelope ----------------------------------------------
|
|
393
|
+
|
|
394
|
+
def build_envelope
|
|
395
|
+
entries = @document.entries.to_h do |name, rule|
|
|
396
|
+
[name, entry_envelope(rule)]
|
|
397
|
+
end
|
|
398
|
+
envelope = {
|
|
399
|
+
"version" => @document.version,
|
|
400
|
+
"grammar" => @document.grammar_name,
|
|
401
|
+
"shape" => "parsanol-tree/v2",
|
|
402
|
+
"binding_version" => 1,
|
|
403
|
+
"entries" => entries,
|
|
404
|
+
"preprocess" => @document.preprocess,
|
|
405
|
+
"tables" => table_manifest,
|
|
406
|
+
"lint" => { "order_warnings" => @warnings.uniq },
|
|
407
|
+
"default_entry" => begin
|
|
408
|
+
resolve_default_entry
|
|
409
|
+
rescue StandardError
|
|
410
|
+
nil
|
|
411
|
+
end,
|
|
412
|
+
"render" => @document.render,
|
|
413
|
+
"derive" => @document.derive,
|
|
414
|
+
"tests" => @document.tests.map do |test|
|
|
415
|
+
{
|
|
416
|
+
# Baked explicitly: JSON object key order is not preserved by
|
|
417
|
+
# every engine, so consumers cannot rederive the author's
|
|
418
|
+
# default entry from the entries map.
|
|
419
|
+
"entry" => test.entry || (@document.own_entries.first if @document.own_entries.size == 1),
|
|
420
|
+
"kind" => test.kind.to_s,
|
|
421
|
+
"input" => test.input,
|
|
422
|
+
"expect" => test.expect.transform_keys(&:to_s),
|
|
423
|
+
}
|
|
424
|
+
end,
|
|
425
|
+
"docs" => @document.docs,
|
|
426
|
+
"source" => @document.source,
|
|
427
|
+
}
|
|
428
|
+
envelope["checksum"] = Compiler.checksum(envelope)
|
|
429
|
+
envelope
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
def entry_envelope(rule)
|
|
433
|
+
{
|
|
434
|
+
"root" => rule,
|
|
435
|
+
"grammar" => JSON.parse(portable_json(rule)),
|
|
436
|
+
"bindings" => binding_list(rule),
|
|
437
|
+
}
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
def portable_json(rule)
|
|
441
|
+
Parsanol::Native::Parser.serialize_grammar(atom_for(rule))
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
# Tables embed their resolved rows in the envelope (self-contained
|
|
445
|
+
# artifacts): every engine - Ruby, Rust, wasm, TS - reads rows from
|
|
446
|
+
# the artifact; no engine touches the filesystem, and the rows are
|
|
447
|
+
# covered by the checksum.
|
|
448
|
+
def table_manifest
|
|
449
|
+
@document.rules.each_value { |node| collect_tables(node) }
|
|
450
|
+
@tables.keys.to_h do |name|
|
|
451
|
+
[name, { "file" => "#{name}.yaml", "rows" => table_rows(name) }]
|
|
452
|
+
end
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
def collect_tables(node)
|
|
456
|
+
case node.kind
|
|
457
|
+
when :table then table_rows(node.a)
|
|
458
|
+
when :seq, :alt then node.a.each { |child| collect_tables(child) }
|
|
459
|
+
when :rep, :opt then collect_tables(node.a)
|
|
460
|
+
when :pred, :cap then collect_tables(node.b)
|
|
461
|
+
end
|
|
462
|
+
end
|
|
463
|
+
end
|
|
464
|
+
end
|
|
465
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Parsanol
|
|
4
|
+
module PARG
|
|
5
|
+
# F6 v1: derive specs — named field-composition templates
|
|
6
|
+
# ("urn:{publisher}:{number}") evaluated against a bound attribute
|
|
7
|
+
# map. {field} interpolates the bound value; missing fields render
|
|
8
|
+
# empty. Byte-identical across engines by construction.
|
|
9
|
+
module Derive
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def apply(derive, name, bound)
|
|
13
|
+
template = derive.fetch(name) do
|
|
14
|
+
raise ArtifactError, "derive spec #{name.inspect} not declared"
|
|
15
|
+
end
|
|
16
|
+
evaluate(template, bound)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def evaluate(template, bound)
|
|
20
|
+
template.gsub(/\{(\w+)\}/) do
|
|
21
|
+
field = Regexp.last_match(1)
|
|
22
|
+
value = bound[field.to_s] || bound[field.to_sym]
|
|
23
|
+
value.nil? ? "" : value.to_s
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Parsanol
|
|
4
|
+
module PARG
|
|
5
|
+
# Parsed PARG document: grammar rules plus the binding sections.
|
|
6
|
+
class Document
|
|
7
|
+
Binding = Struct.new(:capture, :path, :type, :card, :preprocess)
|
|
8
|
+
TEST_KINDS = %i[accept reject example].freeze
|
|
9
|
+
|
|
10
|
+
Test = Struct.new(:entry, :kind, :input, :expect)
|
|
11
|
+
|
|
12
|
+
attr_accessor :grammar_name, :version, :source, :uses
|
|
13
|
+
attr_reader :rules, :bindings, :preprocess, :entries, :docs, :tests, :render, :derive
|
|
14
|
+
# Entries authored in this file (pre-import); nil-entry tests resolve
|
|
15
|
+
# against these first.
|
|
16
|
+
attr_accessor :own_entries
|
|
17
|
+
|
|
18
|
+
def initialize
|
|
19
|
+
@grammar_name = nil
|
|
20
|
+
@version = "0.0.0"
|
|
21
|
+
@source = nil
|
|
22
|
+
@rules = {}
|
|
23
|
+
@render = {}
|
|
24
|
+
@derive = {}
|
|
25
|
+
@bindings = {}
|
|
26
|
+
@preprocess = {}
|
|
27
|
+
@entries = {}
|
|
28
|
+
@docs = {}
|
|
29
|
+
@tests = []
|
|
30
|
+
@uses = []
|
|
31
|
+
@own_entries = nil
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def validate!
|
|
35
|
+
entries.each do |name, rule|
|
|
36
|
+
next if rules.key?(rule)
|
|
37
|
+
|
|
38
|
+
raise ParseError,
|
|
39
|
+
"entry #{name.inspect} references unknown rule #{rule.inspect}"
|
|
40
|
+
end
|
|
41
|
+
bindings.each_key do |rule|
|
|
42
|
+
next if rules.key?(rule)
|
|
43
|
+
|
|
44
|
+
raise ParseError,
|
|
45
|
+
"bindings reference unknown rule #{rule.inspect}"
|
|
46
|
+
end
|
|
47
|
+
known_steps = preprocess.keys
|
|
48
|
+
bindings.each_value do |list|
|
|
49
|
+
list.each do |binding|
|
|
50
|
+
next if binding.preprocess.nil? || known_steps.include?(binding.preprocess)
|
|
51
|
+
|
|
52
|
+
raise ParseError,
|
|
53
|
+
"binding #{binding.capture.inspect} references unknown " \
|
|
54
|
+
"preprocess step #{binding.preprocess.inspect}"
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
tests.each do |test|
|
|
58
|
+
unless TEST_KINDS.include?(test.kind)
|
|
59
|
+
raise ParseError, "unknown test kind #{test.kind.inspect}"
|
|
60
|
+
end
|
|
61
|
+
next if test.entry.nil? || entries.key?(test.entry)
|
|
62
|
+
|
|
63
|
+
raise ParseError,
|
|
64
|
+
"test references unknown entry #{test.entry.inspect}"
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Parsanol
|
|
4
|
+
module PARG
|
|
5
|
+
# Base class for all PARG errors.
|
|
6
|
+
class Error < StandardError; end
|
|
7
|
+
|
|
8
|
+
# Raised when .parg source text cannot be tokenized or parsed.
|
|
9
|
+
class ParseError < Error; end
|
|
10
|
+
|
|
11
|
+
# Raised when a parsed document is semantically invalid: left recursion,
|
|
12
|
+
# unknown rule references, shadowed alternatives, missing tables.
|
|
13
|
+
class CompileError < Error; end
|
|
14
|
+
|
|
15
|
+
# Raised when an artifact envelope is corrupt or its checksum mismatches.
|
|
16
|
+
class ArtifactError < Error; end
|
|
17
|
+
end
|
|
18
|
+
end
|