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
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
"""Generic operator registry and ``@operator`` decorator.
|
|
2
|
+
|
|
3
|
+
Completely independent of quantum code -- it can be used for anything.
|
|
4
|
+
|
|
5
|
+
Operator kinds are named by **how the operator is written**, which is usually
|
|
6
|
+
what you care about:
|
|
7
|
+
|
|
8
|
+
kind operands example
|
|
9
|
+
---------- -------- -------------------------
|
|
10
|
+
nullary 0 ``pi``
|
|
11
|
+
unary_pre 1 ``-x``, ``~x``
|
|
12
|
+
unary_post 1 ``x!``, ``x†``
|
|
13
|
+
binary 2 ``a <-> b``
|
|
14
|
+
ternary 3 ``a ? b : c``
|
|
15
|
+
callable any ``f(a, b, c)``
|
|
16
|
+
matrix 0 a gate factory returning an ``np.ndarray``
|
|
17
|
+
|
|
18
|
+
``kind`` is optional and defaults to ``"callable"``. Common synonyms are
|
|
19
|
+
accepted (``"unary"``/``"prefix"`` -> ``unary_pre``, ``"infix"`` -> ``binary``,
|
|
20
|
+
...), so existing code keeps working. As a shortcut you can write
|
|
21
|
+
``@operator.binary("CNOT")`` instead of ``@operator(kind="binary", name="CNOT")``.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from functools import wraps
|
|
25
|
+
from importlib import import_module
|
|
26
|
+
from types import ModuleType
|
|
27
|
+
from typing import Any, Callable, Literal
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
OperatorKind = Literal[
|
|
31
|
+
"nullary",
|
|
32
|
+
"unary_pre",
|
|
33
|
+
"unary_post",
|
|
34
|
+
"binary",
|
|
35
|
+
"ternary",
|
|
36
|
+
"callable",
|
|
37
|
+
"matrix",
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
#: Every accepted kind, in a convenient order for docs/validation.
|
|
41
|
+
OPERATOR_KINDS: tuple[str, ...] = (
|
|
42
|
+
"nullary",
|
|
43
|
+
"unary_pre",
|
|
44
|
+
"unary_post",
|
|
45
|
+
"binary",
|
|
46
|
+
"ternary",
|
|
47
|
+
"callable",
|
|
48
|
+
"matrix",
|
|
49
|
+
"gate"
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
#: Friendly synonyms, so the old names and obvious variations still work.
|
|
53
|
+
_KIND_ALIASES: dict[str, str] = {
|
|
54
|
+
"constant": "nullary",
|
|
55
|
+
"value": "nullary",
|
|
56
|
+
"zero": "nullary",
|
|
57
|
+
"unary": "unary_pre",
|
|
58
|
+
"prefix": "unary_pre",
|
|
59
|
+
"pre": "unary_pre",
|
|
60
|
+
"postfix": "unary_post",
|
|
61
|
+
"post": "unary_post",
|
|
62
|
+
"infix": "binary",
|
|
63
|
+
"nary": "callable",
|
|
64
|
+
"any": "callable",
|
|
65
|
+
"func": "callable",
|
|
66
|
+
"gate": "matrix",
|
|
67
|
+
"gate_matrix": "matrix",
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
#: Operators that the language knows syntactically even before their runtime
|
|
72
|
+
#: implementation is imported. Keeping these names here means ``parse()`` does
|
|
73
|
+
#: not have to import the quantum simulator just to recognise ``a <-> b``.
|
|
74
|
+
BUILTIN_OPERATOR_KINDS: dict[str, str] = {
|
|
75
|
+
"<->": "binary",
|
|
76
|
+
"<-i->": "binary",
|
|
77
|
+
"<-s->": "binary",
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def normalize_kind(kind: str) -> str:
|
|
82
|
+
"""Return the canonical kind name for `kind` (or raise ``ValueError``)."""
|
|
83
|
+
if not isinstance(kind, str):
|
|
84
|
+
raise ValueError(f"Operator kind must be a string, got {type(kind).__name__}.")
|
|
85
|
+
key = _KIND_ALIASES.get(kind.strip().lower(), kind.strip().lower())
|
|
86
|
+
if key not in OPERATOR_KINDS:
|
|
87
|
+
raise ValueError(
|
|
88
|
+
f"Unknown operator kind {kind!r}. Choose one of: {', '.join(OPERATOR_KINDS)}."
|
|
89
|
+
)
|
|
90
|
+
return key
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# Global operator registry
|
|
94
|
+
_global_operators: dict[str, Callable] = {}
|
|
95
|
+
_operator_metadata: dict[str, dict[str, Any]] = {}
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _mark(obj, name: str, kind: str, requires_instance: bool) -> None:
|
|
99
|
+
obj._operator_name = name
|
|
100
|
+
obj._is_operator = True
|
|
101
|
+
obj._requires_instance = requires_instance
|
|
102
|
+
obj._kind = kind
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _register(name: str, kind: str, func: Callable) -> Callable:
|
|
106
|
+
if not isinstance(name, str) or not name:
|
|
107
|
+
raise ValueError("Operator name must be a non-empty string")
|
|
108
|
+
kind = normalize_kind(kind)
|
|
109
|
+
|
|
110
|
+
# Handle staticmethod/classmethod
|
|
111
|
+
if isinstance(func, (staticmethod, classmethod)):
|
|
112
|
+
func = func.__func__
|
|
113
|
+
|
|
114
|
+
# For matrix kind, call once now to validate and cache the matrix.
|
|
115
|
+
if kind == "matrix":
|
|
116
|
+
import numpy as np
|
|
117
|
+
|
|
118
|
+
try:
|
|
119
|
+
matrix = func()
|
|
120
|
+
except TypeError as error:
|
|
121
|
+
if "missing 1 required positional argument: 'self'" in str(error):
|
|
122
|
+
# Instance method: defer the call until an instance is supplied.
|
|
123
|
+
def instance_wrapper(instance):
|
|
124
|
+
return func(instance)
|
|
125
|
+
|
|
126
|
+
_mark(instance_wrapper, name, kind, True)
|
|
127
|
+
_global_operators[name] = instance_wrapper
|
|
128
|
+
_operator_metadata[name] = {"kind": kind, "requires_instance": True}
|
|
129
|
+
_mark(func, name, kind, True)
|
|
130
|
+
return func
|
|
131
|
+
raise
|
|
132
|
+
|
|
133
|
+
if not isinstance(matrix, np.ndarray):
|
|
134
|
+
raise ValueError(f"Operator function '{name}' must return a numpy array")
|
|
135
|
+
|
|
136
|
+
_mark(func, name, kind, False)
|
|
137
|
+
func._operator_matrix = matrix # type: ignore[attr-defined]
|
|
138
|
+
_global_operators[name] = func
|
|
139
|
+
_operator_metadata[name] = {"kind": kind, "requires_instance": False}
|
|
140
|
+
return func
|
|
141
|
+
|
|
142
|
+
# unary_pre / unary_post / binary / ternary / nullary / callable
|
|
143
|
+
code = getattr(func, "__code__", None)
|
|
144
|
+
requires_instance = bool(code and "self" in code.co_varnames)
|
|
145
|
+
_mark(func, name, kind, requires_instance)
|
|
146
|
+
_global_operators[name] = func
|
|
147
|
+
_operator_metadata[name] = {"kind": kind, "requires_instance": requires_instance}
|
|
148
|
+
|
|
149
|
+
@wraps(func)
|
|
150
|
+
def wrapper(*args, **kwargs):
|
|
151
|
+
return func(*args, **kwargs)
|
|
152
|
+
|
|
153
|
+
_mark(wrapper, name, kind, requires_instance)
|
|
154
|
+
return wrapper
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class operator:
|
|
158
|
+
"""Register a callable under an arbitrary name.
|
|
159
|
+
|
|
160
|
+
The name can be any string: capital letters, symbols, unicode, etc. Works as
|
|
161
|
+
a function, method, or class decorator (or with any Python callable).
|
|
162
|
+
|
|
163
|
+
Examples::
|
|
164
|
+
|
|
165
|
+
@operator(kind="matrix", name="h")
|
|
166
|
+
def h_MAT(): ...
|
|
167
|
+
|
|
168
|
+
@operator.binary("<->")
|
|
169
|
+
def swap(control, target): ...
|
|
170
|
+
|
|
171
|
+
@operator.unary_post("†")
|
|
172
|
+
def adjoint(qubit): ...
|
|
173
|
+
|
|
174
|
+
``kind`` may be omitted (default ``"callable"``).
|
|
175
|
+
"""
|
|
176
|
+
|
|
177
|
+
def __init__(self, name: str, kind: OperatorKind = "callable"):
|
|
178
|
+
self.name = name
|
|
179
|
+
self.kind = kind
|
|
180
|
+
|
|
181
|
+
def __call__(self, func: Callable) -> Callable:
|
|
182
|
+
return _register(self.name, self.kind, func) #type: ignore
|
|
183
|
+
|
|
184
|
+
# ─── Convenience constructors ───
|
|
185
|
+
|
|
186
|
+
@classmethod
|
|
187
|
+
def nullary(cls, name: str) -> "operator":
|
|
188
|
+
return cls(name, "nullary")
|
|
189
|
+
|
|
190
|
+
@classmethod
|
|
191
|
+
def unary_pre(cls, name: str) -> "operator":
|
|
192
|
+
return cls(name, "unary_pre")
|
|
193
|
+
|
|
194
|
+
@classmethod
|
|
195
|
+
def unary_post(cls, name: str) -> "operator":
|
|
196
|
+
return cls(name, "unary_post")
|
|
197
|
+
|
|
198
|
+
@classmethod
|
|
199
|
+
def binary(cls, name: str) -> "operator":
|
|
200
|
+
return cls(name, "binary")
|
|
201
|
+
|
|
202
|
+
@classmethod
|
|
203
|
+
def ternary(cls, name: str) -> "operator":
|
|
204
|
+
return cls(name, "ternary")
|
|
205
|
+
|
|
206
|
+
@classmethod
|
|
207
|
+
def callable(cls, name: str) -> "operator":
|
|
208
|
+
return cls(name, "callable")
|
|
209
|
+
|
|
210
|
+
@classmethod
|
|
211
|
+
def matrix(cls, name: str) -> "operator":
|
|
212
|
+
return cls(name, "matrix")
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
# ─── Public API ───
|
|
216
|
+
|
|
217
|
+
def get_global_operator(name: str):
|
|
218
|
+
"""Retrieve a registered operator by name.
|
|
219
|
+
|
|
220
|
+
Returns ``(operator, kind)``; ``kind`` is one of :data:`OPERATOR_KINDS`, or
|
|
221
|
+
``None`` when the name is not registered.
|
|
222
|
+
"""
|
|
223
|
+
op = _global_operators.get(name)
|
|
224
|
+
if op is None:
|
|
225
|
+
return None, None
|
|
226
|
+
return op, getattr(op, "_kind", "callable")
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def get_operator(name: str):
|
|
230
|
+
"""Alias for get_global_operator (backwards compatibility)."""
|
|
231
|
+
return get_global_operator(name)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def get_operator_kind(name: str) -> str | None:
|
|
235
|
+
"""Get the canonical kind of a registered operator."""
|
|
236
|
+
meta = _operator_metadata.get(name)
|
|
237
|
+
return meta.get("kind") if meta else None
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def get_known_operator_kind(name: str) -> str | None:
|
|
241
|
+
"""Get the kind of a registered or built-in operator."""
|
|
242
|
+
kind = get_operator_kind(name)
|
|
243
|
+
if kind is not None:
|
|
244
|
+
return kind
|
|
245
|
+
return BUILTIN_OPERATOR_KINDS.get(name)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def is_known_operator(name: str) -> bool:
|
|
249
|
+
"""Return whether `name` is a registered or built-in operator."""
|
|
250
|
+
return name in _global_operators or name in BUILTIN_OPERATOR_KINDS
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def get_operator_requires_instance(name: str) -> bool:
|
|
254
|
+
"""Check if an operator requires an instance (is a method)."""
|
|
255
|
+
meta = _operator_metadata.get(name)
|
|
256
|
+
return meta.get("requires_instance", False) if meta else False
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def get_global_operator_matrix(name: str):
|
|
260
|
+
"""Retrieve a registered matrix operator by name."""
|
|
261
|
+
op, kind = get_global_operator(name)
|
|
262
|
+
if kind == "matrix" and callable(op):
|
|
263
|
+
try:
|
|
264
|
+
return op()
|
|
265
|
+
except TypeError:
|
|
266
|
+
# Instance method: can't call without an instance.
|
|
267
|
+
return op
|
|
268
|
+
return None
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def get_operator_matrix(name: str):
|
|
272
|
+
"""Alias for get_global_operator_matrix (backwards compatibility)."""
|
|
273
|
+
return get_global_operator_matrix(name)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def list_operators(kind: str | None = None) -> list[str]:
|
|
277
|
+
"""List registered operator names, optionally filtered to one `kind`."""
|
|
278
|
+
if kind is None:
|
|
279
|
+
return list(_global_operators.keys())
|
|
280
|
+
wanted = normalize_kind(kind)
|
|
281
|
+
return [name for name in _global_operators if _operator_metadata.get(name, {}).get("kind") == wanted]
|
|
282
|
+
|
|
283
|
+
def list_known_operators() -> dict[str, str]:
|
|
284
|
+
"""Return every registered and built-in operator mapped to its kind."""
|
|
285
|
+
names = set(list_operators()) | set(BUILTIN_OPERATOR_KINDS)
|
|
286
|
+
return {
|
|
287
|
+
name: get_known_operator_kind(name) or "callable"
|
|
288
|
+
for name in names
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def load_operators(*modules: str | ModuleType) -> tuple[ModuleType, ...]:
|
|
293
|
+
"""Import operator modules so their registrations take effect.
|
|
294
|
+
|
|
295
|
+
Strings are imported with :func:`importlib.import_module`; module objects
|
|
296
|
+
are accepted as-is. Call this before :func:`lang_systems.parse` when the
|
|
297
|
+
source uses operators defined by another package::
|
|
298
|
+
|
|
299
|
+
from lang_systems import parse
|
|
300
|
+
from lang_systems.operators import load_operators
|
|
301
|
+
|
|
302
|
+
load_operators("my_package.quantar_ops")
|
|
303
|
+
tree = parse("a >< b\\n")
|
|
304
|
+
|
|
305
|
+
Returns the loaded modules in the order given.
|
|
306
|
+
"""
|
|
307
|
+
loaded = []
|
|
308
|
+
for module in modules:
|
|
309
|
+
if isinstance(module, str):
|
|
310
|
+
loaded.append(import_module(module))
|
|
311
|
+
elif isinstance(module, ModuleType):
|
|
312
|
+
loaded.append(module)
|
|
313
|
+
else:
|
|
314
|
+
raise TypeError(
|
|
315
|
+
"load_operators() expects module names or module objects, "
|
|
316
|
+
f"got {type(module).__name__}"
|
|
317
|
+
)
|
|
318
|
+
return tuple(loaded)
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def clear_operators():
|
|
322
|
+
"""Clear all registered operators (useful for testing)."""
|
|
323
|
+
_global_operators.clear()
|
|
324
|
+
_operator_metadata.clear()
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def apply_operator(name: str, *args, **kwargs) -> Any:
|
|
328
|
+
"""Execute a registered operator by name with the given arguments.
|
|
329
|
+
|
|
330
|
+
Raises:
|
|
331
|
+
ValueError: If the operator is unknown or needs an instance.
|
|
332
|
+
"""
|
|
333
|
+
op, kind = get_operator(name)
|
|
334
|
+
if op is None:
|
|
335
|
+
raise ValueError(f"Unknown operator: '{name}'")
|
|
336
|
+
|
|
337
|
+
meta = _operator_metadata.get(name, {})
|
|
338
|
+
if meta.get("kind") == "matrix" and callable(op):
|
|
339
|
+
try:
|
|
340
|
+
return op()
|
|
341
|
+
except TypeError as error:
|
|
342
|
+
if "missing 1 required positional argument: 'self'" in str(error):
|
|
343
|
+
raise ValueError(f"Operator '{name}' is a method and requires an instance") from error
|
|
344
|
+
raise
|
|
345
|
+
|
|
346
|
+
return op(*args, **kwargs)
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
__all__ = [
|
|
350
|
+
"operator",
|
|
351
|
+
"OperatorKind",
|
|
352
|
+
"OPERATOR_KINDS",
|
|
353
|
+
"normalize_kind",
|
|
354
|
+
"BUILTIN_OPERATOR_KINDS",
|
|
355
|
+
"get_operator",
|
|
356
|
+
"get_global_operator",
|
|
357
|
+
"get_global_operator_matrix",
|
|
358
|
+
"get_operator_kind",
|
|
359
|
+
"get_known_operator_kind",
|
|
360
|
+
"is_known_operator",
|
|
361
|
+
"get_operator_requires_instance",
|
|
362
|
+
"get_operator_matrix",
|
|
363
|
+
"list_operators",
|
|
364
|
+
"clear_operators",
|
|
365
|
+
"apply_operator",
|
|
366
|
+
"list_known_operators",
|
|
367
|
+
]
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""Generate ``src/lang_systems/quantar_parser.py``.
|
|
2
|
+
|
|
3
|
+
Quantar is a *superset* of Python: this builds its parser from the vendored
|
|
4
|
+
CPython grammar (``Grammar/python.gram``) plus the small operator overlay in
|
|
5
|
+
``src/lang_systems/quantar.gram``.
|
|
6
|
+
|
|
7
|
+
pegen 0.3.0 is the last PyPI release and predates several things we need, so a
|
|
8
|
+
few fixes are applied before generating (each one is marked below). Run directly
|
|
9
|
+
with::
|
|
10
|
+
|
|
11
|
+
python -m lang_systems.pegen_gram_gen
|
|
12
|
+
|
|
13
|
+
or let the build do it (see ``hatch_build.py``).
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import sys
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
PACKAGE_ROOT = Path(__file__).resolve().parent
|
|
20
|
+
ROOT = PACKAGE_ROOT.parent.parent
|
|
21
|
+
BASE_GRAMMAR = ROOT / "Grammar" / "python.gram"
|
|
22
|
+
OVERLAY_GRAMMAR = PACKAGE_ROOT / "quantar.gram"
|
|
23
|
+
MERGED_GRAMMAR = ROOT / "Grammar" / "quantar_merged.gram"
|
|
24
|
+
OUTPUT_PARSER = PACKAGE_ROOT / "quantar_parser.py"
|
|
25
|
+
|
|
26
|
+
# Pseudo-tokens the overlay grammar references; implemented at runtime in
|
|
27
|
+
# lang_systems/quantar.py (registered here so pegen's checker accepts them).
|
|
28
|
+
EXTRA_TOKENS = {"QUANTUM_OP", "QUANTUM_NAME"}
|
|
29
|
+
|
|
30
|
+
# CPython grammar rules whose names collide (case-insensitively) with pegen's
|
|
31
|
+
# base token methods (e.g. rule `string` vs the STRING token method). pegen
|
|
32
|
+
# 0.3.0 case-folds and generates `self.string()` -> infinite recursion, so the
|
|
33
|
+
# rules are renamed.
|
|
34
|
+
COLLIDING_RULES = {
|
|
35
|
+
"name",
|
|
36
|
+
"number",
|
|
37
|
+
"string",
|
|
38
|
+
"fstring_start",
|
|
39
|
+
"fstring_middle",
|
|
40
|
+
"fstring_end",
|
|
41
|
+
"op",
|
|
42
|
+
"type_comment",
|
|
43
|
+
"soft_keyword",
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
# Splice that gives custom operators their precedence (between `comparison`
|
|
47
|
+
# and `bitwise_or`); see the header of lang_systems/quantar.gram. The comparison
|
|
48
|
+
# left operand and every comparison RHS become `quantum_expr`.
|
|
49
|
+
COMPARISON_OLD = "comparison[expr_ty]:\n | a=bitwise_or b=compare_op_bitwise_or_pair+"
|
|
50
|
+
COMPARISON_NEW = "comparison[expr_ty]:\n | a=quantum_expr b=compare_op_bitwise_or_pair+"
|
|
51
|
+
COMPARISON_BASE_OLD = "\n | bitwise_or\n\ncompare_op_bitwise_or_pair["
|
|
52
|
+
COMPARISON_BASE_NEW = "\n | quantum_expr\n\ncompare_op_bitwise_or_pair["
|
|
53
|
+
COMPARE_PAIRS_START = "compare_op_bitwise_or_pair[CmpopExprPair*]:"
|
|
54
|
+
COMPARE_PAIRS_END = "# Bitwise operators"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _allow_comparison_rhs_operators(base: str) -> str:
|
|
58
|
+
"""Use `quantum_expr` on both sides of comparisons, not just the left."""
|
|
59
|
+
start = base.find(COMPARE_PAIRS_START)
|
|
60
|
+
end = base.find(COMPARE_PAIRS_END, start)
|
|
61
|
+
if start == -1 or end == -1:
|
|
62
|
+
raise RuntimeError(
|
|
63
|
+
"Could not find the comparison operator rules to splice; the "
|
|
64
|
+
"vendored Grammar/python.gram changed."
|
|
65
|
+
)
|
|
66
|
+
block = base[start:end].replace("a=bitwise_or", "a=quantum_expr")
|
|
67
|
+
return base[:start] + block + base[end:]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _build_grammar_text() -> str:
|
|
71
|
+
base = BASE_GRAMMAR.read_text(encoding="utf-8")
|
|
72
|
+
for old in (COMPARISON_OLD, COMPARISON_BASE_OLD):
|
|
73
|
+
if old not in base:
|
|
74
|
+
raise RuntimeError(
|
|
75
|
+
"Could not find the `comparison` rule to splice; the vendored "
|
|
76
|
+
"Grammar/python.gram changed. Update the COMPARISON_* constants."
|
|
77
|
+
)
|
|
78
|
+
base = base.replace(COMPARISON_OLD, COMPARISON_NEW, 1)
|
|
79
|
+
base = base.replace(COMPARISON_BASE_OLD, COMPARISON_BASE_NEW, 1)
|
|
80
|
+
base = _allow_comparison_rhs_operators(base)
|
|
81
|
+
overlay = OVERLAY_GRAMMAR.read_text(encoding="utf-8")
|
|
82
|
+
merged = base + "\n" + overlay
|
|
83
|
+
|
|
84
|
+
MERGED_GRAMMAR.write_text(merged, encoding="utf-8")
|
|
85
|
+
return merged
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def build() -> None:
|
|
89
|
+
# The full CPython grammar is deep; generation recurses a lot.
|
|
90
|
+
sys.setrecursionlimit(200_000)
|
|
91
|
+
|
|
92
|
+
import token
|
|
93
|
+
|
|
94
|
+
from pegen.build import build_parser
|
|
95
|
+
from pegen.grammar import Alt, NameLeaf
|
|
96
|
+
from pegen.python_generator import (
|
|
97
|
+
PythonCallMakerVisitor,
|
|
98
|
+
PythonParserGenerator,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
# pegen 0.3.0 predates t-strings (Python 3.14). Map TSTRING_* tokens to
|
|
102
|
+
# expect() instead of a (nonexistent) self.TSTRING_START() method.
|
|
103
|
+
original_name_leaf = PythonCallMakerVisitor.visit_NameLeaf
|
|
104
|
+
|
|
105
|
+
def patched_name_leaf(self, node):
|
|
106
|
+
name = node.value
|
|
107
|
+
if name in ("TSTRING_START", "TSTRING_MIDDLE", "TSTRING_END"):
|
|
108
|
+
return "_" + name.lower(), f"self.expect({name!r})"
|
|
109
|
+
return original_name_leaf(self, node)
|
|
110
|
+
|
|
111
|
+
PythonCallMakerVisitor.visit_NameLeaf = patched_name_leaf
|
|
112
|
+
|
|
113
|
+
_build_grammar_text()
|
|
114
|
+
grammar, _, _ = build_parser(str(MERGED_GRAMMAR))
|
|
115
|
+
|
|
116
|
+
# pegen 0.3.0 ignores --skip-actions (a `# TODO` in the source) and still
|
|
117
|
+
# ast.parse()s every action, so the CPython C actions must be removed. The
|
|
118
|
+
# `@trailer` meta (C `_PyPegen_parse`) and the `[return_type]` annotations
|
|
119
|
+
# would also leak into the generated Python, so drop those too.
|
|
120
|
+
grammar.metas = {"trailer": ""} #type: ignore
|
|
121
|
+
renames = {r: r + "_rule" for r in COLLIDING_RULES if r in grammar.rules}
|
|
122
|
+
|
|
123
|
+
def strip(node, seen):
|
|
124
|
+
if id(node) in seen:
|
|
125
|
+
return
|
|
126
|
+
seen.add(id(node))
|
|
127
|
+
if isinstance(node, Alt) and node.action:
|
|
128
|
+
node.action = None
|
|
129
|
+
if isinstance(node, NameLeaf):
|
|
130
|
+
if node.value in renames:
|
|
131
|
+
node.value = renames[node.value]
|
|
132
|
+
return
|
|
133
|
+
for value in vars(node).values():
|
|
134
|
+
if isinstance(value, list):
|
|
135
|
+
for item in value:
|
|
136
|
+
strip(item, seen)
|
|
137
|
+
elif hasattr(value, "__dict__"):
|
|
138
|
+
strip(value, seen)
|
|
139
|
+
|
|
140
|
+
seen = set()
|
|
141
|
+
for rule in grammar.rules.values():
|
|
142
|
+
rule.type = None
|
|
143
|
+
strip(rule.rhs, seen)
|
|
144
|
+
|
|
145
|
+
for old, new in renames.items():
|
|
146
|
+
rule = grammar.rules.pop(old)
|
|
147
|
+
rule.name = new
|
|
148
|
+
grammar.rules[new] = rule
|
|
149
|
+
|
|
150
|
+
tokens = set(token.tok_name.values()) | EXTRA_TOKENS
|
|
151
|
+
with open(OUTPUT_PARSER, "w", encoding="utf-8") as out:
|
|
152
|
+
generator = PythonParserGenerator(grammar, out, tokens=tokens)
|
|
153
|
+
generator.generate(str(MERGED_GRAMMAR))
|
|
154
|
+
|
|
155
|
+
print(f"Generated {OUTPUT_PARSER.relative_to(ROOT)} ({len(grammar.rules)} rules)")
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
if __name__ == "__main__":
|
|
159
|
+
build()
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Quantar custom-operator extension.
|
|
2
|
+
#
|
|
3
|
+
# `hatch_build.py` (via lang_systems.pegen_gram_gen) splices this onto the vendored
|
|
4
|
+
# CPython grammar (Grammar/python.gram) and generates lang_systems/quantar_parser.py.
|
|
5
|
+
# Everything below is the *only* quantum-specific grammar; the whole rest of
|
|
6
|
+
# Python is inherited, so quantar is a strict superset of Python.
|
|
7
|
+
#
|
|
8
|
+
# Operators are matched against the `quantar.operators` registry at runtime, in
|
|
9
|
+
# two forms (see lang_systems/quantar.py):
|
|
10
|
+
#
|
|
11
|
+
# symbolic a <-> b matched by QUANTUM_OP (lang_systems/quantar_tokens.py)
|
|
12
|
+
# identifier a CNOT b matched by QUANTUM_NAME (any registered NAME)
|
|
13
|
+
#
|
|
14
|
+
# Precedence: custom operators bind *between* `comparison` and `bitwise_or`,
|
|
15
|
+
# on both sides of a comparison. lang_systems.pegen_gram_gen rewires
|
|
16
|
+
# `comparison: a=quantum_expr ...`; to move the level, change that splice and
|
|
17
|
+
# the rule living here. The resulting order, loosest to tightest, is:
|
|
18
|
+
#
|
|
19
|
+
# or > and > not > comparisons > quantum_operator > | ^ & << >> > + - > * / ...
|
|
20
|
+
|
|
21
|
+
quantum_expr[expr_ty]:
|
|
22
|
+
| a=quantum_expr quantum_operator b=bitwise_or
|
|
23
|
+
| bitwise_or
|
|
24
|
+
|
|
25
|
+
quantum_operator[expr_ty]:
|
|
26
|
+
| QUANTUM_OP
|
|
27
|
+
| QUANTUM_NAME
|
lang_systems/quantar.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Parse Quantar source into a parse tree.
|
|
2
|
+
|
|
3
|
+
Quantar is a superset of Python, so the parser is generated from CPython's
|
|
4
|
+
grammar (see lang_systems/pegen_gram_gen.py) plus a small operator overlay. This
|
|
5
|
+
module adds the two custom-operator token matchers the overlay refers to and a
|
|
6
|
+
convenient :func:`parse` entry point.
|
|
7
|
+
|
|
8
|
+
The result is pegen's raw tree (nested lists of tokens and sub-nodes); turning
|
|
9
|
+
it into an AST or executing it is intentionally a separate concern.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import sys
|
|
13
|
+
import token
|
|
14
|
+
|
|
15
|
+
from pegen.tokenizer import Tokenizer
|
|
16
|
+
|
|
17
|
+
from lang_systems.quantar_parser import GeneratedParser
|
|
18
|
+
from lang_systems.quantar_tokens import tokenize_source
|
|
19
|
+
from lang_systems.operators import is_known_operator
|
|
20
|
+
|
|
21
|
+
# The generated parser is deep; parsing the CPython grammar needs headroom.
|
|
22
|
+
_RECURSION_LIMIT = 200_000
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class QuantarParser(GeneratedParser):
|
|
26
|
+
"""The generated Python parser plus Quantar's custom-operator matchers."""
|
|
27
|
+
|
|
28
|
+
def QUANTUM_OP(self):
|
|
29
|
+
"""Match a symbolic operator (e.g. ``<->``) known to the language."""
|
|
30
|
+
peeked = self._tokenizer.peek()
|
|
31
|
+
if peeked.type == token.OP and is_known_operator(peeked.string):
|
|
32
|
+
return self._tokenizer.getnext()
|
|
33
|
+
return None
|
|
34
|
+
|
|
35
|
+
def QUANTUM_NAME(self):
|
|
36
|
+
"""Match an identifier operator (e.g. ``CNOT``) known to the language."""
|
|
37
|
+
peeked = self._tokenizer.peek()
|
|
38
|
+
if (
|
|
39
|
+
peeked.type == token.NAME
|
|
40
|
+
and peeked.string not in self.KEYWORDS
|
|
41
|
+
and is_known_operator(peeked.string)
|
|
42
|
+
):
|
|
43
|
+
return self._tokenizer.getnext()
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def parse(source: str):
|
|
48
|
+
"""Parse `source` and return the raw parse tree (the grammar's `file` rule).
|
|
49
|
+
|
|
50
|
+
Raises:
|
|
51
|
+
SyntaxError: If `source` is not valid Quantar.
|
|
52
|
+
"""
|
|
53
|
+
sys.setrecursionlimit(max(sys.getrecursionlimit(), _RECURSION_LIMIT))
|
|
54
|
+
tokenizer = Tokenizer(tokenize_source(source))
|
|
55
|
+
parser = QuantarParser(tokenizer)
|
|
56
|
+
tree = parser.file()
|
|
57
|
+
if tree is None:
|
|
58
|
+
bad = parser._tokenizer.peek()
|
|
59
|
+
raise SyntaxError(
|
|
60
|
+
f"invalid Quantar syntax near {bad.string!r}",
|
|
61
|
+
("<quantar>", bad.start[0], bad.start[1] + 1, bad.line),
|
|
62
|
+
)
|
|
63
|
+
return tree
|