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.
@@ -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
@@ -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