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,208 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "rbconfig"
|
|
5
|
+
|
|
6
|
+
module Parsanol
|
|
7
|
+
module PARG
|
|
8
|
+
# F11: minimal PARG LSP over stdio (JSON-RPC 2.0, no framework).
|
|
9
|
+
#
|
|
10
|
+
# initialize / shutdown / exit
|
|
11
|
+
# textDocument/didOpen|didChange -> publishDiagnostics
|
|
12
|
+
# (parse errors, lint errors, failing inline tests)
|
|
13
|
+
# textDocument/hover -> the rule's ## doc comment
|
|
14
|
+
#
|
|
15
|
+
# Diagnostics and hover run entirely on the compiler API — one
|
|
16
|
+
# language implementation, every editor.
|
|
17
|
+
class Lsp
|
|
18
|
+
CONTENT_LENGTH = "Content-Length: "
|
|
19
|
+
|
|
20
|
+
def initialize(input = $stdin, output = $stdout)
|
|
21
|
+
@input = input
|
|
22
|
+
@output = output
|
|
23
|
+
@documents = {}
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def run
|
|
27
|
+
until @input.eof?
|
|
28
|
+
header = @input.gets("\r\n\r\n")
|
|
29
|
+
break if header.nil?
|
|
30
|
+
|
|
31
|
+
length = header[/Content-Length: (\d+)/i, 1].to_i
|
|
32
|
+
break if length.zero?
|
|
33
|
+
|
|
34
|
+
message = JSON.parse(@input.read(length))
|
|
35
|
+
handle(message)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def handle(message)
|
|
42
|
+
case message["method"]
|
|
43
|
+
when "initialize"
|
|
44
|
+
respond(message, {
|
|
45
|
+
"capabilities" => {
|
|
46
|
+
"textDocumentSync" => 1,
|
|
47
|
+
"hoverProvider" => true,
|
|
48
|
+
},
|
|
49
|
+
"serverInfo" => { "name" => "parsanol-parg-lsp" },
|
|
50
|
+
})
|
|
51
|
+
when "shutdown"
|
|
52
|
+
respond(message, nil)
|
|
53
|
+
when "exit"
|
|
54
|
+
exit 0
|
|
55
|
+
when "textDocument/didOpen", "textDocument/didChange"
|
|
56
|
+
doc = message["params"]["textDocument"]
|
|
57
|
+
@documents[doc["uri"]] =
|
|
58
|
+
doc["text"] || message.dig("params", "contentChanges", -1, "text")
|
|
59
|
+
publish_diagnostics(doc["uri"])
|
|
60
|
+
when "textDocument/codeAction"
|
|
61
|
+
respond(message, code_action(message["params"]))
|
|
62
|
+
when "textDocument/hover"
|
|
63
|
+
respond(message, hover(message.dig("params", "textDocument", "uri"),
|
|
64
|
+
message.dig("params", "position")))
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def respond(message, result)
|
|
69
|
+
send_message("id" => message["id"], "result" => result)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def notify(method, params)
|
|
73
|
+
send_message("jsonrpc" => "2.0", "method" => method, "params" => params)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def send_message(payload)
|
|
77
|
+
body = JSON.generate(payload)
|
|
78
|
+
@output.write("Content-Length: #{body.bytesize}\r\n\r\n#{body}")
|
|
79
|
+
@output.flush
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def publish_diagnostics(uri)
|
|
83
|
+
source = @documents[uri]
|
|
84
|
+
diagnostics = []
|
|
85
|
+
document = nil
|
|
86
|
+
begin
|
|
87
|
+
document = Parser.new(source).parse
|
|
88
|
+
rescue ParseError => e
|
|
89
|
+
line = 0
|
|
90
|
+
# The self-hosting artifact yields a precise offset (F7 wire);
|
|
91
|
+
# prefer it when the artifact is available.
|
|
92
|
+
if SelfHost.available?
|
|
93
|
+
begin
|
|
94
|
+
SelfHost.validate(source)
|
|
95
|
+
rescue Parsanol::ParseFailed
|
|
96
|
+
# fall through to line 0
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
diagnostics << diagnostic(line, e.message)
|
|
100
|
+
end
|
|
101
|
+
if document
|
|
102
|
+
begin
|
|
103
|
+
tables_dir = ENV.fetch("PG_TABLES_DIR", nil)
|
|
104
|
+
compiler = Compiler.new(document, tables_dir)
|
|
105
|
+
document.rules.each_key { |rule| compiler.atom_for(rule) }
|
|
106
|
+
Lints.errors(document, compiler).each do |error|
|
|
107
|
+
diagnostics << diagnostic(rule_line(source, error[/\Arule (\w+):/, 1]), error)
|
|
108
|
+
end
|
|
109
|
+
compiler.run_tests.each do |failure|
|
|
110
|
+
diagnostics << diagnostic(0, failure)
|
|
111
|
+
end
|
|
112
|
+
rescue ParseError, PARG::Error => e
|
|
113
|
+
diagnostics << diagnostic(0, e.message)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
notify("textDocument/publishDiagnostics",
|
|
117
|
+
"uri" => uri, "diagnostics" => diagnostics)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Rule-granular position: the line the named rule is defined on.
|
|
121
|
+
def rule_line(source, rule_name)
|
|
122
|
+
return 0 unless rule_name
|
|
123
|
+
|
|
124
|
+
source.lines.index { |l| l.match?(/^\s*#{rule_name}\s*=/) } || 0
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def diagnostic(line, message)
|
|
128
|
+
{
|
|
129
|
+
"range" => { "start" => { "line" => line, "character" => 0 },
|
|
130
|
+
"end" => { "line" => line, "character" => 0 } },
|
|
131
|
+
"severity" => 1,
|
|
132
|
+
"source" => "parsanol-parg",
|
|
133
|
+
"message" => message,
|
|
134
|
+
}
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Reorder code action: only when EVERY top-level alternative of the
|
|
138
|
+
# rule is a quoted literal is the reorder mechanically safe.
|
|
139
|
+
def code_action(params)
|
|
140
|
+
uri = params.dig("textDocument", "uri")
|
|
141
|
+
source = @documents[uri]
|
|
142
|
+
return [] unless source
|
|
143
|
+
|
|
144
|
+
line_no = params.dig("range", "start", "line").to_i
|
|
145
|
+
text = source.lines[line_no].to_s
|
|
146
|
+
return [] unless text.match?(/^\s*[a-z_]+\s*=\s*"/)
|
|
147
|
+
|
|
148
|
+
alternatives = top_level_literals(text)
|
|
149
|
+
return [] if alternatives.nil? || alternatives.size < 2
|
|
150
|
+
|
|
151
|
+
sorted = alternatives.sort_by { |a| -a.length }
|
|
152
|
+
return [] if sorted == alternatives
|
|
153
|
+
|
|
154
|
+
[{
|
|
155
|
+
"title" => "Reorder alternatives longest-first",
|
|
156
|
+
"kind" => "refactor",
|
|
157
|
+
"edit" => {
|
|
158
|
+
"changes" => {
|
|
159
|
+
uri => [{ "range" => whole_line_range(line_no),
|
|
160
|
+
"newText" => "#{text[/^\s*[a-z_]+\s*=\s*/]}#{sorted.join(' / ').inspect}" }],
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
}]
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def top_level_literals(line)
|
|
167
|
+
rhs = line.sub(/^\s*[a-z_]+\s*=\s*/, "")
|
|
168
|
+
parts = rhs.split(/ (?=")/).flat_map { |p| p.split(%r{ / (?=")}) }
|
|
169
|
+
literals = parts.map { |p| p.match(/^"((?:[^"\\]|\\.)*)"$/) }
|
|
170
|
+
return nil if literals.any?(&:nil?)
|
|
171
|
+
|
|
172
|
+
literals.map { |m| m[1] }
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def whole_line_range(line_no)
|
|
176
|
+
{ "start" => { "line" => line_no, "character" => 0 },
|
|
177
|
+
"end" => { "line" => line_no, "character" => 1000 } }
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def hover(uri, position)
|
|
181
|
+
source = @documents[uri]
|
|
182
|
+
return nil unless source && position
|
|
183
|
+
|
|
184
|
+
line_no = position.dig("position", "line") || position["line"]
|
|
185
|
+
text = source.lines[line_no.to_i].to_s
|
|
186
|
+
rule = text[/^\s*([a-z_]+)\s*=/, 1]
|
|
187
|
+
return nil unless rule
|
|
188
|
+
|
|
189
|
+
lines = source.lines
|
|
190
|
+
rule_line = lines.index { |l| l.match?(/^\s*#{rule}\s*=/) }
|
|
191
|
+
return nil unless rule_line
|
|
192
|
+
|
|
193
|
+
doc_lines = []
|
|
194
|
+
cursor = rule_line - 1
|
|
195
|
+
while cursor >= 0 && lines[cursor].strip.start_with?("##")
|
|
196
|
+
doc_lines.unshift(lines[cursor])
|
|
197
|
+
cursor -= 1
|
|
198
|
+
end
|
|
199
|
+
return nil if doc_lines.empty?
|
|
200
|
+
|
|
201
|
+
doc = doc_lines.map(&:strip).join("\n")
|
|
202
|
+
|
|
203
|
+
{ "contents" => { "kind" => "markdown",
|
|
204
|
+
"value" => doc.gsub(/^##\s?/, "").strip } }
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Parsanol
|
|
4
|
+
module PARG
|
|
5
|
+
# lutaml-model integration: register a PARG artifact as a lutaml-model
|
|
6
|
+
# string format, giving every Serializable class from_<format> —
|
|
7
|
+
# artifact parse -> bindings -> model instance.
|
|
8
|
+
#
|
|
9
|
+
# Parsanol::PARG::Lutaml.register(
|
|
10
|
+
# IsoIdentifier,
|
|
11
|
+
# format_name: :pubid_iso,
|
|
12
|
+
# artifact: "artifacts/iso.json",
|
|
13
|
+
# entry: "identifier",
|
|
14
|
+
# )
|
|
15
|
+
# IsoIdentifier.from_pubid_iso("ISO/IEC 12345-1:2020")
|
|
16
|
+
#
|
|
17
|
+
# Registration goes through ::Lutaml::Model::FormatRegistry (the same
|
|
18
|
+
# extension point as xml/json/yaml), so the format participates in the
|
|
19
|
+
# framework's format metadata and error typing. to_<format> (render)
|
|
20
|
+
# arrives with artifact render specs.
|
|
21
|
+
module Lutaml
|
|
22
|
+
module_function
|
|
23
|
+
|
|
24
|
+
def register(model_class, format_name:, artifact:, entry:, tables_dir: nil)
|
|
25
|
+
unless defined?(::Lutaml::Model)
|
|
26
|
+
raise Error, "lutaml-model is not available; add it to your bundle"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
art = Artifact.load(artifact, tables_dir: tables_dir)
|
|
30
|
+
entry_name = entry
|
|
31
|
+
|
|
32
|
+
adapter = Class.new do
|
|
33
|
+
define_singleton_method(:parse) do |data, _options = {}|
|
|
34
|
+
art.parse_and_bind(entry_name, data)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
::Lutaml::Model::FormatRegistry.register(
|
|
39
|
+
format_name,
|
|
40
|
+
mapping_class: ::Lutaml::Model::Mapping,
|
|
41
|
+
adapter_class: adapter,
|
|
42
|
+
transformer: artifact_transformer(model_class, art, entry_name),
|
|
43
|
+
error_types: [Parsanol::ParseFailed],
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
model_class.define_singleton_method(:"from_#{format_name}") do |input, _options = {}|
|
|
47
|
+
new(art.parse_and_bind(entry_name, input))
|
|
48
|
+
end
|
|
49
|
+
format_name
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def artifact_transformer(model_class, artifact, entry_name)
|
|
53
|
+
Class.new do
|
|
54
|
+
define_singleton_method(:name) { "#{model_class.name}PGTransform" }
|
|
55
|
+
|
|
56
|
+
define_method(:data_to_model) do |data, _format, _options = {}|
|
|
57
|
+
model_class.new(data)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
define_singleton_method(:from_pg) do |input|
|
|
61
|
+
model_class.new(artifact.parse_and_bind(entry_name, input))
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Parsanol
|
|
4
|
+
module PARG
|
|
5
|
+
# PARG intermediate representation node.
|
|
6
|
+
#
|
|
7
|
+
# kind + payload slots:
|
|
8
|
+
# [:lit, string, fold] literal; fold = case-insensitive
|
|
9
|
+
# [:class, ranges] byte ranges [[lo, hi], ...]
|
|
10
|
+
# [:seq, items]
|
|
11
|
+
# [:alt, branches] ordered choice
|
|
12
|
+
# [:rep, child, min, max] max nil = unbounded
|
|
13
|
+
# [:opt, child] maybe (0..1)
|
|
14
|
+
# [:pred, positive, child] &x / !x
|
|
15
|
+
# [:cap, name, child] x as name
|
|
16
|
+
# [:ref, name] rule reference
|
|
17
|
+
# [:table, table, column] alternatives expanded from data
|
|
18
|
+
Node = Struct.new(:kind, :a, :b, :c)
|
|
19
|
+
end
|
|
20
|
+
end
|