parsanol 1.3.54-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/atoms/base.rb +1 -1
- data/lib/parsanol/atoms/cut.rb +1 -1
- data/lib/parsanol/atoms/repetition.rb +13 -2
- data/lib/parsanol/cause.rb +1 -1
- data/lib/parsanol/error_reporter/contextual.rb +1 -1
- data/lib/parsanol/lazy_result.rb +12 -3
- data/lib/parsanol/native/libparsanol.so +0 -0
- data/lib/parsanol/native/serializer.rb +5 -2
- data/lib/parsanol/native/transformer.rb +12 -8
- data/lib/parsanol/native.rb +4 -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/parser.rb +1 -1
- data/lib/parsanol/pattern.rb +1 -1
- data/lib/parsanol/pool.rb +1 -1
- data/lib/parsanol/rope.rb +1 -1
- data/lib/parsanol/slice.rb +4 -6
- data/lib/parsanol/source/line_cache.rb +1 -1
- data/lib/parsanol/source.rb +2 -2
- data/lib/parsanol/version.rb +1 -1
- data/lib/parsanol/vm.rb +95 -5
- data/lib/parsanol.rb +9 -0
- data/parsanol.gemspec +3 -1
- metadata +31 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b95e9035301a942d6d8f3acea734d6a4b0f6c5716dee43bc003cbb0be22060d1
|
|
4
|
+
data.tar.gz: c473d80ba7ebcf0ad407c5b1d120641bfc914808d90d7f3cbe45af296f391c69
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f998f1206ae7342ff33b5ed1f8225e0a05b9e6f92af7602d7c7741763ca621ee22c50d95ec303a5f44e2ec2f373ee714e20aeb4a57ccc8da108da1b290895820
|
|
7
|
+
data.tar.gz: 0a607fc6dda81038d2181e7226afe026a025564deed09b12caab1c938afd25f086096bd3bf6210c0a1b4bb38dfe8445fef032654832dfebd2e24f24b4df2becc
|
data/exe/parsanol
ADDED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
data/lib/parsanol/atoms/base.rb
CHANGED
|
@@ -208,7 +208,7 @@ module Parsanol
|
|
|
208
208
|
|
|
209
209
|
# Converts raw input to Source if needed.
|
|
210
210
|
def normalize_input(source)
|
|
211
|
-
source.
|
|
211
|
+
source.is_a?(Parsanol::Source) ? source : Parsanol::Source.new(source)
|
|
212
212
|
end
|
|
213
213
|
|
|
214
214
|
# Detects if we're in a Parser context.
|
data/lib/parsanol/atoms/cut.rb
CHANGED
|
@@ -42,7 +42,7 @@ module Parsanol
|
|
|
42
42
|
# 1. Mark the current position as a cut point
|
|
43
43
|
# 2. Empty the backtrack stack (we won't backtrack past here)
|
|
44
44
|
# 3. Aggressively evict cache entries before this position
|
|
45
|
-
context.cut!(source.bytepos)
|
|
45
|
+
context.cut!(source.bytepos)
|
|
46
46
|
|
|
47
47
|
[success, value]
|
|
48
48
|
end
|
|
@@ -66,7 +66,7 @@ module Parsanol
|
|
|
66
66
|
# @return [Array(Boolean, Object)] result
|
|
67
67
|
def try(source, context, consume_all)
|
|
68
68
|
# Check for tree memoization support
|
|
69
|
-
if context.
|
|
69
|
+
if context.use_tree_memoization?
|
|
70
70
|
return with_tree_cache(source, context, consume_all)
|
|
71
71
|
end
|
|
72
72
|
|
|
@@ -183,6 +183,7 @@ module Parsanol
|
|
|
183
183
|
last_failure = nil
|
|
184
184
|
|
|
185
185
|
loop do
|
|
186
|
+
iteration_start = source.bytepos
|
|
186
187
|
success, value = @parslet.apply(source, context, false)
|
|
187
188
|
|
|
188
189
|
unless success
|
|
@@ -193,6 +194,11 @@ module Parsanol
|
|
|
193
194
|
occurrence += 1
|
|
194
195
|
result[occurrence] = value
|
|
195
196
|
|
|
197
|
+
# A body match that consumes nothing cannot make progress;
|
|
198
|
+
# looping again would re-match empty forever. Count the empty
|
|
199
|
+
# match, then stop — the min check below applies as usual.
|
|
200
|
+
break if source.bytepos == iteration_start
|
|
201
|
+
|
|
196
202
|
break if @max && occurrence >= @max
|
|
197
203
|
end
|
|
198
204
|
|
|
@@ -254,6 +260,7 @@ module Parsanol
|
|
|
254
260
|
last_failure = nil
|
|
255
261
|
|
|
256
262
|
loop do
|
|
263
|
+
iteration_start = source.bytepos
|
|
257
264
|
success, value = @parslet.apply(source, context, false)
|
|
258
265
|
|
|
259
266
|
unless success
|
|
@@ -265,6 +272,10 @@ module Parsanol
|
|
|
265
272
|
result[occurrence] = value
|
|
266
273
|
positions << source.bytepos
|
|
267
274
|
|
|
275
|
+
# Zero-width body match: count it, stop iterating (the min
|
|
276
|
+
# check below applies as usual) — mirrors try_general.
|
|
277
|
+
break if source.bytepos == iteration_start
|
|
278
|
+
|
|
268
279
|
break if @max && occurrence >= @max
|
|
269
280
|
end
|
|
270
281
|
|
|
@@ -287,7 +298,7 @@ module Parsanol
|
|
|
287
298
|
context.store_tree_memo(cache_key, start_pos,
|
|
288
299
|
result[1, occurrence], end_pos)
|
|
289
300
|
end
|
|
290
|
-
context.release_array(positions)
|
|
301
|
+
context.release_array(positions)
|
|
291
302
|
|
|
292
303
|
# Check minimum
|
|
293
304
|
if occurrence < @min
|
data/lib/parsanol/cause.rb
CHANGED
|
@@ -74,7 +74,7 @@ module Parsanol
|
|
|
74
74
|
line_num, col_num = @source.line_and_column(@position)
|
|
75
75
|
|
|
76
76
|
formatted_msg = @message.map do |msg|
|
|
77
|
-
msg.
|
|
77
|
+
msg.is_a?(Parsanol::Slice) ? msg.content.inspect : msg.to_s
|
|
78
78
|
end.join
|
|
79
79
|
|
|
80
80
|
"#{formatted_msg} at line #{line_num} char #{col_num}#{@parsing_label}."
|
|
@@ -72,7 +72,7 @@ module Parsanol
|
|
|
72
72
|
cause = super
|
|
73
73
|
|
|
74
74
|
# Apply label if the atom has one
|
|
75
|
-
if atom.
|
|
75
|
+
if atom.is_a?(Parsanol::Atoms::Base) && (lbl = atom.label)
|
|
76
76
|
maybe_update_label(lbl, src.pos.bytepos)
|
|
77
77
|
cause.set_label(@active_label_text)
|
|
78
78
|
end
|
data/lib/parsanol/lazy_result.rb
CHANGED
|
@@ -103,6 +103,15 @@ module Parsanol
|
|
|
103
103
|
|
|
104
104
|
alias kind_of? is_a?
|
|
105
105
|
|
|
106
|
+
# The delegated interface: Array's full class contract (including
|
|
107
|
+
# Enumerable), computed once against the type rather than probed on
|
|
108
|
+
# instances.
|
|
109
|
+
ARRAY_METHODS = (
|
|
110
|
+
Array.instance_methods(true) + Enumerable.instance_methods + [:to_ary]
|
|
111
|
+
).freeze
|
|
112
|
+
|
|
113
|
+
private_constant :ARRAY_METHODS
|
|
114
|
+
|
|
106
115
|
# Respond to array methods.
|
|
107
116
|
#
|
|
108
117
|
# @param method [Symbol] Method name
|
|
@@ -110,7 +119,7 @@ module Parsanol
|
|
|
110
119
|
# @return [Boolean] true if responds
|
|
111
120
|
#
|
|
112
121
|
def respond_to?(method, include_private = false)
|
|
113
|
-
super ||
|
|
122
|
+
super || ARRAY_METHODS.include?(method)
|
|
114
123
|
end
|
|
115
124
|
|
|
116
125
|
# Delegate unknown methods to materialized array.
|
|
@@ -121,7 +130,7 @@ module Parsanol
|
|
|
121
130
|
# @return [Object] Result of method call
|
|
122
131
|
#
|
|
123
132
|
def method_missing(method, ...)
|
|
124
|
-
if
|
|
133
|
+
if ARRAY_METHODS.include?(method)
|
|
125
134
|
to_a.public_send(method, ...)
|
|
126
135
|
else
|
|
127
136
|
super
|
|
@@ -135,7 +144,7 @@ module Parsanol
|
|
|
135
144
|
# @return [Boolean] true if method is supported
|
|
136
145
|
#
|
|
137
146
|
def respond_to_missing?(method, include_private = false)
|
|
138
|
-
|
|
147
|
+
ARRAY_METHODS.include?(method) || super
|
|
139
148
|
end
|
|
140
149
|
|
|
141
150
|
# Compare with another object.
|
|
Binary file
|
|
@@ -95,9 +95,12 @@ module Parsanol
|
|
|
95
95
|
|
|
96
96
|
def serialize_re(atom)
|
|
97
97
|
# Ruby's Regexp#to_s produces "(?-mix:pattern)" format
|
|
98
|
-
#
|
|
98
|
+
# The pattern must reach the Rust regex engine verbatim, including
|
|
99
|
+
# inline flag groups: stripping (?i:...) silently turned every
|
|
100
|
+
# case-insensitive regex into a case-sensitive one on the native
|
|
101
|
+
# path (the Ruby atom still compiled it with the flag, so the two
|
|
102
|
+
# engines disagreed). Rust regex understands the same inline groups.
|
|
99
103
|
pattern = atom.match
|
|
100
|
-
pattern = ::Regexp.last_match(1) if pattern =~ /^\(\?[-mix]*:(.+)\)$/
|
|
101
104
|
{
|
|
102
105
|
"Re" => {
|
|
103
106
|
"pattern" => pattern,
|
|
@@ -391,15 +391,19 @@ module Parsanol
|
|
|
391
391
|
return left.is_a?(Hash) ? left.merge(right) : left + right
|
|
392
392
|
end
|
|
393
393
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
394
|
+
# String-like = String or Slice (both implement to_str). When
|
|
395
|
+
# only ONE side is string-like, the OTHER side wins — the
|
|
396
|
+
# original duck-typed priority, preserved exactly.
|
|
397
|
+
left_stringlike = left.is_a?(String) || left.is_a?(Parsanol::Slice)
|
|
398
|
+
right_stringlike = right.is_a?(String) || right.is_a?(Parsanol::Slice)
|
|
399
|
+
if left_stringlike && right_stringlike
|
|
400
|
+
return right if right.is_a?(Parsanol::Slice)
|
|
401
|
+
return left if left.is_a?(Parsanol::Slice)
|
|
402
|
+
|
|
403
|
+
return left + right
|
|
399
404
|
end
|
|
400
|
-
|
|
401
|
-
return
|
|
402
|
-
return right if left.respond_to?(:to_str)
|
|
405
|
+
return left if right_stringlike
|
|
406
|
+
return right if left_stringlike
|
|
403
407
|
|
|
404
408
|
return left + [right] if right.is_a?(Hash)
|
|
405
409
|
return [left] + right if left.is_a?(Hash)
|
data/lib/parsanol/native.rb
CHANGED
|
@@ -216,7 +216,9 @@ module Parsanol
|
|
|
216
216
|
# cause from that directly — the failure path never needs a
|
|
217
217
|
# reporter reparse. The single interpreter pass below stays only
|
|
218
218
|
# to recover inputs from grammars native cannot express.
|
|
219
|
-
|
|
219
|
+
parsanol_grammar =
|
|
220
|
+
grammar.is_a?(Parsanol::Atoms::Base) || grammar.is_a?(Parsanol::Parser)
|
|
221
|
+
if parsanol_grammar && (m = error.message.match(NATIVE_POS_MARKER))
|
|
220
222
|
source = Parsanol::Source.new(input)
|
|
221
223
|
success, value = grammar.run_with_context(source, nil, true)
|
|
222
224
|
return grammar.finalize_result(value) if success
|
|
@@ -227,7 +229,7 @@ module Parsanol
|
|
|
227
229
|
raise Parsanol::ParseFailed.new(cause.to_s, cause)
|
|
228
230
|
end
|
|
229
231
|
|
|
230
|
-
if
|
|
232
|
+
if parsanol_grammar
|
|
231
233
|
# No native diagnostics (e.g. incomplete-input errors): one
|
|
232
234
|
# interpreter pass with the reporter attached — a success
|
|
233
235
|
# recovers the tree, a failure raises the cause tree.
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "yaml"
|
|
5
|
+
|
|
6
|
+
module Parsanol
|
|
7
|
+
module PARG
|
|
8
|
+
# A compiled artifact envelope: load, verify checksum, parse inputs.
|
|
9
|
+
#
|
|
10
|
+
# The envelope's grammar section is the portable JSON the native engines
|
|
11
|
+
# consume; the embedded PARG source lets a Ruby runtime recompile the atom
|
|
12
|
+
# tree locally without any other dependency.
|
|
13
|
+
class Artifact
|
|
14
|
+
attr_reader :envelope, :path, :tables_dir
|
|
15
|
+
|
|
16
|
+
def self.load(path, tables_dir: nil)
|
|
17
|
+
envelope = JSON.parse(File.read(path))
|
|
18
|
+
new(envelope, path, tables_dir || File.dirname(path))
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.from_json(text, tables_dir: nil)
|
|
22
|
+
new(JSON.parse(text), nil, tables_dir)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
SUPPORTED_SHAPE = "parsanol-tree/v2"
|
|
26
|
+
|
|
27
|
+
def initialize(envelope, path, tables_dir)
|
|
28
|
+
unless envelope["shape"] == SUPPORTED_SHAPE
|
|
29
|
+
raise ArtifactError,
|
|
30
|
+
"unsupported artifact shape #{envelope['shape'].inspect} " \
|
|
31
|
+
"(this runtime implements #{SUPPORTED_SHAPE.inspect})"
|
|
32
|
+
end
|
|
33
|
+
@envelope = envelope
|
|
34
|
+
@path = path
|
|
35
|
+
@tables_dir = tables_dir
|
|
36
|
+
verify_checksum!
|
|
37
|
+
@compiler = nil
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def version = envelope["version"]
|
|
41
|
+
|
|
42
|
+
def entries = envelope["entries"].keys
|
|
43
|
+
|
|
44
|
+
def entry(name)
|
|
45
|
+
envelope["entries"].fetch(name) do
|
|
46
|
+
raise ArtifactError, "unknown entry #{name.inspect}"
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Parse via the native engine: the envelope's grammar section is the
|
|
51
|
+
# exact portable JSON the Rust side registers, so the artifact parse
|
|
52
|
+
# path is a straight register-and-run — no Ruby recompilation. The
|
|
53
|
+
# Ruby atom runtime (mode: :ruby) remains available explicitly, and
|
|
54
|
+
# serves as the fallback on platforms without the extension.
|
|
55
|
+
def parse(entry_name, input, mode: :native)
|
|
56
|
+
case mode
|
|
57
|
+
when :native
|
|
58
|
+
if Native.available?
|
|
59
|
+
return Native.parse(JSON.generate(entry(entry_name).fetch("grammar")), input)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
root_atom(entry_name).parse(input)
|
|
63
|
+
when :ruby
|
|
64
|
+
root_atom(entry_name).parse(input)
|
|
65
|
+
else
|
|
66
|
+
raise ArgumentError, "unknown mode #{mode.inspect} (use :native or :ruby)"
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def apply_bindings(entry_name, shape)
|
|
71
|
+
Bindings.apply(self, entry(entry_name), shape)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Render the identifier string from a bound attribute map (F6).
|
|
75
|
+
def render(_entry_name, bound, variant: "default")
|
|
76
|
+
Render.apply(envelope["render"] || {}, variant, bound)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def render_string(entry_name, input, variant: "default")
|
|
80
|
+
render(entry_name, parse_and_bind(entry_name, input), variant: variant)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Evaluate a named derive spec against a bound map (F6).
|
|
84
|
+
def derive(name, bound)
|
|
85
|
+
Derive.apply(envelope["derive"] || {}, name, bound)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def derive_string(entry_name, input, name)
|
|
89
|
+
derive(name, parse_and_bind(entry_name, input))
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def parse_and_bind(entry_name, input, mode: :native)
|
|
93
|
+
apply_bindings(entry_name, parse(entry_name, input, mode: mode))
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Re-run the grammar's in-file tests against this artifact. Returns
|
|
97
|
+
# the failure descriptions; empty means every test passes.
|
|
98
|
+
def run_tests(mode: :native)
|
|
99
|
+
run_test_list(envelope.fetch("tests", []), mode: mode)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Run an external test list (suite files yield Document::Test
|
|
103
|
+
# structs; embedded tests are artifact-shaped hashes).
|
|
104
|
+
def run_test_list(tests, mode: :native)
|
|
105
|
+
tests.filter_map do |test|
|
|
106
|
+
hash = test.is_a?(Document::Test) ? test_to_hash(test) : test
|
|
107
|
+
entry_name = hash["entry"] || entries.first
|
|
108
|
+
begin
|
|
109
|
+
bound = parse_and_bind(entry_name, hash["input"], mode: mode)
|
|
110
|
+
if hash["kind"] == "reject"
|
|
111
|
+
"test #{hash['input'].inspect}: expected the input to be rejected"
|
|
112
|
+
elsif hash["kind"] == "example"
|
|
113
|
+
expect = hash["expect"].to_h { |key, value| [key.to_sym, value] }
|
|
114
|
+
mismatched = expect.reject { |key, value| bound.key?(key) && bound[key] == value }
|
|
115
|
+
next if mismatched.empty?
|
|
116
|
+
|
|
117
|
+
"test #{hash['input'].inspect}: expected captures " \
|
|
118
|
+
"#{mismatched.transform_values(&:inspect).inspect}, got #{bound.inspect}"
|
|
119
|
+
end
|
|
120
|
+
rescue Parsanol::ParseFailed
|
|
121
|
+
unless hash["kind"] == "reject"
|
|
122
|
+
"test #{hash['input'].inspect}: expected the input to parse"
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def test_to_hash(test)
|
|
129
|
+
{
|
|
130
|
+
"entry" => test.entry,
|
|
131
|
+
"kind" => test.kind.to_s,
|
|
132
|
+
"input" => test.input,
|
|
133
|
+
"expect" => test.expect,
|
|
134
|
+
}
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Rule documentation embedded from ## doc comments.
|
|
138
|
+
def rule_docs = envelope.fetch("docs", {})
|
|
139
|
+
|
|
140
|
+
# Structured parse diagnostics (PN 2): {offset, message} for the
|
|
141
|
+
# deepest failure of the most recent parse attempt on this entry.
|
|
142
|
+
def parse_with_diagnostics(entry_name, input, mode: :native)
|
|
143
|
+
shape = parse(entry_name, input, mode: mode)
|
|
144
|
+
{ "ok" => true, "offset" => nil, "message" => nil, "shape" => shape }
|
|
145
|
+
rescue Parsanol::ParseFailed => e
|
|
146
|
+
cause = deepest_cause(e.parse_failure_cause)
|
|
147
|
+
{ "ok" => false, "offset" => cause&.position,
|
|
148
|
+
"message" => cause&.message || e.message, "shape" => nil }
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def deepest_cause(cause)
|
|
152
|
+
return cause if cause.nil? || cause.children.empty?
|
|
153
|
+
|
|
154
|
+
deepest = cause.children.filter_map { |child| deepest_cause(child) }
|
|
155
|
+
.max_by { |node| node.position.to_i }
|
|
156
|
+
(deepest&.position.to_i >= cause.position.to_i ? deepest : cause)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def table_rows(name)
|
|
160
|
+
@table_cache ||= {}
|
|
161
|
+
return @table_cache[name] if @table_cache.key?(name)
|
|
162
|
+
|
|
163
|
+
declared = envelope["tables"].fetch(name) do
|
|
164
|
+
raise ArtifactError, "artifact does not declare table #{name.inspect}"
|
|
165
|
+
end
|
|
166
|
+
if declared.is_a?(Hash) && declared.key?("rows")
|
|
167
|
+
return @table_cache[name] = declared["rows"]
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
file = declared
|
|
171
|
+
path = File.join(@tables_dir.to_s, file)
|
|
172
|
+
raw = path.end_with?(".json") ? JSON.parse(File.read(path)) : YAML.safe_load_file(path, aliases: true)
|
|
173
|
+
rows = case raw
|
|
174
|
+
when Hash
|
|
175
|
+
raw.map { |key, value| { "name" => key.to_s }.merge(value.to_h.transform_keys(&:to_s)) }
|
|
176
|
+
when Array then raw.map { |row| row.to_h.transform_keys(&:to_s) }
|
|
177
|
+
else raise ArtifactError, "table #{name.inspect} must be a map or array"
|
|
178
|
+
end
|
|
179
|
+
@table_cache[name] = rows
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def compiler
|
|
183
|
+
@compiler ||= begin
|
|
184
|
+
document = Parser.new(envelope.fetch("source")).parse
|
|
185
|
+
# `use`-importing grammars carry dotted cross-grammar rule
|
|
186
|
+
# references that only resolve once the imported sources merge;
|
|
187
|
+
# without this the pure-Ruby path fails on artifacts whose
|
|
188
|
+
# native atom graphs inline everything. The imported sources
|
|
189
|
+
# sit beside the artifact (vendors ship them together).
|
|
190
|
+
imports_dir = path && File.directory?(File.dirname(path)) ? File.dirname(path) : nil
|
|
191
|
+
Imports.merge!(document, [imports_dir].compact) if imports_dir
|
|
192
|
+
Compiler.new(document, @tables_dir)
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def root_atom(entry_name)
|
|
197
|
+
rule = entry(entry_name).fetch("root")
|
|
198
|
+
@compiled ||= {}
|
|
199
|
+
@compiled[rule] ||= compiler.atom_for(rule)
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def verify_checksum!
|
|
203
|
+
stored = envelope["checksum"]
|
|
204
|
+
computed = Compiler.checksum(envelope)
|
|
205
|
+
return if stored == computed
|
|
206
|
+
|
|
207
|
+
raise ArtifactError,
|
|
208
|
+
"artifact checksum mismatch: stored #{stored.inspect}, " \
|
|
209
|
+
"computed #{computed.inspect}"
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Parsanol
|
|
6
|
+
module PARG
|
|
7
|
+
# External test suites: *.pgtest files carrying the same test syntax
|
|
8
|
+
# as the inline test section (accept / reject / example). Suites are
|
|
9
|
+
# development-time corpora — they iterate a grammar against its
|
|
10
|
+
# binding requirements without being embedded in the artifact.
|
|
11
|
+
#
|
|
12
|
+
# suite smoke for entry identifier {
|
|
13
|
+
# accept "ISO 8601-1:2019"
|
|
14
|
+
# example "ISO/CD 12345" { stage: "draft20" }
|
|
15
|
+
# }
|
|
16
|
+
module Suite
|
|
17
|
+
module_function
|
|
18
|
+
|
|
19
|
+
# Load every *.pgtest file in a directory.
|
|
20
|
+
# Returns { suite_name => [Document::Test, ...] }.
|
|
21
|
+
def load(dir)
|
|
22
|
+
Dir.glob(File.join(dir, "*.pgtest")).to_h do |file|
|
|
23
|
+
[File.basename(file, ".pgtest"), read_file(file)]
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def read_file(file)
|
|
28
|
+
content = File.read(file).gsub(/^#[^\n]*\n/, "")
|
|
29
|
+
header = content.match(/suite\s+\w+(?:\s+for\s+entry\s+(?<entry>\w+))?\s*\{/)
|
|
30
|
+
unless header
|
|
31
|
+
raise ParseError,
|
|
32
|
+
"#{file}: expected 'suite <name> [for <entry>] { ... }'"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
body = content[header.end(0)..].sub(/\}\s*\z/, "")
|
|
36
|
+
document = Parser.new("grammar Suite version \"0.0.0\" {\n}\ntest {\n#{body}\n}\n").parse
|
|
37
|
+
document.tests.map do |test|
|
|
38
|
+
Document::Test.new(header[:entry] || test.entry, test.kind, test.input, test.expect)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# The binding-requirements schema: what a language-specific data model
|
|
44
|
+
# must implement for an entry. Derived from the declared bindings plus
|
|
45
|
+
# the observed example values.
|
|
46
|
+
module Schema
|
|
47
|
+
module_function
|
|
48
|
+
|
|
49
|
+
def from_artifact(artifact)
|
|
50
|
+
artifact.entries.to_h do |entry|
|
|
51
|
+
entry_data = artifact.entry(entry)
|
|
52
|
+
fields = entry_data["bindings"].to_h do |binding|
|
|
53
|
+
requirements = {
|
|
54
|
+
"type" => binding["type"],
|
|
55
|
+
"card" => binding["card"] || "1",
|
|
56
|
+
}
|
|
57
|
+
requirements["preprocess"] = binding["preprocess"] if binding["preprocess"]
|
|
58
|
+
[binding["path"], requirements]
|
|
59
|
+
end
|
|
60
|
+
examples = artifact.envelope.fetch("tests", [])
|
|
61
|
+
.select { |test| (test["kind"] == "example" && test["entry"].nil?) || test["entry"] == entry }
|
|
62
|
+
.map { |test| { "input" => test["input"], "captures" => test["expect"] } }
|
|
63
|
+
[entry, { "root" => entry_data["root"], "fields" => fields, "examples" => examples }]
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# TypeScript materialization types: one interface per entry, one
|
|
68
|
+
# property per bound field. Cards ending in * become arrays;
|
|
69
|
+
# 0..1 becomes nullable.
|
|
70
|
+
def to_typescript(schema)
|
|
71
|
+
lines = ["// Generated by parsanol parg schema --ts — binding-requirements types."]
|
|
72
|
+
schema.each do |entry, data|
|
|
73
|
+
lines << ""
|
|
74
|
+
lines << "export interface #{ts_name(entry)} {"
|
|
75
|
+
data["fields"].each do |path, requirements|
|
|
76
|
+
lines << " #{ts_prop(path)}: #{ts_type(requirements)};"
|
|
77
|
+
end
|
|
78
|
+
lines << "}"
|
|
79
|
+
end
|
|
80
|
+
"#{lines.join("\n")}\n"
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def ts_name(entry)
|
|
84
|
+
entry.split(/[_.]/).map(&:capitalize).join
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def ts_prop(path)
|
|
88
|
+
leaf = path.include?("[]") ? path.split("].").last : path
|
|
89
|
+
leaf.gsub(/([A-Za-z0-9])_([a-z])/) do
|
|
90
|
+
"#{Regexp.last_match(1)}#{Regexp.last_match(2).upcase}"
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def ts_type(requirements)
|
|
95
|
+
base = case requirements["type"]
|
|
96
|
+
when "integer", "float" then "number"
|
|
97
|
+
when "boolean" then "boolean"
|
|
98
|
+
else "string"
|
|
99
|
+
end
|
|
100
|
+
if requirements["card"].to_s.end_with?("*")
|
|
101
|
+
"#{base}[]"
|
|
102
|
+
elsif requirements["card"] == "0..1"
|
|
103
|
+
"#{base} | null"
|
|
104
|
+
else
|
|
105
|
+
base
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|