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,251 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Parsanol
|
|
6
|
+
module PARG
|
|
7
|
+
# Command-line tool for authoring and testing PARG grammars without any
|
|
8
|
+
# language binding: compile, run the in-file tests, parse inputs
|
|
9
|
+
# interactively, and read the rule documentation.
|
|
10
|
+
#
|
|
11
|
+
# parsanol parg compile grammars/iso.parg -o iso.json
|
|
12
|
+
# parsanol parg test grammars/iso.parg
|
|
13
|
+
# parsanol parg parse grammars/iso.parg "ISO 8601-1:2019"
|
|
14
|
+
# parsanol parg repl iso.json
|
|
15
|
+
# parsanol parg doc grammars/iso.parg publisher
|
|
16
|
+
class CLI
|
|
17
|
+
USAGE = <<~TEXT
|
|
18
|
+
usage: parsanol parg <command> <grammar> [arguments]
|
|
19
|
+
|
|
20
|
+
commands:
|
|
21
|
+
compile <file.parg> [-o out.json] compile to an artifact envelope
|
|
22
|
+
test <file.parg|artifact.json> run the grammar's test section
|
|
23
|
+
parse <file> [--entry E] INPUT parse one input; prints the tree
|
|
24
|
+
and the bound captures
|
|
25
|
+
repl <file> [--entry E] interactive parse loop (blank
|
|
26
|
+
line quits, :quit exits)
|
|
27
|
+
doc <file> [rule] print rule documentation
|
|
28
|
+
|
|
29
|
+
options:
|
|
30
|
+
--tables DIR directory for alt-from-table data (default:
|
|
31
|
+
the grammar file's directory)
|
|
32
|
+
TEXT
|
|
33
|
+
|
|
34
|
+
def self.run(argv)
|
|
35
|
+
new(argv).run
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def initialize(argv)
|
|
39
|
+
@argv = argv.dup
|
|
40
|
+
@tables_dir = nil
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def run
|
|
44
|
+
if (index = @argv.index("--tables"))
|
|
45
|
+
@tables_dir = @argv.delete_at(index + 1)
|
|
46
|
+
@argv.delete_at(index)
|
|
47
|
+
end
|
|
48
|
+
@json = !!@argv.delete("--json")
|
|
49
|
+
command = @argv.shift
|
|
50
|
+
case command
|
|
51
|
+
when "compile" then compile
|
|
52
|
+
when "test" then test
|
|
53
|
+
when "schema" then schema
|
|
54
|
+
when "parse" then parse
|
|
55
|
+
when "repl" then repl
|
|
56
|
+
when "doc" then doc
|
|
57
|
+
when "lsp" then Lsp.new.run && 0
|
|
58
|
+
else
|
|
59
|
+
warn USAGE
|
|
60
|
+
1
|
|
61
|
+
end
|
|
62
|
+
rescue Parsanol::PARG::Error, Parsanol::ParseFailed => e
|
|
63
|
+
warn "#{e.class}: #{e.message}"
|
|
64
|
+
1
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
# parsanol-tree/v2 JSON: leaves become {value, line, column, offset,
|
|
70
|
+
# length}; Ruby atom-runtime leaves are Parsanol::Slice (to_s is the
|
|
71
|
+
# captured text, positions unknown at this layer).
|
|
72
|
+
def json_shape(node)
|
|
73
|
+
case node
|
|
74
|
+
when Hash then node.to_h { |k, v| [k.to_s, json_shape(v)] }
|
|
75
|
+
when Array then node.map { |item| json_shape(item) }
|
|
76
|
+
when Parsanol::Slice then { "value" => node.to_s }
|
|
77
|
+
else node
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def compile
|
|
82
|
+
source = @argv.shift
|
|
83
|
+
out = flag_value("-o") || default_artifact_name(source)
|
|
84
|
+
envelope = compile_source(source).envelope
|
|
85
|
+
File.write(out, "#{JSON.generate(envelope)}\n")
|
|
86
|
+
puts "#{out} #{envelope['checksum']} warnings=#{envelope['lint']['order_warnings'].size}"
|
|
87
|
+
0
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def test
|
|
91
|
+
target = @argv.shift
|
|
92
|
+
if File.directory?(target)
|
|
93
|
+
return batch(target) { |file| artifact_for(file).run_tests }
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
artifact = artifact_for(target)
|
|
97
|
+
failures = artifact.run_tests
|
|
98
|
+
suite_dir = flag_value("--suite")
|
|
99
|
+
suites = suite_dir ? Parsanol::PARG::Suite.load(suite_dir) : {}
|
|
100
|
+
suites.each_value do |tests|
|
|
101
|
+
failures.concat(artifact.run_test_list(tests))
|
|
102
|
+
end
|
|
103
|
+
if failures.empty?
|
|
104
|
+
if @json
|
|
105
|
+
puts JSON.generate({ "ok" => true, "failures" => [] })
|
|
106
|
+
else
|
|
107
|
+
puts suites.empty? ? "all tests pass" : "all tests pass (#{suites.keys.join(', ')})"
|
|
108
|
+
end
|
|
109
|
+
0
|
|
110
|
+
else
|
|
111
|
+
if @json
|
|
112
|
+
puts JSON.generate({ "ok" => false, "failures" => failures })
|
|
113
|
+
else
|
|
114
|
+
failures.each { |failure| warn failure }
|
|
115
|
+
end
|
|
116
|
+
1
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def schema
|
|
121
|
+
ts = @argv.delete("--ts")
|
|
122
|
+
artifact = artifact_for(@argv.shift)
|
|
123
|
+
schema = Parsanol::PARG::Schema.from_artifact(artifact)
|
|
124
|
+
if ts
|
|
125
|
+
puts Parsanol::PARG::Schema.to_typescript(schema)
|
|
126
|
+
else
|
|
127
|
+
puts JSON.pretty_generate(schema)
|
|
128
|
+
end
|
|
129
|
+
0
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def parse
|
|
133
|
+
file = @argv.shift
|
|
134
|
+
entry = flag_value("--entry")
|
|
135
|
+
input = @argv.join(" ")
|
|
136
|
+
if input.empty?
|
|
137
|
+
warn "usage: parsanol parg parse <file> [--entry E] INPUT"
|
|
138
|
+
return 1
|
|
139
|
+
end
|
|
140
|
+
artifact = artifact_for(file)
|
|
141
|
+
entry_name = entry || artifact.entries.first
|
|
142
|
+
shape = artifact.parse(entry_name, input)
|
|
143
|
+
if @json
|
|
144
|
+
puts JSON.generate({
|
|
145
|
+
"entry" => entry_name,
|
|
146
|
+
"shape" => json_shape(shape),
|
|
147
|
+
"bound" => artifact.apply_bindings(entry_name, shape),
|
|
148
|
+
})
|
|
149
|
+
else
|
|
150
|
+
puts "tree: #{shape.inspect}"
|
|
151
|
+
puts "captures: #{artifact.apply_bindings(entry_name, shape).inspect}"
|
|
152
|
+
end
|
|
153
|
+
0
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def repl
|
|
157
|
+
file = @argv.shift
|
|
158
|
+
entry = flag_value("--entry")
|
|
159
|
+
artifact = artifact_for(file)
|
|
160
|
+
entry_name = entry || artifact.entries.first
|
|
161
|
+
puts "parsanol parg repl — #{file} entry #{entry_name.inspect}; blank line quits"
|
|
162
|
+
loop do
|
|
163
|
+
print "> "
|
|
164
|
+
input = $stdin.gets
|
|
165
|
+
break if input.nil? || input.strip.empty?
|
|
166
|
+
|
|
167
|
+
begin
|
|
168
|
+
shape = artifact.parse(entry_name, input.rstrip)
|
|
169
|
+
puts "tree: #{shape.inspect}"
|
|
170
|
+
puts "captures: #{artifact.apply_bindings(entry_name, shape).inspect}"
|
|
171
|
+
rescue Parsanol::ParseFailed => e
|
|
172
|
+
puts "parse failed: #{e.message[0, 160]}"
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
0
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def doc
|
|
179
|
+
file = @argv.shift
|
|
180
|
+
rule = @argv.shift
|
|
181
|
+
docs = artifact_for(file).rule_docs
|
|
182
|
+
selected = rule ? { rule => docs[rule] } : docs
|
|
183
|
+
if selected.values.all?(&:nil?)
|
|
184
|
+
warn "no documentation#{" for #{rule.inspect}" if rule}"
|
|
185
|
+
return 1
|
|
186
|
+
end
|
|
187
|
+
selected.each do |name, text|
|
|
188
|
+
puts "#{name}:"
|
|
189
|
+
text.to_s.split("\n").each { |line| puts " #{line}" }
|
|
190
|
+
end
|
|
191
|
+
0
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def for_source_or_artifact
|
|
195
|
+
file = @argv.shift
|
|
196
|
+
artifact = artifact_for(file)
|
|
197
|
+
yield artifact
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def artifact_for(file)
|
|
201
|
+
if file.end_with?(".json")
|
|
202
|
+
Parsanol::PARG::Artifact.load(file, tables_dir: @tables_dir || default_tables_dir(file))
|
|
203
|
+
else
|
|
204
|
+
artifact = compile_source(file)
|
|
205
|
+
Parsanol::PARG::Artifact.from_json(JSON.generate(artifact.envelope),
|
|
206
|
+
tables_dir: @tables_dir || default_tables_dir(file))
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def compile_source(file)
|
|
211
|
+
tables = @tables_dir || default_tables_dir(file)
|
|
212
|
+
document = Parsanol::PARG::Parser.new(File.read(file)).parse
|
|
213
|
+
Parsanol::PARG::Imports.merge!(document, [File.dirname(file)])
|
|
214
|
+
Parsanol::PARG::Compiler.compile(document, tables_dir: tables)
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def default_artifact_name(source)
|
|
218
|
+
"#{File.basename(source, '.parg')}.artifact.json"
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# Batch mode: run a command over every *.parg in a directory.
|
|
222
|
+
def batch(target)
|
|
223
|
+
failures = {}
|
|
224
|
+
Dir.glob(File.join(target, "*.parg")).each do |file|
|
|
225
|
+
result = yield artifact_for(file)
|
|
226
|
+
failures[file] = result unless result.empty?
|
|
227
|
+
end
|
|
228
|
+
if failures.empty?
|
|
229
|
+
puts "all flavors pass"
|
|
230
|
+
0
|
|
231
|
+
else
|
|
232
|
+
failures.each { |file, list| list.each { |f| warn "#{file}: #{f}" } }
|
|
233
|
+
1
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# Tables conventionally live in a "tables" directory beside the
|
|
238
|
+
# grammar (as a sibling of the grammars directory).
|
|
239
|
+
def default_tables_dir(file)
|
|
240
|
+
dir = File.dirname(file)
|
|
241
|
+
candidate = File.join(dir, "tables")
|
|
242
|
+
candidate = File.join(File.dirname(dir), "tables") unless File.directory?(candidate)
|
|
243
|
+
File.directory?(candidate) ? candidate : dir
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def flag_value(flag)
|
|
247
|
+
@argv.delete_at(@argv.index(flag) + 1).tap { @argv.delete(flag) } if @argv.include?(flag)
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
end
|