Quantar 0.1.0__py3-none-any.whl
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.
- lang_systems/__init__.py +38 -0
- lang_systems/ast_builder.py +272 -0
- lang_systems/lsp/__init__.py +1 -0
- lang_systems/lsp/__main__.py +3 -0
- lang_systems/lsp/server.py +247 -0
- lang_systems/operators.py +367 -0
- lang_systems/pegen_gram_gen.py +159 -0
- lang_systems/quantar.gram +27 -0
- lang_systems/quantar.py +63 -0
- lang_systems/quantar_parser.py +11729 -0
- lang_systems/quantar_tokens.py +87 -0
- lang_systems/runner.py +55 -0
- lang_systems/runtime.py +63 -0
- lang_systems/visitors.py +30 -0
- quantar-0.1.0.dist-info/METADATA +399 -0
- quantar-0.1.0.dist-info/RECORD +39 -0
- quantar-0.1.0.dist-info/WHEEL +4 -0
- quantar-0.1.0.dist-info/entry_points.txt +3 -0
- quantar-0.1.0.dist-info/licenses/LICENSE +201 -0
- quantum/__init__.py +60 -0
- quantum/basic_funcs.py +262 -0
- quantum/composites.py +33 -0
- quantum/gate_abs.py +127 -0
- quantum/gate_matrix.py +171 -0
- quantum/kernels.py +234 -0
- quantum/parse_qasm.py +393 -0
- quantum/qasm.py +89 -0
- quantum/qubit_abs.py +682 -0
- stl/__init__.py +1 -0
- stl/physics_algs/__init__.py +57 -0
- stl/physics_algs/duals_and_coords.py +482 -0
- stl/physics_algs/units.py +923 -0
- stl/physics_algs/vector_fields.py +270 -0
- stl/quantum_algs/__init__.py +70 -0
- stl/quantum_algs/fast_factor.py +195 -0
- stl/quantum_algs/grovers_alg.py +125 -0
- stl/quantum_algs/quantum_optim.py +154 -0
- stl/quantum_algs/quantum_optim_hl.py +362 -0
- stl/quantum_algs/shors_alg.py +371 -0
lang_systems/__init__.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Quantar language systems - parsers, ASTs, and language tools.
|
|
2
|
+
|
|
3
|
+
``lang_systems.parse`` returns pegen's raw parse tree. ``lang_systems.parse_ast``
|
|
4
|
+
returns a Python AST with custom :class:`QuantumOp` nodes. Both are imported
|
|
5
|
+
lazily so that importing this package does not require the generated parser
|
|
6
|
+
(which is produced at build time) to already exist.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import importlib
|
|
10
|
+
|
|
11
|
+
__version__ = "0.1.0"
|
|
12
|
+
|
|
13
|
+
_EXPORTS = {
|
|
14
|
+
"parse": ("lang_systems.quantar", "parse"),
|
|
15
|
+
"parse_ast": ("lang_systems.ast_builder", "parse_ast"),
|
|
16
|
+
"build_ast": ("lang_systems.ast_builder", "build_ast"),
|
|
17
|
+
"lower_ast": ("lang_systems.ast_builder", "lower_ast"),
|
|
18
|
+
"compile_ast": ("lang_systems.ast_builder", "compile_ast"),
|
|
19
|
+
"QuantumOp": ("lang_systems.ast_builder", "QuantumOp"),
|
|
20
|
+
"LowerQuantumOps": ("lang_systems.ast_builder", "LowerQuantumOps"),
|
|
21
|
+
"load_operators": ("lang_systems.operators", "load_operators"),
|
|
22
|
+
"list_known_operators": ("lang_systems.operators", "list_known_operators"),
|
|
23
|
+
"QuantarNodeVisitor": ("lang_systems.visitors", "QuantarNodeVisitor"),
|
|
24
|
+
"OperatorCollector": ("lang_systems.visitors", "OperatorCollector"),
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def __getattr__(name):
|
|
29
|
+
target = _EXPORTS.get(name)
|
|
30
|
+
if target is None:
|
|
31
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
32
|
+
module_name, attribute = target
|
|
33
|
+
value = getattr(importlib.import_module(module_name), attribute)
|
|
34
|
+
globals()[name] = value
|
|
35
|
+
return value
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
__all__ = list(_EXPORTS)
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
"""Build a Python AST for Quantar source.
|
|
2
|
+
|
|
3
|
+
Quantar adds a handful of infix operators to Python's syntax. Reimplementing
|
|
4
|
+
CPython's grammar would be wasteful, so this module takes the opposite route:
|
|
5
|
+
|
|
6
|
+
1. recursively find custom infix expressions in the pegen parse tree;
|
|
7
|
+
2. replace each one with a unique sentinel call;
|
|
8
|
+
3. hand the resulting text to :func:`ast.parse`;
|
|
9
|
+
4. replace the sentinel calls with :class:`QuantumOp` nodes.
|
|
10
|
+
|
|
11
|
+
The result is ordinary Python AST plus custom :class:`QuantumOp` nodes, which
|
|
12
|
+
can then be inspected with :mod:`lang_systems.visitors` or lowered back to calls.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import ast
|
|
18
|
+
import token
|
|
19
|
+
import tokenize
|
|
20
|
+
from dataclasses import dataclass
|
|
21
|
+
from types import CodeType
|
|
22
|
+
|
|
23
|
+
from lang_systems.operators import get_known_operator_kind
|
|
24
|
+
from lang_systems.quantar import parse
|
|
25
|
+
from lang_systems.quantar_tokens import tokenize_source
|
|
26
|
+
|
|
27
|
+
_SENTINEL_PREFIX = "__quantar_operator_"
|
|
28
|
+
_SENTINEL_SUFFIX = "__"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class QuantumOp(ast.expr):
|
|
32
|
+
"""A Quantar infix operation, stored as a first-class AST node."""
|
|
33
|
+
|
|
34
|
+
_fields = ("operator", "operands", "kind")
|
|
35
|
+
|
|
36
|
+
def __init__(
|
|
37
|
+
self,
|
|
38
|
+
operator: str,
|
|
39
|
+
operands: list[ast.expr],
|
|
40
|
+
kind: str = "binary",
|
|
41
|
+
**kwargs,
|
|
42
|
+
):
|
|
43
|
+
super().__init__(**kwargs)
|
|
44
|
+
self.operator = operator
|
|
45
|
+
self.operands = operands
|
|
46
|
+
self.kind = kind
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass
|
|
50
|
+
class _Rewrite:
|
|
51
|
+
name: str
|
|
52
|
+
operator: str
|
|
53
|
+
kind: str
|
|
54
|
+
start: tuple[int, int]
|
|
55
|
+
end: tuple[int, int]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class _Source:
|
|
59
|
+
"""Map token positions (line, column) to source offsets and slices."""
|
|
60
|
+
|
|
61
|
+
def __init__(self, text: str):
|
|
62
|
+
self.text = text
|
|
63
|
+
self.lines = text.splitlines(keepends=True)
|
|
64
|
+
self.starts: list[int] = []
|
|
65
|
+
offset = 0
|
|
66
|
+
for line in self.lines:
|
|
67
|
+
self.starts.append(offset)
|
|
68
|
+
offset += len(line)
|
|
69
|
+
|
|
70
|
+
def offset(self, position: tuple[int, int]) -> int:
|
|
71
|
+
row, column = position
|
|
72
|
+
if row < 1 or row > len(self.starts):
|
|
73
|
+
return len(self.text)
|
|
74
|
+
return self.starts[row - 1] + column
|
|
75
|
+
|
|
76
|
+
def slice(self, start: tuple[int, int], end: tuple[int, int]) -> str:
|
|
77
|
+
return self.text[self.offset(start) : self.offset(end)]
|
|
78
|
+
|
|
79
|
+
def line(self, row: int) -> str:
|
|
80
|
+
if 1 <= row <= len(self.lines):
|
|
81
|
+
return self.lines[row - 1]
|
|
82
|
+
return ""
|
|
83
|
+
|
|
84
|
+
def byte_column(self, position: tuple[int, int]) -> int:
|
|
85
|
+
_, column = position
|
|
86
|
+
return len(self.line(position[0])[:column].encode("utf-8"))
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _first_token(node) -> tokenize.TokenInfo | None:
|
|
90
|
+
if isinstance(node, tokenize.TokenInfo):
|
|
91
|
+
return node
|
|
92
|
+
if isinstance(node, list):
|
|
93
|
+
for item in node:
|
|
94
|
+
found = _first_token(item)
|
|
95
|
+
if found is not None:
|
|
96
|
+
return found
|
|
97
|
+
return None
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _last_token(node) -> tokenize.TokenInfo | None:
|
|
101
|
+
if isinstance(node, tokenize.TokenInfo):
|
|
102
|
+
return node
|
|
103
|
+
if isinstance(node, list):
|
|
104
|
+
for item in reversed(node):
|
|
105
|
+
found = _last_token(item)
|
|
106
|
+
if found is not None:
|
|
107
|
+
return found
|
|
108
|
+
return None
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _custom_operator(node) -> tokenize.TokenInfo | None:
|
|
112
|
+
"""Return the operator token if `node` is a custom binary expression."""
|
|
113
|
+
if not isinstance(node, list) or len(node) != 3:
|
|
114
|
+
return None
|
|
115
|
+
left, middle, right = node
|
|
116
|
+
if not isinstance(middle, tokenize.TokenInfo):
|
|
117
|
+
return None
|
|
118
|
+
if middle.type not in (token.OP, token.NAME):
|
|
119
|
+
return None
|
|
120
|
+
if get_known_operator_kind(middle.string) is None:
|
|
121
|
+
return None
|
|
122
|
+
if left is None or right is None:
|
|
123
|
+
return None
|
|
124
|
+
return middle
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _direct_custom_nodes(node) -> list:
|
|
128
|
+
"""Return the outermost custom expressions inside a standard node."""
|
|
129
|
+
found = []
|
|
130
|
+
|
|
131
|
+
def visit(value):
|
|
132
|
+
if _custom_operator(value) is not None:
|
|
133
|
+
found.append(value)
|
|
134
|
+
return
|
|
135
|
+
if isinstance(value, list):
|
|
136
|
+
for item in value:
|
|
137
|
+
visit(item)
|
|
138
|
+
|
|
139
|
+
visit(node)
|
|
140
|
+
return found
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _node_text(node, source: _Source) -> str:
|
|
144
|
+
if isinstance(node, tokenize.TokenInfo):
|
|
145
|
+
return node.string
|
|
146
|
+
if not isinstance(node, list):
|
|
147
|
+
return ""
|
|
148
|
+
first = _first_token(node)
|
|
149
|
+
last = _last_token(node)
|
|
150
|
+
if first is None or last is None:
|
|
151
|
+
return ""
|
|
152
|
+
return source.slice(first.start, last.end)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _render(node, source: _Source, rewrites: list[_Rewrite]) -> str:
|
|
156
|
+
operator = _custom_operator(node)
|
|
157
|
+
if operator is not None:
|
|
158
|
+
left, _, right = node
|
|
159
|
+
first = _first_token(node)
|
|
160
|
+
last = _last_token(node)
|
|
161
|
+
if first is None or last is None:
|
|
162
|
+
return _node_text(node, source)
|
|
163
|
+
|
|
164
|
+
name = f"{_SENTINEL_PREFIX}{len(rewrites)}{_SENTINEL_SUFFIX}"
|
|
165
|
+
kind = get_known_operator_kind(operator.string) or "binary"
|
|
166
|
+
rewrites.append(_Rewrite(name, operator.string, kind, first.start, last.end))
|
|
167
|
+
|
|
168
|
+
left_text = _render(left, source, rewrites)
|
|
169
|
+
right_text = _render(right, source, rewrites)
|
|
170
|
+
return f"{name}({operator.string!r}, ({left_text}), ({right_text}))"
|
|
171
|
+
|
|
172
|
+
if isinstance(node, tokenize.TokenInfo):
|
|
173
|
+
return node.string
|
|
174
|
+
|
|
175
|
+
text = _node_text(node, source)
|
|
176
|
+
if not text:
|
|
177
|
+
return text
|
|
178
|
+
|
|
179
|
+
first = _first_token(node)
|
|
180
|
+
if first is None:
|
|
181
|
+
return text
|
|
182
|
+
base = source.offset(first.start)
|
|
183
|
+
replacements = []
|
|
184
|
+
for child in _direct_custom_nodes(node):
|
|
185
|
+
child_first = _first_token(child)
|
|
186
|
+
child_last = _last_token(child)
|
|
187
|
+
if child_first is None or child_last is None:
|
|
188
|
+
continue
|
|
189
|
+
start = source.offset(child_first.start) - base
|
|
190
|
+
end = source.offset(child_last.end) - base
|
|
191
|
+
replacements.append((start, end, _render(child, source, rewrites)))
|
|
192
|
+
|
|
193
|
+
for start, end, replacement in sorted(replacements, reverse=True):
|
|
194
|
+
text = text[:start] + replacement + text[end:]
|
|
195
|
+
return text
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
class _ReplaceSentinels(ast.NodeTransformer):
|
|
199
|
+
def __init__(self, rewrites: list[_Rewrite], source: _Source):
|
|
200
|
+
self.rewrites = {rewrite.name: rewrite for rewrite in rewrites}
|
|
201
|
+
self.source = source
|
|
202
|
+
|
|
203
|
+
def visit_Call(self, node: ast.Call):
|
|
204
|
+
if not (isinstance(node.func, ast.Name) and node.func.id in self.rewrites):
|
|
205
|
+
return self.generic_visit(node)
|
|
206
|
+
|
|
207
|
+
node = self.generic_visit(node) #type: ignore
|
|
208
|
+
rewrite = self.rewrites[node.func.id] #type: ignore
|
|
209
|
+
if len(node.args) != 3:
|
|
210
|
+
return node
|
|
211
|
+
|
|
212
|
+
result = QuantumOp(rewrite.operator, [node.args[1], node.args[2]], rewrite.kind)
|
|
213
|
+
result.lineno = rewrite.start[0]
|
|
214
|
+
result.col_offset = self.source.byte_column(rewrite.start)
|
|
215
|
+
result.end_lineno = rewrite.end[0]
|
|
216
|
+
result.end_col_offset = self.source.byte_column(rewrite.end)
|
|
217
|
+
return result
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
class LowerQuantumOps(ast.NodeTransformer):
|
|
221
|
+
"""Lower :class:`QuantumOp` nodes to ``__quantar_apply__(name, *operands)``."""
|
|
222
|
+
|
|
223
|
+
def visit_QuantumOp(self, node: QuantumOp):
|
|
224
|
+
node = self.generic_visit(node) #type: ignore
|
|
225
|
+
call = ast.Call(
|
|
226
|
+
func=ast.Name(id="__quantar_apply__", ctx=ast.Load()),
|
|
227
|
+
args=[ast.Constant(node.operator), *node.operands],
|
|
228
|
+
keywords=[],
|
|
229
|
+
)
|
|
230
|
+
return ast.copy_location(call, node)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def parse_ast(source: str, filename: str = "<quantar>") -> ast.Module:
|
|
234
|
+
"""Parse Quantar source into a Python AST containing :class:`QuantumOp` nodes."""
|
|
235
|
+
source_map = _Source(source)
|
|
236
|
+
rewrites: list[_Rewrite] = []
|
|
237
|
+
rewritten = _render(parse(source), source_map, rewrites)
|
|
238
|
+
tree = ast.parse(rewritten, filename=filename)
|
|
239
|
+
tree = _ReplaceSentinels(rewrites, source_map).visit(tree)
|
|
240
|
+
ast.fix_missing_locations(tree)
|
|
241
|
+
return tree
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def lower_ast(tree: ast.AST) -> ast.AST:
|
|
245
|
+
"""Return `tree` with all :class:`QuantumOp` nodes lowered to calls."""
|
|
246
|
+
lowered = LowerQuantumOps().visit(tree)
|
|
247
|
+
ast.fix_missing_locations(lowered)
|
|
248
|
+
return lowered
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def compile_ast(tree: ast.AST, filename: str = "<quantar>", mode: str = "exec") -> CodeType:
|
|
252
|
+
"""Compile an AST produced by :func:`parse_ast`."""
|
|
253
|
+
return compile(lower_ast(tree), filename, mode) #type: ignore
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def tokenize_ast(source: str):
|
|
257
|
+
"""Return the token stream used while building an AST (useful for tooling)."""
|
|
258
|
+
return tokenize_source(source)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
build_ast = parse_ast
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
__all__ = [
|
|
265
|
+
"QuantumOp",
|
|
266
|
+
"LowerQuantumOps",
|
|
267
|
+
"parse_ast",
|
|
268
|
+
"build_ast",
|
|
269
|
+
"lower_ast",
|
|
270
|
+
"compile_ast",
|
|
271
|
+
"tokenize_ast",
|
|
272
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Language server support for Quantar."""
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
"""Quantar language server.
|
|
2
|
+
|
|
3
|
+
The server deliberately reuses the real tokenizer and parser so editor
|
|
4
|
+
highlighting and diagnostics cannot drift from the language implementation.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import keyword
|
|
10
|
+
import logging
|
|
11
|
+
import os
|
|
12
|
+
import sys
|
|
13
|
+
import token
|
|
14
|
+
import tokenize
|
|
15
|
+
|
|
16
|
+
from lsprotocol import types
|
|
17
|
+
from pygls.lsp.server import LanguageServer
|
|
18
|
+
|
|
19
|
+
from lang_systems.ast_builder import parse_ast
|
|
20
|
+
from lang_systems.operators import list_known_operators, load_operators
|
|
21
|
+
from lang_systems.quantar_tokens import tokenize_source
|
|
22
|
+
|
|
23
|
+
TOKEN_TYPES = ("quantarOperator",)
|
|
24
|
+
TOKEN_MODIFIERS = (
|
|
25
|
+
"nullary",
|
|
26
|
+
"unary",
|
|
27
|
+
"binary",
|
|
28
|
+
"ternary",
|
|
29
|
+
"callable",
|
|
30
|
+
"matrix",
|
|
31
|
+
)
|
|
32
|
+
MODIFIER_BITS = {
|
|
33
|
+
name: 1 << index for index, name in enumerate(TOKEN_MODIFIERS)
|
|
34
|
+
}
|
|
35
|
+
LEGEND = types.SemanticTokensLegend(
|
|
36
|
+
token_types=list(TOKEN_TYPES),
|
|
37
|
+
token_modifiers=list(TOKEN_MODIFIERS),
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
logger = logging.getLogger(__name__)
|
|
41
|
+
|
|
42
|
+
server = LanguageServer("quantar-lsp", "0.1.0")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _load_default_operators() -> None:
|
|
46
|
+
"""Register the quantum gate operators shipped with Quantar."""
|
|
47
|
+
try:
|
|
48
|
+
import quantum.gate_matrix # noqa: F401
|
|
49
|
+
except ImportError:
|
|
50
|
+
logger.debug("Quantum gate operators are not available")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _load_configured_operators() -> None:
|
|
54
|
+
"""Import operator modules listed in QUANTAR_OPERATOR_MODULES."""
|
|
55
|
+
raw_modules = os.environ.get("QUANTAR_OPERATOR_MODULES", "")
|
|
56
|
+
modules = [module.strip() for module in raw_modules.split(",") if module.strip()]
|
|
57
|
+
if not modules:
|
|
58
|
+
return
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
load_operators(*modules)
|
|
62
|
+
except Exception:
|
|
63
|
+
logger.exception("Failed to load Quantar operator modules: %s", modules)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _position(document, line: int, character: int) -> types.Position:
|
|
67
|
+
"""Convert a Python code-point position into a client position."""
|
|
68
|
+
return document.position_to_client_units(
|
|
69
|
+
types.Position(line=max(line, 0), character=max(character, 0))
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _full_range(document) -> types.Range:
|
|
74
|
+
lines = document.lines
|
|
75
|
+
if not lines:
|
|
76
|
+
return types.Range(
|
|
77
|
+
start=types.Position(line=0, character=0),
|
|
78
|
+
end=types.Position(line=0, character=0),
|
|
79
|
+
)
|
|
80
|
+
last = len(lines) - 1
|
|
81
|
+
return types.Range(
|
|
82
|
+
start=types.Position(line=0, character=0),
|
|
83
|
+
end=_position(document, last, len(lines[last].rstrip("\r\n"))),
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _syntax_error_range(document, error: SyntaxError) -> types.Range:
|
|
88
|
+
lines = document.lines
|
|
89
|
+
if not lines:
|
|
90
|
+
return _full_range(document)
|
|
91
|
+
|
|
92
|
+
line = min(max((error.lineno or 1) - 1, 0), len(lines) - 1)
|
|
93
|
+
text = lines[line]
|
|
94
|
+
start = min(max((error.offset or 1) - 1, 0), len(text))
|
|
95
|
+
|
|
96
|
+
end_line = min(
|
|
97
|
+
max((getattr(error, "end_lineno", None) or line + 1) - 1, line),
|
|
98
|
+
len(lines) - 1,
|
|
99
|
+
)
|
|
100
|
+
if end_line == line:
|
|
101
|
+
end = max(
|
|
102
|
+
start + 1,
|
|
103
|
+
min(
|
|
104
|
+
max((getattr(error, "end_offset", None) or start + 2) - 1, 0),
|
|
105
|
+
len(text),
|
|
106
|
+
),
|
|
107
|
+
)
|
|
108
|
+
else:
|
|
109
|
+
end = len(lines[end_line].rstrip("\r\n"))
|
|
110
|
+
|
|
111
|
+
return types.Range(
|
|
112
|
+
start=_position(document, line, start),
|
|
113
|
+
end=_position(document, end_line, end),
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _token_error_range(document, error: tokenize.TokenError) -> types.Range:
|
|
118
|
+
location = error.args[1] if len(error.args) > 1 else (1, 0)
|
|
119
|
+
line, character = location
|
|
120
|
+
return types.Range(
|
|
121
|
+
start=_position(document, line - 1, character),
|
|
122
|
+
end=_position(document, line - 1, character + 1),
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _publish_diagnostics(ls: LanguageServer, document) -> None:
|
|
127
|
+
diagnostics: list[types.Diagnostic] = []
|
|
128
|
+
try:
|
|
129
|
+
parse_ast(document.source, document.path or document.uri)
|
|
130
|
+
except SyntaxError as error:
|
|
131
|
+
diagnostics.append(
|
|
132
|
+
types.Diagnostic(
|
|
133
|
+
range=_syntax_error_range(document, error),
|
|
134
|
+
message=error.msg or "Invalid Quantar syntax",
|
|
135
|
+
severity=types.DiagnosticSeverity.Error,
|
|
136
|
+
source="quantar",
|
|
137
|
+
)
|
|
138
|
+
)
|
|
139
|
+
except tokenize.TokenError as error:
|
|
140
|
+
diagnostics.append(
|
|
141
|
+
types.Diagnostic(
|
|
142
|
+
range=_token_error_range(document, error),
|
|
143
|
+
message=error.args[0] if error.args else "Tokenizer error",
|
|
144
|
+
severity=types.DiagnosticSeverity.Error,
|
|
145
|
+
source="quantar",
|
|
146
|
+
)
|
|
147
|
+
)
|
|
148
|
+
except Exception as error:
|
|
149
|
+
diagnostics.append(
|
|
150
|
+
types.Diagnostic(
|
|
151
|
+
range=_full_range(document),
|
|
152
|
+
message=str(error),
|
|
153
|
+
severity=types.DiagnosticSeverity.Error,
|
|
154
|
+
source="quantar",
|
|
155
|
+
)
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
ls.text_document_publish_diagnostics(
|
|
159
|
+
types.PublishDiagnosticsParams(
|
|
160
|
+
uri=document.uri,
|
|
161
|
+
diagnostics=diagnostics,
|
|
162
|
+
version=document.version,
|
|
163
|
+
)
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
@server.feature(types.TEXT_DOCUMENT_DID_OPEN)
|
|
168
|
+
def did_open(ls: LanguageServer, params: types.DidOpenTextDocumentParams) -> None:
|
|
169
|
+
document = ls.workspace.get_text_document(params.text_document.uri)
|
|
170
|
+
_publish_diagnostics(ls, document)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
@server.feature(types.TEXT_DOCUMENT_DID_CHANGE)
|
|
174
|
+
def did_change(ls: LanguageServer, params: types.DidChangeTextDocumentParams) -> None:
|
|
175
|
+
document = ls.workspace.get_text_document(params.text_document.uri)
|
|
176
|
+
_publish_diagnostics(ls, document)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
@server.feature(types.TEXT_DOCUMENT_DID_SAVE)
|
|
180
|
+
def did_save(ls: LanguageServer, params: types.DidSaveTextDocumentParams) -> None:
|
|
181
|
+
document = ls.workspace.get_text_document(params.text_document.uri)
|
|
182
|
+
_publish_diagnostics(ls, document)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
@server.feature(
|
|
186
|
+
types.TEXT_DOCUMENT_SEMANTIC_TOKENS_FULL,
|
|
187
|
+
LEGEND,
|
|
188
|
+
)
|
|
189
|
+
def semantic_tokens_full(
|
|
190
|
+
ls: LanguageServer, params: types.SemanticTokensParams
|
|
191
|
+
) -> types.SemanticTokens:
|
|
192
|
+
document = ls.workspace.get_text_document(params.text_document.uri)
|
|
193
|
+
operators = list_known_operators()
|
|
194
|
+
data: list[int] = []
|
|
195
|
+
|
|
196
|
+
try:
|
|
197
|
+
tokens = tokenize_source(document.source)
|
|
198
|
+
previous_line = 0
|
|
199
|
+
previous_start = 0
|
|
200
|
+
|
|
201
|
+
for current in tokens:
|
|
202
|
+
if current.type not in (token.OP, token.NAME):
|
|
203
|
+
continue
|
|
204
|
+
if current.string not in operators:
|
|
205
|
+
continue
|
|
206
|
+
if current.type == token.NAME and keyword.iskeyword(current.string):
|
|
207
|
+
continue
|
|
208
|
+
|
|
209
|
+
start = _position(document, current.start[0] - 1, current.start[1])
|
|
210
|
+
end = _position(document, current.end[0] - 1, current.end[1])
|
|
211
|
+
if start.line != end.line or end.character <= start.character:
|
|
212
|
+
continue
|
|
213
|
+
|
|
214
|
+
delta_line = start.line - previous_line
|
|
215
|
+
delta_start = (
|
|
216
|
+
start.character - previous_start
|
|
217
|
+
if delta_line == 0
|
|
218
|
+
else start.character
|
|
219
|
+
)
|
|
220
|
+
modifier = MODIFIER_BITS.get(operators[current.string], 0)
|
|
221
|
+
|
|
222
|
+
data.extend(
|
|
223
|
+
[
|
|
224
|
+
delta_line,
|
|
225
|
+
delta_start,
|
|
226
|
+
end.character - start.character,
|
|
227
|
+
0,
|
|
228
|
+
modifier,
|
|
229
|
+
]
|
|
230
|
+
)
|
|
231
|
+
previous_line = start.line
|
|
232
|
+
previous_start = start.character
|
|
233
|
+
except (SyntaxError, tokenize.TokenError, IndentationError):
|
|
234
|
+
return types.SemanticTokens(data=[])
|
|
235
|
+
|
|
236
|
+
return types.SemanticTokens(data=data)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def main() -> None:
|
|
240
|
+
logging.basicConfig(level=logging.INFO, stream=sys.stderr)
|
|
241
|
+
_load_default_operators()
|
|
242
|
+
_load_configured_operators()
|
|
243
|
+
server.start_io()
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
if __name__ == "__main__":
|
|
247
|
+
main()
|