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,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