parsanol 1.3.95-aarch64-linux → 1.3.97-aarch64-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/lib/parsanol/3.2/parsanol_native.so +2 -2
- data/lib/parsanol/3.3/parsanol_native.so +2 -2
- data/lib/parsanol/3.4/parsanol_native.so +2 -2
- data/lib/parsanol/4.0/parsanol_native.so +2 -2
- data/lib/parsanol/atoms/context.rb +8 -0
- data/lib/parsanol/atoms/named.rb +2 -0
- data/lib/parsanol/native/libparsanol.so +2 -2
- data/lib/parsanol/native.rb +14 -0
- data/lib/parsanol/parg/artifact.rb +47 -0
- data/lib/parsanol/parg/compiler.rb +6 -0
- data/lib/parsanol/parg/lsp.rb +464 -43
- data/lib/parsanol/recovery.rb +33 -0
- data/lib/parsanol/version.rb +1 -1
- data/lib/parsanol.rb +1 -0
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f259ea55092d5673e2d9b3604df6fb9adba3807dba48500b765562de3b1b6aee
|
|
4
|
+
data.tar.gz: d6c1ba491efa08d1ccc551d79899558577c72138909a17d46a39560ca818b80a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 066d798f2dc0b057a46db9f077aee2e0d4a424b076c836cce9152ef1d969b2f3d587b0071ed9f1b3524a0534c223aa9cd58b2d43d8511ec58eb8d867438a8fd9
|
|
7
|
+
data.tar.gz: 8bce87d1b44af2a4ec860b5a1072837bda1ac171a6aee8d1fd1dd505cac84a3ae84f7529f413430014de1dd902ce69197d2c8f9ae6cb16c465591d520a36af37
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
[diffend] Oversized file quarantined before diffing.
|
|
2
2
|
name: data/lib/parsanol/3.2/parsanol_native.so
|
|
3
|
-
size:
|
|
4
|
-
sha256:
|
|
3
|
+
size: 40739336 bytes
|
|
4
|
+
sha256: 4cad7bf4894a1cea08ee2b319696f166f2e7cc38ed6eef38dee79057f9fd684a
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
[diffend] Oversized file quarantined before diffing.
|
|
2
2
|
name: data/lib/parsanol/3.3/parsanol_native.so
|
|
3
|
-
size:
|
|
4
|
-
sha256:
|
|
3
|
+
size: 40974392 bytes
|
|
4
|
+
sha256: 25df57a50ccd92bd51baaf86008559487f4259b81a899c30c61e8e023ff7ec23
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
[diffend] Oversized file quarantined before diffing.
|
|
2
2
|
name: data/lib/parsanol/3.4/parsanol_native.so
|
|
3
|
-
size:
|
|
4
|
-
sha256:
|
|
3
|
+
size: 40961728 bytes
|
|
4
|
+
sha256: e6e8cae20b2ffc4754570fd4e4e19081e24e4b5c37ccc0e122619ada30ca7d97
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
[diffend] Oversized file quarantined before diffing.
|
|
2
2
|
name: data/lib/parsanol/4.0/parsanol_native.so
|
|
3
|
-
size:
|
|
4
|
-
sha256:
|
|
3
|
+
size: 40971784 bytes
|
|
4
|
+
sha256: 4c426cf8bfd77fb270f32e171664208d9b3fd3414bcf22b2d677fddc2ec14e9f
|
|
@@ -72,6 +72,10 @@ module Parsanol
|
|
|
72
72
|
# and the NEXT successful Named capture attaches (and drains)
|
|
73
73
|
# them under `comments:`.
|
|
74
74
|
@pending_trivia = []
|
|
75
|
+
|
|
76
|
+
# Recovery journal (TODO.sota/1): non-nil only in recovery
|
|
77
|
+
# re-parses; Named captures append to the shared array.
|
|
78
|
+
@recovery_journal = Parsanol::Recovery.journal if Parsanol::Recovery.journaling?
|
|
75
79
|
@evict_threshold = 200
|
|
76
80
|
@evict_counter = 0
|
|
77
81
|
@evict_interval = 100
|
|
@@ -327,6 +331,10 @@ module Parsanol
|
|
|
327
331
|
# @return [Parsanol::Scope] capture variable bindings
|
|
328
332
|
attr_reader :captures
|
|
329
333
|
|
|
334
|
+
# Recovery journal (TODO.sota/1): a shared array when this
|
|
335
|
+
# context was created inside a recovery re-parse; nil otherwise.
|
|
336
|
+
attr_accessor :recovery_journal
|
|
337
|
+
|
|
330
338
|
# @return [Parsanol::Pools::ArrayPool] array object pool
|
|
331
339
|
attr_reader :array_pool
|
|
332
340
|
|
data/lib/parsanol/atoms/named.rb
CHANGED
|
@@ -42,6 +42,8 @@ module Parsanol
|
|
|
42
42
|
comments = context.take_pending_trivia
|
|
43
43
|
result = wrap_result(value)
|
|
44
44
|
result = attach_comments(result, comments, source) unless comments.empty?
|
|
45
|
+
# Recovery journal (TODO.sota/1): nil on every normal parse.
|
|
46
|
+
context.recovery_journal << [@name, result] if context.recovery_journal
|
|
45
47
|
ok(result)
|
|
46
48
|
end
|
|
47
49
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
[diffend] Oversized file quarantined before diffing.
|
|
2
2
|
name: data/lib/parsanol/native/libparsanol.so
|
|
3
|
-
size:
|
|
4
|
-
sha256:
|
|
3
|
+
size: 36519512 bytes
|
|
4
|
+
sha256: e6eab7ae66da64b0abf1e86985786423cce6eba79524e64771a52fd53d541530
|
data/lib/parsanol/native.rb
CHANGED
|
@@ -382,6 +382,20 @@ module Parsanol
|
|
|
382
382
|
:unavailable
|
|
383
383
|
end
|
|
384
384
|
end
|
|
385
|
+
name_cache_version if @native_state == :loaded
|
|
386
|
+
@native_state
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
private
|
|
390
|
+
|
|
391
|
+
# Compiled VM programs are cached on disk across processes; naming
|
|
392
|
+
# this gem release in the cache key means a new release never runs
|
|
393
|
+
# a program an older one compiled. Extensions built before the
|
|
394
|
+
# engine grew the hook simply skip it.
|
|
395
|
+
def name_cache_version
|
|
396
|
+
return unless respond_to?(:_set_cache_version)
|
|
397
|
+
|
|
398
|
+
_set_cache_version("parsanol-ruby #{Parsanol::VERSION}")
|
|
385
399
|
end
|
|
386
400
|
end
|
|
387
401
|
end
|
|
@@ -197,6 +197,53 @@ module Parsanol
|
|
|
197
197
|
end
|
|
198
198
|
end
|
|
199
199
|
|
|
200
|
+
# Best-effort recovery (TODO.sota/1, R1): on success returns
|
|
201
|
+
# {"ok": true, "shape": tree}; on ParseFailed returns
|
|
202
|
+
# {"ok": false, "error": {"offset", "message"}, "captures":
|
|
203
|
+
# {name => [values...]}} — the captures observed before the
|
|
204
|
+
# failure, from a journaling re-parse on the Ruby engine (tree
|
|
205
|
+
# parity with the native engine is enforced by the differential
|
|
206
|
+
# specs). The journal is best-effort: it may include captures
|
|
207
|
+
# from branches that later failed.
|
|
208
|
+
def parse_recovering(entry_name, input, mode: :native)
|
|
209
|
+
shape = parse(entry_name, input, mode: mode)
|
|
210
|
+
{ "ok" => true, "shape" => shape }
|
|
211
|
+
rescue Parsanol::ParseFailed => e
|
|
212
|
+
{ "ok" => false,
|
|
213
|
+
"error" => recovery_error(e),
|
|
214
|
+
"captures" => recovery_journal(entry_name, input, mode) }
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def recovery_error(err)
|
|
218
|
+
pos = err.parse_failure_cause&.position
|
|
219
|
+
{ "offset" => pos, "message" => err.message }
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# Journaling re-parse — always on the Ruby engine (journaling
|
|
223
|
+
# rides the interpreter's diagnostic reparse; the native engine
|
|
224
|
+
# has no journal). Tree parity with the native engine is enforced
|
|
225
|
+
# by the differential specs, so the observed captures agree.
|
|
226
|
+
def recovery_journal(entry_name, input, _mode)
|
|
227
|
+
Parsanol::Recovery.start_journal
|
|
228
|
+
begin
|
|
229
|
+
parse(entry_name, input, mode: :ruby)
|
|
230
|
+
rescue Parsanol::ParseFailed
|
|
231
|
+
# expected: the journaling pass fails at the same point
|
|
232
|
+
end
|
|
233
|
+
Parsanol::Recovery.stop_journal.each_with_object({}) do |(name, value), out|
|
|
234
|
+
# Both the initial attempt and the diagnostic re-parse journal
|
|
235
|
+
# into the shared thread-local; identical observations dedupe.
|
|
236
|
+
wire = value.is_a?(Slice) ? slice_wire(value) : value
|
|
237
|
+
(out[name.to_s] ||= []) << wire
|
|
238
|
+
out[name.to_s].uniq!
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def slice_wire(slice)
|
|
243
|
+
{ "value" => slice.to_s, "offset" => slice.offset,
|
|
244
|
+
"length" => slice.content.length }
|
|
245
|
+
end
|
|
246
|
+
|
|
200
247
|
def test_to_hash(test)
|
|
201
248
|
{
|
|
202
249
|
"entry" => test.entry,
|
|
@@ -337,6 +337,12 @@ module Parsanol
|
|
|
337
337
|
find_left_cycle_from(name, [], {})
|
|
338
338
|
end
|
|
339
339
|
|
|
340
|
+
# In-file test failures without raising: the LSP surfaces them as
|
|
341
|
+
# diagnostics instead of failing a build.
|
|
342
|
+
def test_failures
|
|
343
|
+
run_tests
|
|
344
|
+
end
|
|
345
|
+
|
|
340
346
|
private
|
|
341
347
|
|
|
342
348
|
# In-file grammar tests run at compile time: the build fails when an
|
data/lib/parsanol/parg/lsp.rb
CHANGED
|
@@ -1,26 +1,63 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "json"
|
|
4
|
-
require "
|
|
4
|
+
require "strscan"
|
|
5
|
+
require "uri"
|
|
5
6
|
|
|
6
7
|
module Parsanol
|
|
7
8
|
module PARG
|
|
8
|
-
#
|
|
9
|
+
# PARG language server over stdio (JSON-RPC 2.0, no framework).
|
|
9
10
|
#
|
|
10
11
|
# initialize / shutdown / exit
|
|
11
|
-
# textDocument/didOpen|didChange -> publishDiagnostics
|
|
12
|
-
# (parse errors, lint errors, failing inline tests)
|
|
13
|
-
# textDocument/hover
|
|
12
|
+
# textDocument/didOpen|didChange|didClose -> publishDiagnostics
|
|
13
|
+
# (parse errors, lint errors, failing inline tests; imports merged)
|
|
14
|
+
# textDocument/hover -> rule docs (Imports-aware)
|
|
15
|
+
# textDocument/definition -> rule definition / imported file
|
|
16
|
+
# textDocument/references -> rule occurrences
|
|
17
|
+
# textDocument/completion -> keywords, rule names, snippets
|
|
18
|
+
# textDocument/semanticTokens/full
|
|
19
|
+
# textDocument/codeAction -> longest-first reorder
|
|
14
20
|
#
|
|
15
|
-
#
|
|
16
|
-
# language implementation, every editor.
|
|
21
|
+
# Everything runs on the compiler API and the parsed Document model — one
|
|
22
|
+
# language implementation, every editor. Imported grammars are resolved
|
|
23
|
+
# from the document's directory and PG_IMPORT_DIR (path-separator list).
|
|
17
24
|
class Lsp
|
|
18
25
|
CONTENT_LENGTH = "Content-Length: "
|
|
19
26
|
|
|
27
|
+
SEMANTIC_TYPES = %w[keyword rule capture string number comment].freeze
|
|
28
|
+
SEMANTIC_MODIFIERS = %w[declaration].freeze
|
|
29
|
+
DECLARATION_BIT = 1
|
|
30
|
+
|
|
31
|
+
KEYWORDS = (%w[version test accept reject example use] + Parser::KEYWORDS).uniq.freeze
|
|
32
|
+
|
|
33
|
+
COMPLETION_KEYWORDS = KEYWORDS.map do |keyword|
|
|
34
|
+
{ "label" => keyword, "kind" => 14 }
|
|
35
|
+
end.freeze
|
|
36
|
+
|
|
37
|
+
SNIPPETS = [
|
|
38
|
+
["%i\"...\"", "%i\"${1:text}\"", "case-insensitive literal"],
|
|
39
|
+
["%x41", "%x41", "one code point"],
|
|
40
|
+
["%x41-5A", "%x41-5A", "code point range"],
|
|
41
|
+
["%x0D.0A", "%x0D.0A", "code point sequence"],
|
|
42
|
+
["[ ... ]", "[ ${1:content} ]", "optional"],
|
|
43
|
+
["*( ... )", "*( ${1:content} )", "zero or more"],
|
|
44
|
+
["1*( ... )", "1*( ${1:content} )", "one or more"],
|
|
45
|
+
["!x", "!${1:x}", "negative lookahead"],
|
|
46
|
+
["&x", "&${1:x}", "positive lookahead"],
|
|
47
|
+
["!<x", "!< ${1:x}", "negative behind-guard"],
|
|
48
|
+
["&<x", "&< ${1:x}", "positive behind-guard"],
|
|
49
|
+
["as name", "as ${1:name}", "capture"],
|
|
50
|
+
].map do |label, insert, detail|
|
|
51
|
+
{ "label" => label, "kind" => 15, "detail" => detail,
|
|
52
|
+
"insertText" => insert, "insertTextFormat" => 2 }
|
|
53
|
+
end.freeze
|
|
54
|
+
|
|
20
55
|
def initialize(input = $stdin, output = $stdout)
|
|
21
56
|
@input = input
|
|
22
57
|
@output = output
|
|
23
58
|
@documents = {}
|
|
59
|
+
@parsed = {}
|
|
60
|
+
@import_paths = {}
|
|
24
61
|
end
|
|
25
62
|
|
|
26
63
|
def run
|
|
@@ -45,6 +82,19 @@ module Parsanol
|
|
|
45
82
|
"capabilities" => {
|
|
46
83
|
"textDocumentSync" => 1,
|
|
47
84
|
"hoverProvider" => true,
|
|
85
|
+
"definitionProvider" => true,
|
|
86
|
+
"referencesProvider" => true,
|
|
87
|
+
"codeActionProvider" => true,
|
|
88
|
+
"completionProvider" => {
|
|
89
|
+
"triggerCharacters" => [".", "%", "\""],
|
|
90
|
+
},
|
|
91
|
+
"semanticTokensProvider" => {
|
|
92
|
+
"legend" => {
|
|
93
|
+
"tokenTypes" => SEMANTIC_TYPES,
|
|
94
|
+
"tokenModifiers" => SEMANTIC_MODIFIERS,
|
|
95
|
+
},
|
|
96
|
+
"full" => true,
|
|
97
|
+
},
|
|
48
98
|
},
|
|
49
99
|
"serverInfo" => { "name" => "parsanol-parg-lsp" },
|
|
50
100
|
})
|
|
@@ -56,17 +106,32 @@ module Parsanol
|
|
|
56
106
|
doc = message["params"]["textDocument"]
|
|
57
107
|
@documents[doc["uri"]] =
|
|
58
108
|
doc["text"] || message.dig("params", "contentChanges", -1, "text")
|
|
109
|
+
@parsed.delete(doc["uri"])
|
|
59
110
|
publish_diagnostics(doc["uri"])
|
|
111
|
+
when "textDocument/didClose"
|
|
112
|
+
uri = message.dig("params", "textDocument", "uri")
|
|
113
|
+
@documents.delete(uri)
|
|
114
|
+
@parsed.delete(uri)
|
|
60
115
|
when "textDocument/codeAction"
|
|
61
116
|
respond(message, code_action(message["params"]))
|
|
62
117
|
when "textDocument/hover"
|
|
63
|
-
respond(message, hover(message
|
|
64
|
-
|
|
118
|
+
respond(message, hover(*position_params(message["params"])))
|
|
119
|
+
when "textDocument/definition"
|
|
120
|
+
respond(message, definition(*position_params(message["params"])))
|
|
121
|
+
when "textDocument/references"
|
|
122
|
+
params = message["params"]
|
|
123
|
+
respond(message, references(params.dig("textDocument", "uri"),
|
|
124
|
+
params["position"],
|
|
125
|
+
params.dig("context", "includeDeclaration")))
|
|
126
|
+
when "textDocument/completion"
|
|
127
|
+
respond(message, completion(message.dig("params", "textDocument", "uri")))
|
|
128
|
+
when "textDocument/semanticTokens/full"
|
|
129
|
+
respond(message, semantic_tokens(message.dig("params", "textDocument", "uri")))
|
|
65
130
|
end
|
|
66
131
|
end
|
|
67
132
|
|
|
68
133
|
def respond(message, result)
|
|
69
|
-
send_message("id" => message["id"], "result" => result)
|
|
134
|
+
send_message("jsonrpc" => "2.0", "id" => message["id"], "result" => result)
|
|
70
135
|
end
|
|
71
136
|
|
|
72
137
|
def notify(method, params)
|
|
@@ -79,12 +144,379 @@ module Parsanol
|
|
|
79
144
|
@output.flush
|
|
80
145
|
end
|
|
81
146
|
|
|
147
|
+
# ---- documents and imports -------------------------------------------
|
|
148
|
+
|
|
149
|
+
def position_params(params)
|
|
150
|
+
[params.dig("textDocument", "uri"), params["position"]]
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def document_source(uri)
|
|
154
|
+
return @documents[uri] if @documents.key?(uri)
|
|
155
|
+
|
|
156
|
+
path = @import_paths[uri] || file_path(uri)
|
|
157
|
+
return nil unless path && File.file?(path)
|
|
158
|
+
|
|
159
|
+
File.read(path)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def parsed(uri)
|
|
163
|
+
source = @documents[uri]
|
|
164
|
+
return nil unless source
|
|
165
|
+
|
|
166
|
+
cached = @parsed[uri]
|
|
167
|
+
return cached[1] if cached && cached[0] == source
|
|
168
|
+
|
|
169
|
+
document = begin
|
|
170
|
+
Parser.new(source).parse
|
|
171
|
+
rescue ParseError
|
|
172
|
+
nil
|
|
173
|
+
end
|
|
174
|
+
@parsed[uri] = [source, document]
|
|
175
|
+
document
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def file_path(uri)
|
|
179
|
+
text = uri.to_s
|
|
180
|
+
return nil unless text.start_with?("file://")
|
|
181
|
+
|
|
182
|
+
URI::DEFAULT_PARSER.unescape(text.delete_prefix("file://"))
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def import_dirs(uri)
|
|
186
|
+
dirs = []
|
|
187
|
+
path = file_path(uri)
|
|
188
|
+
dirs << File.dirname(path) if path
|
|
189
|
+
ENV.fetch("PG_IMPORT_DIR", "").split(File::PATH_SEPARATOR).each do |dir|
|
|
190
|
+
dirs << dir unless dir.to_s.empty?
|
|
191
|
+
end
|
|
192
|
+
dirs.uniq
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def import_uri(uri, name)
|
|
196
|
+
import_dirs(uri).each do |dir|
|
|
197
|
+
path = File.join(dir, "#{name}.parg")
|
|
198
|
+
next unless File.file?(path)
|
|
199
|
+
|
|
200
|
+
expanded = File.expand_path(path)
|
|
201
|
+
target = "file://#{URI::DEFAULT_PARSER.escape(expanded)}"
|
|
202
|
+
@import_paths[target] = expanded
|
|
203
|
+
return target
|
|
204
|
+
end
|
|
205
|
+
nil
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# ---- rule model ------------------------------------------------------
|
|
209
|
+
|
|
210
|
+
Definition = Struct.new(:line, :character, :name_length, :docs)
|
|
211
|
+
|
|
212
|
+
# Rule definitions with their preceding ## doc comments. Works on a
|
|
213
|
+
# source that does not parse: hover/definition stay useful mid-edit.
|
|
214
|
+
def rule_definitions(source)
|
|
215
|
+
defs = {}
|
|
216
|
+
lines = source.to_s.lines
|
|
217
|
+
lines.each_with_index do |line, index|
|
|
218
|
+
match = line.match(/\A(\s*)(?:atomic\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=/)
|
|
219
|
+
next unless match
|
|
220
|
+
|
|
221
|
+
name = match[2]
|
|
222
|
+
docs = []
|
|
223
|
+
cursor = index - 1
|
|
224
|
+
while cursor.negative? == false && lines[cursor].to_s.strip.start_with?("##")
|
|
225
|
+
docs.unshift(lines[cursor].strip.sub(/\A##\s?/, ""))
|
|
226
|
+
cursor -= 1
|
|
227
|
+
end
|
|
228
|
+
defs[name] = Definition.new(index, match.begin(2), name.length, docs.join("\n"))
|
|
229
|
+
end
|
|
230
|
+
defs
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def rule_names(uri)
|
|
234
|
+
document = parsed(uri)
|
|
235
|
+
return document.rules.keys if document
|
|
236
|
+
|
|
237
|
+
rule_definitions(document_source(uri)).keys
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# Resolve a (possibly dotted) rule reference to its defining file.
|
|
241
|
+
def resolve(uri, word)
|
|
242
|
+
segments = word.to_s.split(".").reject(&:empty?)
|
|
243
|
+
return nil if segments.empty?
|
|
244
|
+
|
|
245
|
+
resolve_segments(uri, segments)
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def resolve_segments(uri, segments)
|
|
249
|
+
source = document_source(uri)
|
|
250
|
+
return nil unless source
|
|
251
|
+
|
|
252
|
+
if segments.size == 1
|
|
253
|
+
name = segments.first
|
|
254
|
+
defs = rule_definitions(source)
|
|
255
|
+
definition = defs[name]
|
|
256
|
+
return nil unless definition
|
|
257
|
+
|
|
258
|
+
return { uri: uri, name: name, definition: definition }
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
document = if @documents.key?(uri)
|
|
262
|
+
parsed(uri)
|
|
263
|
+
else
|
|
264
|
+
begin
|
|
265
|
+
Parser.new(source).parse
|
|
266
|
+
rescue ParseError
|
|
267
|
+
nil
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
return nil unless document&.uses&.include?(segments.first)
|
|
271
|
+
|
|
272
|
+
target = import_uri(uri, segments.first)
|
|
273
|
+
return nil unless target
|
|
274
|
+
|
|
275
|
+
resolve_segments(target, segments[1..])
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def word_at(source, position)
|
|
279
|
+
return nil unless source && position
|
|
280
|
+
|
|
281
|
+
line = source.lines[position["line"].to_i].to_s
|
|
282
|
+
character = position["character"].to_i
|
|
283
|
+
left = line[0...character].to_s
|
|
284
|
+
right = line[character..].to_s
|
|
285
|
+
prefix = left[/[A-Za-z0-9_.]*\z/] || ""
|
|
286
|
+
suffix = right[/\A[A-Za-z0-9_.]*/] || ""
|
|
287
|
+
word = (prefix + suffix).sub(/\A\.+/, "").sub(/\.+\z/, "")
|
|
288
|
+
return nil if word.empty?
|
|
289
|
+
|
|
290
|
+
word
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def import_use_line?(source, position)
|
|
294
|
+
line = source.lines[position["line"].to_i].to_s
|
|
295
|
+
line.match?(/\A\s*use\s+/)
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
def location(uri, line, character, length)
|
|
299
|
+
{
|
|
300
|
+
"uri" => uri,
|
|
301
|
+
"range" => {
|
|
302
|
+
"start" => { "line" => line, "character" => character },
|
|
303
|
+
"end" => { "line" => line, "character" => character + length },
|
|
304
|
+
},
|
|
305
|
+
}
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
# ---- hover / definition / references ---------------------------------
|
|
309
|
+
|
|
310
|
+
def hover(uri, position)
|
|
311
|
+
source = document_source(uri)
|
|
312
|
+
return nil unless source && position
|
|
313
|
+
|
|
314
|
+
word = word_at(source, position)
|
|
315
|
+
return nil unless word
|
|
316
|
+
|
|
317
|
+
if import_use_line?(source, position)
|
|
318
|
+
target = import_uri(uri, word)
|
|
319
|
+
return nil unless target
|
|
320
|
+
|
|
321
|
+
return { "contents" => { "kind" => "markdown",
|
|
322
|
+
"value" => "Import `#{word}`\n\n`#{File.basename(target)}`" } }
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
resolved = resolve(uri, word)
|
|
326
|
+
return nil unless resolved
|
|
327
|
+
|
|
328
|
+
definition = resolved[:definition]
|
|
329
|
+
return nil unless definition
|
|
330
|
+
|
|
331
|
+
value = if definition.docs.empty?
|
|
332
|
+
"`#{resolved[:name]}` — rule"
|
|
333
|
+
else
|
|
334
|
+
"`#{resolved[:name]}`\n\n#{definition.docs}"
|
|
335
|
+
end
|
|
336
|
+
{ "contents" => { "kind" => "markdown", "value" => value } }
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
def definition(uri, position)
|
|
340
|
+
source = document_source(uri)
|
|
341
|
+
return nil unless source && position
|
|
342
|
+
|
|
343
|
+
word = word_at(source, position)
|
|
344
|
+
return nil unless word
|
|
345
|
+
|
|
346
|
+
if import_use_line?(source, position)
|
|
347
|
+
target = import_uri(uri, word)
|
|
348
|
+
return target ? location(target, 0, 0, 0) : nil
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
resolved = resolve(uri, word)
|
|
352
|
+
return nil unless resolved
|
|
353
|
+
|
|
354
|
+
definition = resolved[:definition]
|
|
355
|
+
location(resolved[:uri], definition.line, definition.character,
|
|
356
|
+
definition.name_length)
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
def references(uri, position, include_declaration)
|
|
360
|
+
source = document_source(uri)
|
|
361
|
+
return nil unless source && position
|
|
362
|
+
|
|
363
|
+
word = word_at(source, position)
|
|
364
|
+
resolved = word && resolve(uri, word)
|
|
365
|
+
return nil unless resolved
|
|
366
|
+
|
|
367
|
+
name = resolved[:name]
|
|
368
|
+
uris = [resolved[:uri], uri].uniq
|
|
369
|
+
results = []
|
|
370
|
+
uris.each do |target_uri|
|
|
371
|
+
target_source = document_source(target_uri)
|
|
372
|
+
next unless target_source
|
|
373
|
+
|
|
374
|
+
qualified = target_uri == uri && resolved[:uri] != uri && word.include?(".")
|
|
375
|
+
needle = qualified ? word : name
|
|
376
|
+
declaration = rule_definitions(target_source)[name]
|
|
377
|
+
target_source.lines.each_with_index do |line, index|
|
|
378
|
+
identifier_occurrences(line, needle).each do |character, length|
|
|
379
|
+
declaration_here = declaration && declaration.line == index &&
|
|
380
|
+
character == declaration.character && needle == name
|
|
381
|
+
results << location(target_uri, index, character, length) if
|
|
382
|
+
include_declaration || !declaration_here
|
|
383
|
+
end
|
|
384
|
+
end
|
|
385
|
+
end
|
|
386
|
+
results
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
# Identifier tokens outside strings and comments: a rule name inside a
|
|
390
|
+
# literal or a doc comment is not a reference.
|
|
391
|
+
def identifier_occurrences(line, name)
|
|
392
|
+
found = []
|
|
393
|
+
scanner = StringScanner.new(line)
|
|
394
|
+
until scanner.eos?
|
|
395
|
+
start = scanner.pos
|
|
396
|
+
start_char = line.byteslice(0, start).length
|
|
397
|
+
if scanner.scan(/#.*/)
|
|
398
|
+
break
|
|
399
|
+
elsif scanner.scan(/%(?:i|s)?"(?:\\.|[^"\\])*"/) ||
|
|
400
|
+
scanner.scan(/"(?:\\.|[^"\\])*"/) ||
|
|
401
|
+
scanner.scan(/%x[0-9A-Fa-f]{2}(?:[.-][0-9A-Fa-f]{2})*|[0-9]+/) ||
|
|
402
|
+
scanner.scan(/\s+/)
|
|
403
|
+
next
|
|
404
|
+
elsif (text = scanner.scan(/[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*/))
|
|
405
|
+
found << [start_char, text.length] if text == name
|
|
406
|
+
else
|
|
407
|
+
scanner.getch
|
|
408
|
+
end
|
|
409
|
+
end
|
|
410
|
+
found
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
# ---- completion ------------------------------------------------------
|
|
414
|
+
|
|
415
|
+
def completion(uri)
|
|
416
|
+
source = document_source(uri)
|
|
417
|
+
return nil unless source
|
|
418
|
+
|
|
419
|
+
items = COMPLETION_KEYWORDS.dup + SNIPPETS.dup
|
|
420
|
+
rule_names(uri).each do |name|
|
|
421
|
+
items << { "label" => name, "kind" => 18, "detail" => "rule" }
|
|
422
|
+
end
|
|
423
|
+
document = parsed(uri)
|
|
424
|
+
(document&.uses || []).each do |import|
|
|
425
|
+
target = import_uri(uri, import)
|
|
426
|
+
next unless target
|
|
427
|
+
|
|
428
|
+
rule_definitions(document_source(target)).each_key do |name|
|
|
429
|
+
items << { "label" => "#{import}.#{name}", "kind" => 18,
|
|
430
|
+
"detail" => "imported rule" }
|
|
431
|
+
end
|
|
432
|
+
end
|
|
433
|
+
{ "isIncomplete" => false, "items" => items }
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
# ---- semantic tokens -------------------------------------------------
|
|
437
|
+
|
|
438
|
+
def semantic_tokens(uri)
|
|
439
|
+
source = document_source(uri)
|
|
440
|
+
return nil unless source
|
|
441
|
+
|
|
442
|
+
names = rule_names(uri)
|
|
443
|
+
tokens = []
|
|
444
|
+
source.lines.each_with_index do |line, line_no|
|
|
445
|
+
scan_line(line, line_no, names, tokens)
|
|
446
|
+
end
|
|
447
|
+
{ "data" => encode_tokens(tokens) }
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
def scan_line(line, line_no, names, tokens)
|
|
451
|
+
scanner = StringScanner.new(line)
|
|
452
|
+
previous_word = nil
|
|
453
|
+
until scanner.eos?
|
|
454
|
+
start = scanner.pos
|
|
455
|
+
start_char = line.byteslice(0, start).length
|
|
456
|
+
|
|
457
|
+
if (text = scanner.scan(/#.*/))
|
|
458
|
+
tokens << [line_no, start_char, text.length, "comment", 0]
|
|
459
|
+
break
|
|
460
|
+
elsif (text = scanner.scan(/%(?:i|s)?"(?:\\.|[^"\\])*"|"(?:\\.|[^"\\])*"/))
|
|
461
|
+
tokens << [line_no, start_char, text.length, "string", 0]
|
|
462
|
+
previous_word = nil
|
|
463
|
+
elsif (text = scanner.scan(/%x[0-9A-Fa-f]{2}(?:[.-][0-9A-Fa-f]{2})*|[0-9]+/))
|
|
464
|
+
tokens << [line_no, start_char, text.length, "number", 0]
|
|
465
|
+
previous_word = nil
|
|
466
|
+
elsif (text = scanner.scan(/[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*/))
|
|
467
|
+
type, modifiers = classify_word(line, start_char, text, names, previous_word)
|
|
468
|
+
tokens << [line_no, start_char, text.length, type, modifiers] if type
|
|
469
|
+
previous_word = text
|
|
470
|
+
elsif (text = scanner.scan(/\s+/))
|
|
471
|
+
_ = text
|
|
472
|
+
else
|
|
473
|
+
scanner.getch
|
|
474
|
+
previous_word = nil
|
|
475
|
+
end
|
|
476
|
+
end
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
def classify_word(line, start_char, text, names, previous_word)
|
|
480
|
+
base = text.split(".").last
|
|
481
|
+
if KEYWORDS.include?(text)
|
|
482
|
+
["keyword", 0]
|
|
483
|
+
elsif previous_word == "as"
|
|
484
|
+
["capture", 0]
|
|
485
|
+
elsif names.include?(base) || text.include?(".")
|
|
486
|
+
modifier = declaration_here?(line, start_char, text) ? DECLARATION_BIT : 0
|
|
487
|
+
["rule", modifier]
|
|
488
|
+
elsif %w[entry bindings test].include?(previous_word)
|
|
489
|
+
["rule", DECLARATION_BIT]
|
|
490
|
+
end
|
|
491
|
+
end
|
|
492
|
+
|
|
493
|
+
def declaration_here?(line, start_char, text)
|
|
494
|
+
before = line[0...start_char].to_s
|
|
495
|
+
after = line[(start_char + text.length)..].to_s
|
|
496
|
+
before.match?(/\A\s*(?:atomic\s+)?\z/) && after.match?(/\A\s*=/)
|
|
497
|
+
end
|
|
498
|
+
|
|
499
|
+
def encode_tokens(tokens)
|
|
500
|
+
data = []
|
|
501
|
+
last_line = 0
|
|
502
|
+
last_char = 0
|
|
503
|
+
tokens.sort_by { |token| [token[0], token[1]] }.each do |line, char, length, type, modifiers|
|
|
504
|
+
delta_line = line - last_line
|
|
505
|
+
delta_char = delta_line.zero? ? char - last_char : char
|
|
506
|
+
data.push(delta_line, delta_char, length, SEMANTIC_TYPES.index(type), modifiers)
|
|
507
|
+
last_line = line
|
|
508
|
+
last_char = char
|
|
509
|
+
end
|
|
510
|
+
data
|
|
511
|
+
end
|
|
512
|
+
|
|
513
|
+
# ---- diagnostics -----------------------------------------------------
|
|
514
|
+
|
|
82
515
|
def publish_diagnostics(uri)
|
|
83
516
|
source = @documents[uri]
|
|
84
517
|
diagnostics = []
|
|
85
|
-
document =
|
|
86
|
-
|
|
87
|
-
document = Parser.new(source).parse
|
|
518
|
+
document = begin
|
|
519
|
+
Parser.new(source).parse
|
|
88
520
|
rescue ParseError => e
|
|
89
521
|
line = 0
|
|
90
522
|
# The self-hosting artifact yields a precise offset (F7 wire);
|
|
@@ -97,16 +529,23 @@ module Parsanol
|
|
|
97
529
|
end
|
|
98
530
|
end
|
|
99
531
|
diagnostics << diagnostic(line, e.message)
|
|
532
|
+
nil
|
|
100
533
|
end
|
|
101
534
|
if document
|
|
535
|
+
dirs = import_dirs(uri)
|
|
102
536
|
begin
|
|
103
|
-
|
|
537
|
+
Imports.merge!(document, dirs) if document.uses.any?
|
|
538
|
+
rescue PARG::Error, ParseError => e
|
|
539
|
+
diagnostics << diagnostic(use_line(source, document), e.message)
|
|
540
|
+
end
|
|
541
|
+
begin
|
|
542
|
+
tables_dir = ENV.fetch("PG_TABLES_DIR", nil) || dirs.first
|
|
104
543
|
compiler = Compiler.new(document, tables_dir)
|
|
105
544
|
document.rules.each_key { |rule| compiler.atom_for(rule) }
|
|
106
545
|
Lints.errors(document, compiler).each do |error|
|
|
107
546
|
diagnostics << diagnostic(rule_line(source, error[/\Arule (\w+):/, 1]), error)
|
|
108
547
|
end
|
|
109
|
-
compiler.
|
|
548
|
+
compiler.test_failures.each do |failure|
|
|
110
549
|
diagnostics << diagnostic(0, failure)
|
|
111
550
|
end
|
|
112
551
|
rescue ParseError, PARG::Error => e
|
|
@@ -117,11 +556,18 @@ module Parsanol
|
|
|
117
556
|
"uri" => uri, "diagnostics" => diagnostics)
|
|
118
557
|
end
|
|
119
558
|
|
|
559
|
+
def use_line(source, document)
|
|
560
|
+
name = document.uses.first
|
|
561
|
+
return 0 unless name
|
|
562
|
+
|
|
563
|
+
source.lines.index { |line| line.match?(/^\s*use\s+#{Regexp.escape(name)}\b/) } || 0
|
|
564
|
+
end
|
|
565
|
+
|
|
120
566
|
# Rule-granular position: the line the named rule is defined on.
|
|
121
567
|
def rule_line(source, rule_name)
|
|
122
568
|
return 0 unless rule_name
|
|
123
569
|
|
|
124
|
-
source.lines.index { |
|
|
570
|
+
source.lines.index { |line| line.match?(/^\s*#{rule_name}\s*=/) } || 0
|
|
125
571
|
end
|
|
126
572
|
|
|
127
573
|
def diagnostic(line, message)
|
|
@@ -134,6 +580,8 @@ module Parsanol
|
|
|
134
580
|
}
|
|
135
581
|
end
|
|
136
582
|
|
|
583
|
+
# ---- code action -----------------------------------------------------
|
|
584
|
+
|
|
137
585
|
# Reorder code action: only when EVERY top-level alternative of the
|
|
138
586
|
# rule is a quoted literal is the reorder mechanically safe.
|
|
139
587
|
def code_action(params)
|
|
@@ -176,33 +624,6 @@ module Parsanol
|
|
|
176
624
|
{ "start" => { "line" => line_no, "character" => 0 },
|
|
177
625
|
"end" => { "line" => line_no, "character" => 1000 } }
|
|
178
626
|
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
627
|
end
|
|
207
628
|
end
|
|
208
629
|
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Parsanol
|
|
4
|
+
# Recovery journaling (TODO.sota/1): when enabled for the current
|
|
5
|
+
# thread, every Named capture appends to a shared journal for the
|
|
6
|
+
# duration of the recovery re-parse. Success paths never enable it —
|
|
7
|
+
# normal parses see only a nil-context check.
|
|
8
|
+
module Recovery
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
def start_journal
|
|
12
|
+
Thread.current[:parsanol_recovery_journal] = []
|
|
13
|
+
Thread.current[:parsanol_recovery_journaling] = true
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Stops journaling and returns the journal: [name, value] pairs in
|
|
17
|
+
# capture order, including captures from branches that later failed
|
|
18
|
+
# (best-effort superset — same semantics as the rs recovery
|
|
19
|
+
# journal).
|
|
20
|
+
def stop_journal
|
|
21
|
+
Thread.current[:parsanol_recovery_journaling] = false
|
|
22
|
+
journal
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def journal
|
|
26
|
+
Thread.current[:parsanol_recovery_journal] || []
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def journaling?
|
|
30
|
+
Thread.current[:parsanol_recovery_journaling] == true
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
data/lib/parsanol/version.rb
CHANGED
data/lib/parsanol.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: parsanol
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.3.
|
|
4
|
+
version: 1.3.97
|
|
5
5
|
platform: aarch64-linux
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-10-
|
|
11
|
+
date: 2026-10-20 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description: A small Ruby library for constructing parsers in the PEG (Parsing Expression
|
|
14
14
|
Grammar) fashion. Parsanol provides Parslet-compatible API with additional features
|
|
@@ -133,6 +133,7 @@ files:
|
|
|
133
133
|
- lib/parsanol/pools/position_pool.rb
|
|
134
134
|
- lib/parsanol/pools/slice_pool.rb
|
|
135
135
|
- lib/parsanol/position.rb
|
|
136
|
+
- lib/parsanol/recovery.rb
|
|
136
137
|
- lib/parsanol/resettable.rb
|
|
137
138
|
- lib/parsanol/result.rb
|
|
138
139
|
- lib/parsanol/result_builder.rb
|