expressir 2.4.35-arm-linux → 2.4.37-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/.github/workflows/conformance.yml +34 -0
- data/.gitignore +3 -0
- data/Gemfile +1 -1
- data/TODO.sota/00-overview.md +57 -0
- data/TODO.sota/01-type-checking.md +115 -0
- data/TODO.sota/02-evaluator.md +86 -0
- data/TODO.sota/03-conformance.md +53 -0
- data/TODO.sota/04-lsp.md +70 -0
- data/TODO.sota/05-interop.md +55 -0
- data/TODO.sota/06-dialects.md +57 -0
- data/TODO.sota/07-reference-artifacts.md +55 -0
- data/docs/_guides/interop/index.adoc +25 -0
- data/docs/_guides/interop/json-schema.adoc +88 -0
- data/docs/_guides/interop/owl-rdf.adoc +87 -0
- data/docs/_guides/reference/checker-notes.adoc +55 -0
- data/docs/_guides/reference/checker.adoc +36 -0
- data/docs/_guides/reference/evaluator.adoc +40 -0
- data/docs/_guides/reference/grammar-crosswalk.adoc +299 -0
- data/docs/_guides/reference/index.adoc +22 -0
- data/docs/_guides/reference/lsp.adoc +41 -0
- data/docs/_guides/reference/p21.adoc +30 -0
- data/docs/_guides/reference/shtolo.adoc +36 -0
- data/exe/expressir-lsp +6 -0
- data/expressir.gemspec +2 -2
- data/lib/expressir/3.2/expressir_core.so +0 -0
- data/lib/expressir/3.3/expressir_core.so +0 -0
- data/lib/expressir/3.4/expressir_core.so +0 -0
- data/lib/expressir/4.0/expressir_core.so +0 -0
- data/lib/expressir/cli.rb +18 -0
- data/lib/expressir/commands/check.rb +3 -1
- data/lib/expressir/commands/export_json_schema.rb +29 -0
- data/lib/expressir/commands/export_owl.rb +26 -0
- data/lib/expressir/commands/validate.rb +3 -0
- data/lib/expressir/commands.rb +2 -0
- data/lib/expressir/conformance/trend.rb +100 -0
- data/lib/expressir/conformance.rb +8 -0
- data/lib/expressir/eval/environment.rb +83 -0
- data/lib/expressir/eval/expression.rb +610 -0
- data/lib/expressir/eval/interpreter.rb +316 -0
- data/lib/expressir/eval/rules.rb +88 -0
- data/lib/expressir/eval/values.rb +74 -0
- data/lib/expressir/eval.rb +17 -0
- data/lib/expressir/express/builders/expression_builder.rb +9 -1
- data/lib/expressir/express/checker.rb +779 -2
- data/lib/expressir/express/dialect.rb +44 -0
- data/lib/expressir/express/formatters/expressions_formatter.rb +4 -1
- data/lib/expressir/express/parser.rb +9 -4
- data/lib/expressir/express.rb +1 -0
- data/lib/expressir/interop/json_schema.rb +195 -0
- data/lib/expressir/interop/owl.rb +216 -0
- data/lib/expressir/interop.rb +13 -0
- data/lib/expressir/lsp/document.rb +226 -0
- data/lib/expressir/lsp/server.rb +184 -0
- data/lib/expressir/lsp/workspace.rb +402 -0
- data/lib/expressir/lsp.rb +11 -0
- data/lib/expressir/mapping/refpath.rb +65 -4
- data/lib/expressir/reference/grammar_crosswalk.rb +122 -0
- data/lib/expressir/reference/grammar_crosswalk.yml +800 -0
- data/lib/expressir/reference/note_catalog.rb +54 -0
- data/lib/expressir/reference.rb +10 -0
- data/lib/expressir/version.rb +1 -1
- data/lib/expressir.rb +4 -0
- data/lib/tasks/conformance.rake +27 -0
- data/lib/tasks/reference.rake +25 -0
- metadata +50 -4
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Expressir
|
|
4
|
+
module Lsp
|
|
5
|
+
# An open document: source text, last good AST, and the LSP position
|
|
6
|
+
# translations that the protocol needs. Offsets crossing the boundary
|
|
7
|
+
# are byte offsets into the UTF-8 text; LSP positions are 0-based
|
|
8
|
+
# UTF-16 line/character pairs.
|
|
9
|
+
class Document
|
|
10
|
+
IDENTIFIER = /[A-Za-z][A-Za-z0-9_]*/
|
|
11
|
+
NAME_WINDOW = 1024
|
|
12
|
+
|
|
13
|
+
Token = Struct.new(:text, :start_byte, :byte_length, keyword_init: true) do
|
|
14
|
+
def end_byte
|
|
15
|
+
start_byte + byte_length
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
SYMBOL_KINDS = {
|
|
20
|
+
Model::Declarations::Schema => 3, # Namespace
|
|
21
|
+
Model::Declarations::Entity => 5, # Class
|
|
22
|
+
Model::Declarations::Type => 26, # TypeParameter
|
|
23
|
+
Model::Declarations::Function => 12, # Function
|
|
24
|
+
Model::Declarations::Procedure => 6, # Method
|
|
25
|
+
Model::Declarations::Rule => 11, # Interface
|
|
26
|
+
Model::Declarations::Constant => 14, # Constant
|
|
27
|
+
Model::Declarations::Attribute => 8, # Field
|
|
28
|
+
Model::Declarations::Variable => 13, # Variable
|
|
29
|
+
}.freeze
|
|
30
|
+
|
|
31
|
+
attr_reader :uri, :path, :text, :file, :parse_error
|
|
32
|
+
|
|
33
|
+
def initialize(uri:, text:, path: nil)
|
|
34
|
+
@uri = uri
|
|
35
|
+
@path = path || uri_path(uri)
|
|
36
|
+
@text = text.to_s
|
|
37
|
+
@parse_error = nil
|
|
38
|
+
parse!
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def update(text)
|
|
42
|
+
text = text.to_s
|
|
43
|
+
return if text == @text
|
|
44
|
+
|
|
45
|
+
@text = text
|
|
46
|
+
parse!
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def schemas
|
|
50
|
+
Array(@file&.schemas)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def schema_containing(byte)
|
|
54
|
+
schemas.select { |schema| schema.source_offset.to_i <= byte }
|
|
55
|
+
.max_by(&:source_offset)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def symbols
|
|
59
|
+
return [] unless @file
|
|
60
|
+
|
|
61
|
+
position_index.nodes.filter_map do |entry|
|
|
62
|
+
node = entry[:node]
|
|
63
|
+
kind = SYMBOL_KINDS[node.class]
|
|
64
|
+
next unless kind && node.respond_to?(:id) && node.id && entry[:line]
|
|
65
|
+
|
|
66
|
+
{
|
|
67
|
+
"name" => node.id.to_s,
|
|
68
|
+
"kind" => kind,
|
|
69
|
+
"range" => line_range(entry[:line], entry[:end_line] || entry[:line]),
|
|
70
|
+
"selectionRange" => range_for(node),
|
|
71
|
+
}
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def reference_nodes
|
|
76
|
+
nodes_of_type(Model::References::SimpleReference)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def declaration_nodes
|
|
80
|
+
nodes_of_type(Model::ModelElement).select do |node|
|
|
81
|
+
SYMBOL_KINDS.key?(node.class) && node.respond_to?(:id) && node.id
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def token_at(position)
|
|
86
|
+
byte = byte_offset(position["line"], position["character"])
|
|
87
|
+
return nil unless byte
|
|
88
|
+
|
|
89
|
+
line_start = line_starts[position["line"]]
|
|
90
|
+
line_text = text.byteslice(line_start,
|
|
91
|
+
line_end_byte(position["line"]) - line_start)
|
|
92
|
+
column = byte - line_start
|
|
93
|
+
matches = line_text.to_s.b.to_enum(:scan, IDENTIFIER)
|
|
94
|
+
matches.each do
|
|
95
|
+
match = Regexp.last_match
|
|
96
|
+
next unless column.between?(match.begin(0), match.end(0))
|
|
97
|
+
|
|
98
|
+
return Token.new(text: match[0],
|
|
99
|
+
start_byte: line_start + match.begin(0),
|
|
100
|
+
byte_length: match[0].bytesize)
|
|
101
|
+
end
|
|
102
|
+
nil
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def position(byte)
|
|
106
|
+
byte = 0 if byte.nil? || byte.negative?
|
|
107
|
+
byte = text.bytesize if byte > text.bytesize
|
|
108
|
+
line = line_starts.bsearch_index { |start| start > byte }
|
|
109
|
+
line = line ? line - 1 : line_starts.length - 1
|
|
110
|
+
prefix = text.byteslice(line_starts[line], byte - line_starts[line]).to_s
|
|
111
|
+
{ "line" => line, "character" => utf16_length(prefix) }
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def range(start_byte, length)
|
|
115
|
+
{ "start" => position(start_byte),
|
|
116
|
+
"end" => position(start_byte + length) }
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def range_for(node)
|
|
120
|
+
length = node.respond_to?(:id) && node.id ? node.id.to_s.bytesize : 1
|
|
121
|
+
range(name_offset(node), length)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def name_offset(node)
|
|
125
|
+
offset = node.source_offset.to_i
|
|
126
|
+
return offset if node.is_a?(Model::Declarations::Attribute)
|
|
127
|
+
|
|
128
|
+
if node.respond_to?(:id) && node.id
|
|
129
|
+
window = text.byteslice(offset, NAME_WINDOW).to_s
|
|
130
|
+
match = /\b#{Regexp.escape(node.id.to_s)}\b/i.match(window)
|
|
131
|
+
return offset + match.begin(0) if match
|
|
132
|
+
end
|
|
133
|
+
offset
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def line_range(start_line, end_line)
|
|
137
|
+
start_byte = line_starts[start_line - 1] || text.bytesize
|
|
138
|
+
end_byte = line_end_byte(end_line - 1)
|
|
139
|
+
{ "start" => position(start_byte), "end" => position(end_byte) }
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def byte_offset(line, character)
|
|
143
|
+
start = line_starts[line]
|
|
144
|
+
return nil unless start
|
|
145
|
+
|
|
146
|
+
byte = start
|
|
147
|
+
units = 0
|
|
148
|
+
text.byteslice(start, line_end_byte(line) - start).to_s.each_char do |char|
|
|
149
|
+
break if units >= character
|
|
150
|
+
|
|
151
|
+
units += utf16_code_units(char)
|
|
152
|
+
byte += char.bytesize
|
|
153
|
+
end
|
|
154
|
+
byte
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
private
|
|
158
|
+
|
|
159
|
+
def parse!
|
|
160
|
+
@file = Expressir::Express::Parser.from_exp(@text,
|
|
161
|
+
include_source: true)
|
|
162
|
+
@collector = nil
|
|
163
|
+
@line_starts = nil
|
|
164
|
+
@parse_error = nil
|
|
165
|
+
rescue StandardError => e
|
|
166
|
+
@parse_error = e
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def position_index
|
|
170
|
+
Express::NodePositionIndex.new(@file, Express::LineMap.new(@text.b))
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def nodes_of_type(type)
|
|
174
|
+
collector.nodes.grep(type)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def collector
|
|
178
|
+
@collector ||= Collector.new.tap { |instance| instance.visit(@file) }
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
class Collector < Express::ModelVisitor
|
|
182
|
+
attr_reader :nodes
|
|
183
|
+
|
|
184
|
+
def initialize
|
|
185
|
+
super
|
|
186
|
+
@nodes = []
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def visit(node)
|
|
190
|
+
@nodes << node
|
|
191
|
+
super
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def line_starts
|
|
196
|
+
@line_starts ||= begin
|
|
197
|
+
bytes = @text.b
|
|
198
|
+
starts = [0]
|
|
199
|
+
index = 0
|
|
200
|
+
while (index = bytes.index("\n", index))
|
|
201
|
+
starts << (index + 1)
|
|
202
|
+
index += 1
|
|
203
|
+
end
|
|
204
|
+
starts
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def line_end_byte(line)
|
|
209
|
+
following = line_starts[line + 1]
|
|
210
|
+
following ? following - 1 : @text.bytesize
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def utf16_length(string)
|
|
214
|
+
string.each_char.sum { |char| utf16_code_units(char) }
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def utf16_code_units(char)
|
|
218
|
+
char.ord > 0xFFFF ? 2 : 1
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def uri_path(uri)
|
|
222
|
+
uri.to_s.delete_prefix("file://")
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
end
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Expressir
|
|
6
|
+
module Lsp
|
|
7
|
+
# JSON-RPC 2.0 transport over stdio (or any IO pair) implementing the
|
|
8
|
+
# LSP 3.17 subset: lifecycle, text sync, diagnostics, symbols,
|
|
9
|
+
# definition, references, hover and rename.
|
|
10
|
+
class Server
|
|
11
|
+
SUPPORTED_CHARACTERS = "utf-16"
|
|
12
|
+
|
|
13
|
+
def initialize(input: $stdin, output: $stdout)
|
|
14
|
+
@input = input
|
|
15
|
+
@output = output
|
|
16
|
+
@workspace = Workspace.new
|
|
17
|
+
@shutdown = false
|
|
18
|
+
@exit = false
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def run
|
|
22
|
+
while (message = read_message)
|
|
23
|
+
dispatch(message)
|
|
24
|
+
break if @exit
|
|
25
|
+
end
|
|
26
|
+
@shutdown ? 0 : 1
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def dispatch(message)
|
|
32
|
+
method = message["method"]
|
|
33
|
+
return respond_error(message, -32601, "unknown method #{method}") if method.nil?
|
|
34
|
+
|
|
35
|
+
case method
|
|
36
|
+
when "initialize" then respond(message, initialize_result)
|
|
37
|
+
when "initialized", "$/cancelRequest" then nil
|
|
38
|
+
when "shutdown"
|
|
39
|
+
@shutdown = true
|
|
40
|
+
respond(message, nil)
|
|
41
|
+
when "exit" then @exit = true
|
|
42
|
+
when "textDocument/didOpen" then did_open(message["params"])
|
|
43
|
+
when "textDocument/didChange" then did_change(message["params"])
|
|
44
|
+
when "textDocument/didSave" then did_save(message["params"])
|
|
45
|
+
when "textDocument/didClose" then did_close(message["params"])
|
|
46
|
+
when "textDocument/documentSymbol"
|
|
47
|
+
respond(message, @workspace.symbols(uri_of(message)))
|
|
48
|
+
when "textDocument/definition"
|
|
49
|
+
respond(message, @workspace.definition(uri_of(message), position_of(message)))
|
|
50
|
+
when "textDocument/references"
|
|
51
|
+
respond(message, @workspace.references(
|
|
52
|
+
uri_of(message), position_of(message),
|
|
53
|
+
include_declaration: include_declaration?(message)
|
|
54
|
+
))
|
|
55
|
+
when "textDocument/hover"
|
|
56
|
+
respond(message, @workspace.hover(uri_of(message), position_of(message)))
|
|
57
|
+
when "textDocument/prepareRename"
|
|
58
|
+
respond(message, @workspace.prepare_rename(uri_of(message), position_of(message)))
|
|
59
|
+
when "textDocument/rename"
|
|
60
|
+
respond(message, @workspace.rename(
|
|
61
|
+
uri_of(message), position_of(message),
|
|
62
|
+
message.dig("params", "newName").to_s
|
|
63
|
+
))
|
|
64
|
+
else
|
|
65
|
+
respond_error(message, -32601, "unknown method #{method}") if message["id"]
|
|
66
|
+
end
|
|
67
|
+
rescue StandardError => e
|
|
68
|
+
respond_error(message, -32603, e.message) if message["id"]
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def initialize_result
|
|
72
|
+
{
|
|
73
|
+
"capabilities" => {
|
|
74
|
+
"positionEncoding" => SUPPORTED_CHARACTERS,
|
|
75
|
+
"textDocumentSync" => {
|
|
76
|
+
"openClose" => true,
|
|
77
|
+
"change" => 1,
|
|
78
|
+
"save" => { "includeText" => false },
|
|
79
|
+
},
|
|
80
|
+
"documentSymbolProvider" => true,
|
|
81
|
+
"definitionProvider" => true,
|
|
82
|
+
"referencesProvider" => true,
|
|
83
|
+
"hoverProvider" => true,
|
|
84
|
+
"renameProvider" => { "prepareProvider" => true },
|
|
85
|
+
},
|
|
86
|
+
"serverInfo" => {
|
|
87
|
+
"name" => "expressir-lsp",
|
|
88
|
+
"version" => Expressir::VERSION,
|
|
89
|
+
},
|
|
90
|
+
}
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def did_open(params)
|
|
94
|
+
document = params.fetch("textDocument")
|
|
95
|
+
@workspace.open(uri: document.fetch("uri"),
|
|
96
|
+
text: document.fetch("text", ""))
|
|
97
|
+
publish_diagnostics(document.fetch("uri"))
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def did_change(params)
|
|
101
|
+
uri = params.dig("textDocument", "uri")
|
|
102
|
+
change = Array(params["contentChanges"]).last || {}
|
|
103
|
+
@workspace.change(uri: uri, text: change.fetch("text", ""))
|
|
104
|
+
publish_diagnostics(uri)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def did_save(params)
|
|
108
|
+
publish_diagnostics(params.dig("textDocument", "uri"))
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def did_close(params)
|
|
112
|
+
uri = params.dig("textDocument", "uri")
|
|
113
|
+
@workspace.close(uri)
|
|
114
|
+
notify("textDocument/publishDiagnostics",
|
|
115
|
+
"uri" => uri, "diagnostics" => [])
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def publish_diagnostics(uri)
|
|
119
|
+
return unless uri
|
|
120
|
+
|
|
121
|
+
notify("textDocument/publishDiagnostics",
|
|
122
|
+
"uri" => uri,
|
|
123
|
+
"diagnostics" => @workspace.diagnostics(uri))
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def uri_of(message)
|
|
127
|
+
message.dig("params", "textDocument", "uri")
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def position_of(message)
|
|
131
|
+
message.dig("params", "position") || { "line" => 0, "character" => 0 }
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def include_declaration?(message)
|
|
135
|
+
message.dig("params", "context", "includeDeclaration") != false
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def respond(message, result)
|
|
139
|
+
write("jsonrpc" => "2.0", "id" => message["id"], "result" => result)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def respond_error(message, code, text)
|
|
143
|
+
write("jsonrpc" => "2.0", "id" => message["id"],
|
|
144
|
+
"error" => { "code" => code, "message" => text.to_s })
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def notify(method, params)
|
|
148
|
+
write("jsonrpc" => "2.0", "method" => method, "params" => params)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def read_message
|
|
152
|
+
headers = read_headers
|
|
153
|
+
return nil unless headers
|
|
154
|
+
|
|
155
|
+
length = headers["content-length"].to_i
|
|
156
|
+
body = @input.read(length)
|
|
157
|
+
return nil if body.nil? || body.empty?
|
|
158
|
+
|
|
159
|
+
JSON.parse(body)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def read_headers
|
|
163
|
+
headers = {}
|
|
164
|
+
loop do
|
|
165
|
+
line = @input.gets
|
|
166
|
+
return nil unless line
|
|
167
|
+
|
|
168
|
+
line = line.sub(/\r?\n\z/, "")
|
|
169
|
+
break if line.empty?
|
|
170
|
+
|
|
171
|
+
name, value = line.split(":", 2)
|
|
172
|
+
headers[name.downcase] = value.to_s.strip
|
|
173
|
+
end
|
|
174
|
+
headers
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def write(payload)
|
|
178
|
+
json = JSON.generate(payload)
|
|
179
|
+
@output.write("Content-Length: #{json.bytesize}\r\n\r\n#{json}")
|
|
180
|
+
@output.flush
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|