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.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/exe/parsanol +7 -0
  3. data/lib/parsanol/3.2/parsanol_native.so +0 -0
  4. data/lib/parsanol/3.3/parsanol_native.so +0 -0
  5. data/lib/parsanol/3.4/parsanol_native.so +0 -0
  6. data/lib/parsanol/4.0/parsanol_native.so +0 -0
  7. data/lib/parsanol/native/libparsanol.so +0 -0
  8. data/lib/parsanol/native/serializer.rb +5 -2
  9. data/lib/parsanol/parg/artifact.rb +213 -0
  10. data/lib/parsanol/parg/authoring.rb +110 -0
  11. data/lib/parsanol/parg/bindings.rb +129 -0
  12. data/lib/parsanol/parg/cli.rb +251 -0
  13. data/lib/parsanol/parg/compiler.rb +465 -0
  14. data/lib/parsanol/parg/derive.rb +28 -0
  15. data/lib/parsanol/parg/document.rb +69 -0
  16. data/lib/parsanol/parg/error.rb +18 -0
  17. data/lib/parsanol/parg/frontend.rb +173 -0
  18. data/lib/parsanol/parg/import.rb +34 -0
  19. data/lib/parsanol/parg/importers/abnf.rb +299 -0
  20. data/lib/parsanol/parg/importers/ebnf.rb +201 -0
  21. data/lib/parsanol/parg/importers/pest.rb +314 -0
  22. data/lib/parsanol/parg/imports.rb +87 -0
  23. data/lib/parsanol/parg/lexer.rb +72 -0
  24. data/lib/parsanol/parg/lints.rb +171 -0
  25. data/lib/parsanol/parg/lsp.rb +208 -0
  26. data/lib/parsanol/parg/lutaml.rb +67 -0
  27. data/lib/parsanol/parg/node.rb +20 -0
  28. data/lib/parsanol/parg/parser.rb +458 -0
  29. data/lib/parsanol/parg/preprocess.rb +36 -0
  30. data/lib/parsanol/parg/render.rb +44 -0
  31. data/lib/parsanol/parg/selfhost.rb +49 -0
  32. data/lib/parsanol/parg/visitor.rb +64 -0
  33. data/lib/parsanol/parg.rb +38 -0
  34. data/lib/parsanol/version.rb +1 -1
  35. data/lib/parsanol/vm.rb +7 -5
  36. data/lib/parsanol.rb +3 -0
  37. data/parsanol.gemspec +3 -1
  38. 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