qoro-divi 0.2.1b1__py3-none-any.whl → 0.3.0b1__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.
Potentially problematic release.
This version of qoro-divi might be problematic. Click here for more details.
- divi/_pbar.py +1 -3
- divi/circuits.py +3 -3
- divi/exp/cirq/__init__.py +1 -0
- divi/exp/cirq/_validator.py +645 -0
- divi/parallel_simulator.py +9 -9
- divi/qasm.py +2 -3
- divi/qoro_service.py +210 -141
- divi/qprog/__init__.py +2 -2
- divi/qprog/_graph_partitioning.py +103 -66
- divi/qprog/_qaoa.py +33 -8
- divi/qprog/_qubo_partitioning.py +199 -0
- divi/qprog/_vqe.py +48 -39
- divi/qprog/_vqe_sweep.py +413 -46
- divi/qprog/batch.py +61 -14
- divi/qprog/quantum_program.py +10 -11
- divi/qpu_system.py +20 -0
- qoro_divi-0.3.0b1.dist-info/LICENSES/.license-header +3 -0
- {qoro_divi-0.2.1b1.dist-info → qoro_divi-0.3.0b1.dist-info}/METADATA +10 -3
- {qoro_divi-0.2.1b1.dist-info → qoro_divi-0.3.0b1.dist-info}/RECORD +22 -19
- divi/qprog/_mlae.py +0 -182
- {qoro_divi-0.2.1b1.dist-info → qoro_divi-0.3.0b1.dist-info}/LICENSE +0 -0
- {qoro_divi-0.2.1b1.dist-info → qoro_divi-0.3.0b1.dist-info}/LICENSES/Apache-2.0.txt +0 -0
- {qoro_divi-0.2.1b1.dist-info → qoro_divi-0.3.0b1.dist-info}/WHEEL +0 -0
divi/_pbar.py
CHANGED
|
@@ -2,8 +2,6 @@
|
|
|
2
2
|
#
|
|
3
3
|
# SPDX-License-Identifier: Apache-2.0
|
|
4
4
|
|
|
5
|
-
from typing import Optional
|
|
6
|
-
|
|
7
5
|
from rich.progress import (
|
|
8
6
|
BarColumn,
|
|
9
7
|
MofNCompleteColumn,
|
|
@@ -58,7 +56,7 @@ class PhaseStatusColumn(ProgressColumn):
|
|
|
58
56
|
|
|
59
57
|
|
|
60
58
|
def make_progress_bar(
|
|
61
|
-
max_retries:
|
|
59
|
+
max_retries: int | None = None, is_jupyter: bool = False
|
|
62
60
|
) -> Progress:
|
|
63
61
|
return Progress(
|
|
64
62
|
TextColumn("[bold blue]{task.fields[job_name]}"),
|
divi/circuits.py
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import re
|
|
6
6
|
from copy import deepcopy
|
|
7
7
|
from itertools import product
|
|
8
|
-
from typing import Literal
|
|
8
|
+
from typing import Literal
|
|
9
9
|
|
|
10
10
|
import dill
|
|
11
11
|
import pennylane as qml
|
|
@@ -66,8 +66,8 @@ class MetaCircuit:
|
|
|
66
66
|
self,
|
|
67
67
|
main_circuit,
|
|
68
68
|
symbols,
|
|
69
|
-
grouping_strategy:
|
|
70
|
-
qem_protocol:
|
|
69
|
+
grouping_strategy: Literal["wires", "default", "qwc"] | None = None,
|
|
70
|
+
qem_protocol: QEMProtocol | None = None,
|
|
71
71
|
):
|
|
72
72
|
self.main_circuit = main_circuit
|
|
73
73
|
self.symbols = symbols
|
divi/exp/cirq/__init__.py
CHANGED
|
@@ -0,0 +1,645 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2025 Qoro Quantum Ltd <divi@qoroquantum.de>
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from typing import NamedTuple
|
|
7
|
+
|
|
8
|
+
# ---------- Lexer ----------
|
|
9
|
+
_WS_RE = re.compile(r"\s+")
|
|
10
|
+
_LINE_COMMENT_RE = re.compile(r"//[^\n]*")
|
|
11
|
+
_BLOCK_COMMENT_RE = re.compile(r"/\*.*?\*/", re.DOTALL)
|
|
12
|
+
|
|
13
|
+
TOKEN_SPECS = [
|
|
14
|
+
("ARROW", r"->"),
|
|
15
|
+
("EQ", r"=="),
|
|
16
|
+
("LBRACE", r"\{"),
|
|
17
|
+
("RBRACE", r"\}"),
|
|
18
|
+
("LPAREN", r"\("),
|
|
19
|
+
("RPAREN", r"\)"),
|
|
20
|
+
("LBRACKET", r"\["),
|
|
21
|
+
("RBRACKET", r"\]"),
|
|
22
|
+
("COMMA", r","),
|
|
23
|
+
("SEMI", r";"),
|
|
24
|
+
("STAR", r"\*"),
|
|
25
|
+
("SLASH", r"/"),
|
|
26
|
+
("PLUS", r"\+"),
|
|
27
|
+
("MINUS", r"-"),
|
|
28
|
+
("CARET", r"\^"),
|
|
29
|
+
("STRING", r"\"[^\"\n]*\""),
|
|
30
|
+
("NUMBER", r"\d+(?:\.\d+)?"),
|
|
31
|
+
("ID", r"[A-Za-z_][A-Za-z0-9_]*"),
|
|
32
|
+
]
|
|
33
|
+
TOKEN_REGEX = re.compile("|".join(f"(?P<{n}>{p})" for n, p in TOKEN_SPECS))
|
|
34
|
+
|
|
35
|
+
KEYWORDS = {
|
|
36
|
+
"OPENQASM",
|
|
37
|
+
"include",
|
|
38
|
+
"qreg",
|
|
39
|
+
"creg",
|
|
40
|
+
"qubit",
|
|
41
|
+
"bit",
|
|
42
|
+
"gate",
|
|
43
|
+
"barrier",
|
|
44
|
+
"measure",
|
|
45
|
+
"reset",
|
|
46
|
+
"if",
|
|
47
|
+
"pi",
|
|
48
|
+
"sin",
|
|
49
|
+
"cos",
|
|
50
|
+
"tan",
|
|
51
|
+
"exp",
|
|
52
|
+
"ln",
|
|
53
|
+
"sqrt",
|
|
54
|
+
"acos",
|
|
55
|
+
"atan",
|
|
56
|
+
"asin",
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class Tok(NamedTuple):
|
|
61
|
+
type: str
|
|
62
|
+
value: str
|
|
63
|
+
pos: int
|
|
64
|
+
line: int
|
|
65
|
+
col: int
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _strip_comments(src: str) -> str:
|
|
69
|
+
return _LINE_COMMENT_RE.sub("", _BLOCK_COMMENT_RE.sub("", src))
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _lex(src: str) -> list[Tok]:
|
|
73
|
+
src = _strip_comments(src)
|
|
74
|
+
i, n = 0, len(src)
|
|
75
|
+
line, line_start = 1, 0
|
|
76
|
+
out: list[Tok] = []
|
|
77
|
+
while i < n:
|
|
78
|
+
m = _WS_RE.match(src, i)
|
|
79
|
+
if m:
|
|
80
|
+
chunk = src[i : m.end()]
|
|
81
|
+
nl = chunk.count("\n")
|
|
82
|
+
if nl:
|
|
83
|
+
line += nl
|
|
84
|
+
line_start = m.end() - (len(chunk) - chunk.rfind("\n") - 1)
|
|
85
|
+
i = m.end()
|
|
86
|
+
continue
|
|
87
|
+
m = TOKEN_REGEX.match(src, i)
|
|
88
|
+
if not m:
|
|
89
|
+
snippet = src[i : i + 20].replace("\n", "\\n")
|
|
90
|
+
raise SyntaxError(
|
|
91
|
+
f"Illegal character at {line}:{i-line_start+1}: {snippet!r}"
|
|
92
|
+
)
|
|
93
|
+
kind = m.lastgroup
|
|
94
|
+
val = m.group(kind)
|
|
95
|
+
col = i - line_start + 1
|
|
96
|
+
if kind == "ID" and val in KEYWORDS:
|
|
97
|
+
kind = val.upper()
|
|
98
|
+
out.append(Tok(kind, val, i, line, col))
|
|
99
|
+
i = m.end()
|
|
100
|
+
out.append(Tok("EOF", "", i, line, i - line_start + 1))
|
|
101
|
+
return out
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
# ---------- Built-in gates (name -> (num_params, num_qubits)) ----------
|
|
105
|
+
BUILTINS: dict[str, tuple[int, int]] = {
|
|
106
|
+
# 1q
|
|
107
|
+
"id": (0, 1),
|
|
108
|
+
"x": (0, 1),
|
|
109
|
+
"y": (0, 1),
|
|
110
|
+
"z": (0, 1),
|
|
111
|
+
"h": (0, 1),
|
|
112
|
+
"s": (0, 1),
|
|
113
|
+
"sdg": (0, 1),
|
|
114
|
+
"t": (0, 1),
|
|
115
|
+
"tdg": (0, 1),
|
|
116
|
+
"sx": (0, 1),
|
|
117
|
+
"sxdg": (0, 1),
|
|
118
|
+
"rx": (1, 1),
|
|
119
|
+
"ry": (1, 1),
|
|
120
|
+
"rz": (1, 1),
|
|
121
|
+
"u1": (1, 1),
|
|
122
|
+
"u2": (2, 1),
|
|
123
|
+
"u3": (3, 1),
|
|
124
|
+
"u": (3, 1), # allow 'u' alias
|
|
125
|
+
"U": (3, 1),
|
|
126
|
+
# 2q
|
|
127
|
+
"cx": (0, 2),
|
|
128
|
+
"cy": (0, 2),
|
|
129
|
+
"cz": (0, 2),
|
|
130
|
+
"iswap": (0, 2),
|
|
131
|
+
"swap": (0, 2),
|
|
132
|
+
"rxx": (1, 2),
|
|
133
|
+
"ryy": (1, 2),
|
|
134
|
+
"rzz": (1, 2),
|
|
135
|
+
"crx": (1, 2),
|
|
136
|
+
"cry": (1, 2),
|
|
137
|
+
"crz": (1, 2),
|
|
138
|
+
"cu1": (1, 2),
|
|
139
|
+
"cu3": (3, 2),
|
|
140
|
+
"ch": (0, 2),
|
|
141
|
+
# 3q
|
|
142
|
+
"ccx": (0, 3),
|
|
143
|
+
"cswap": (0, 3),
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
_MATH_FUNCS = {"SIN", "COS", "TAN", "EXP", "LN", "SQRT", "ACOS", "ATAN", "ASIN"}
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
# ---------- Parser with symbol checks ----------
|
|
150
|
+
class Parser:
|
|
151
|
+
def __init__(self, toks: list[Tok]):
|
|
152
|
+
self.toks = toks
|
|
153
|
+
self.i = 0
|
|
154
|
+
# symbols
|
|
155
|
+
self.qregs: dict[str, int] = {}
|
|
156
|
+
self.cregs: dict[str, int] = {}
|
|
157
|
+
self.user_gates: dict[str, tuple[tuple[str, ...], tuple[str, ...]]] = {}
|
|
158
|
+
# gate-def scope
|
|
159
|
+
self.in_gate_def = False
|
|
160
|
+
self.g_params: set[str] = set()
|
|
161
|
+
self.g_qubits: set[str] = set()
|
|
162
|
+
|
|
163
|
+
# -- helpers --
|
|
164
|
+
def peek(self, k=0) -> Tok:
|
|
165
|
+
j = self.i + k
|
|
166
|
+
return self.toks[j] if j < len(self.toks) else self.toks[-1]
|
|
167
|
+
|
|
168
|
+
def match(self, *types: str) -> Tok:
|
|
169
|
+
t = self.peek()
|
|
170
|
+
if t.type in types:
|
|
171
|
+
self.i += 1
|
|
172
|
+
return t
|
|
173
|
+
exp = " or ".join(types)
|
|
174
|
+
raise SyntaxError(f"Expected {exp} at {t.line}:{t.col}, got {t.type}")
|
|
175
|
+
|
|
176
|
+
def accept(self, *types: str) -> Tok | None:
|
|
177
|
+
if self.peek().type in types:
|
|
178
|
+
return self.match(*types)
|
|
179
|
+
return None
|
|
180
|
+
|
|
181
|
+
# -- entry --
|
|
182
|
+
def parse(self):
|
|
183
|
+
self.header()
|
|
184
|
+
while self.accept("INCLUDE"):
|
|
185
|
+
self.include_stmt()
|
|
186
|
+
while self.peek().type != "EOF":
|
|
187
|
+
start_line = self.peek().line
|
|
188
|
+
self.statement()
|
|
189
|
+
# After statement, check if it ended correctly
|
|
190
|
+
prev_tok = self.toks[self.i - 1] if self.i > 0 else None
|
|
191
|
+
# A statement is valid if it ends in a semicolon OR a closing brace (for gates)
|
|
192
|
+
if not prev_tok or (prev_tok.type != "SEMI" and prev_tok.type != "RBRACE"):
|
|
193
|
+
raise SyntaxError(
|
|
194
|
+
f"Statement at line {start_line} must end with a semicolon or a closing brace."
|
|
195
|
+
)
|
|
196
|
+
self.match("EOF")
|
|
197
|
+
|
|
198
|
+
# OPENQASM 2.0 ;
|
|
199
|
+
def header(self):
|
|
200
|
+
self.match("OPENQASM")
|
|
201
|
+
v = self.match("NUMBER")
|
|
202
|
+
if v.value not in ("2.0", "2", "3.0", "3"):
|
|
203
|
+
raise SyntaxError(
|
|
204
|
+
f"Unsupported OPENQASM version '{v.value}' at {v.line}:{v.col}"
|
|
205
|
+
)
|
|
206
|
+
self.match("SEMI")
|
|
207
|
+
|
|
208
|
+
def include_stmt(self):
|
|
209
|
+
self.match("STRING")
|
|
210
|
+
self.match("SEMI")
|
|
211
|
+
|
|
212
|
+
def statement(self):
|
|
213
|
+
t = self.peek().type
|
|
214
|
+
if t == "QREG":
|
|
215
|
+
self.qreg_decl()
|
|
216
|
+
elif t == "CREG":
|
|
217
|
+
self.creg_decl()
|
|
218
|
+
elif t == "QUBIT":
|
|
219
|
+
self.qubit_decl()
|
|
220
|
+
elif t == "BIT":
|
|
221
|
+
self.bit_decl()
|
|
222
|
+
elif t == "GATE":
|
|
223
|
+
self.gate_def()
|
|
224
|
+
elif t == "MEASURE":
|
|
225
|
+
self.measure_stmt()
|
|
226
|
+
elif t == "RESET":
|
|
227
|
+
self.reset_stmt()
|
|
228
|
+
elif t == "BARRIER":
|
|
229
|
+
self.barrier_stmt()
|
|
230
|
+
elif t == "IF":
|
|
231
|
+
self.if_stmt()
|
|
232
|
+
elif t == "ID":
|
|
233
|
+
self.gate_op_stmt_top()
|
|
234
|
+
else:
|
|
235
|
+
tok = self.peek()
|
|
236
|
+
raise SyntaxError(f"Unexpected token {tok.type} at {tok.line}:{tok.col}")
|
|
237
|
+
|
|
238
|
+
# ---- declarations ----
|
|
239
|
+
def qreg_decl(self):
|
|
240
|
+
self.match("QREG")
|
|
241
|
+
name = self.match("ID").value
|
|
242
|
+
self.match("LBRACKET")
|
|
243
|
+
n = self.natural_number_tok()
|
|
244
|
+
self.match("RBRACKET")
|
|
245
|
+
self.match("SEMI")
|
|
246
|
+
if name in self.qregs or name in self.cregs:
|
|
247
|
+
self._dupe(name)
|
|
248
|
+
self.qregs[name] = n
|
|
249
|
+
|
|
250
|
+
def creg_decl(self):
|
|
251
|
+
self.match("CREG")
|
|
252
|
+
name = self.match("ID").value
|
|
253
|
+
self.match("LBRACKET")
|
|
254
|
+
n = self.natural_number_tok()
|
|
255
|
+
self.match("RBRACKET")
|
|
256
|
+
self.match("SEMI")
|
|
257
|
+
if name in self.qregs or name in self.cregs:
|
|
258
|
+
self._dupe(name)
|
|
259
|
+
self.cregs[name] = n
|
|
260
|
+
|
|
261
|
+
def qubit_decl(self):
|
|
262
|
+
self.match("QUBIT")
|
|
263
|
+
if self.accept("LBRACKET"):
|
|
264
|
+
n = self.natural_number_tok()
|
|
265
|
+
self.match("RBRACKET")
|
|
266
|
+
name = self.match("ID").value
|
|
267
|
+
else:
|
|
268
|
+
name = self.match("ID").value
|
|
269
|
+
n = 1
|
|
270
|
+
self.match("SEMI")
|
|
271
|
+
if name in self.qregs or name in self.cregs:
|
|
272
|
+
self._dupe(name)
|
|
273
|
+
self.qregs[name] = n
|
|
274
|
+
|
|
275
|
+
def bit_decl(self):
|
|
276
|
+
self.match("BIT")
|
|
277
|
+
if self.accept("LBRACKET"):
|
|
278
|
+
n = self.natural_number_tok()
|
|
279
|
+
self.match("RBRACKET")
|
|
280
|
+
name = self.match("ID").value
|
|
281
|
+
else:
|
|
282
|
+
name = self.match("ID").value
|
|
283
|
+
n = 1
|
|
284
|
+
self.match("SEMI")
|
|
285
|
+
if name in self.qregs or name in self.cregs:
|
|
286
|
+
self._dupe(name)
|
|
287
|
+
self.cregs[name] = n
|
|
288
|
+
|
|
289
|
+
# ---- gate definitions ----
|
|
290
|
+
def gate_def(self):
|
|
291
|
+
self.match("GATE")
|
|
292
|
+
gname = self.match("ID").value
|
|
293
|
+
if gname in self.user_gates:
|
|
294
|
+
self._dupe(gname)
|
|
295
|
+
params: tuple[str, ...] = ()
|
|
296
|
+
if self.accept("LPAREN"):
|
|
297
|
+
params = self._id_list_tuple()
|
|
298
|
+
self.match("RPAREN")
|
|
299
|
+
qubits = self._id_list_tuple()
|
|
300
|
+
# enter scope
|
|
301
|
+
saved = (self.in_gate_def, self.g_params.copy(), self.g_qubits.copy())
|
|
302
|
+
self.in_gate_def = True
|
|
303
|
+
self.g_params = set(params)
|
|
304
|
+
self.g_qubits = set(qubits)
|
|
305
|
+
self.match("LBRACE")
|
|
306
|
+
# body: only gate ops; they can use local qubit ids and local params in expr
|
|
307
|
+
while self.peek().type == "ID":
|
|
308
|
+
self.gate_op_stmt_in_body()
|
|
309
|
+
self.match("RBRACE")
|
|
310
|
+
# leave scope
|
|
311
|
+
self.in_gate_def, self.g_params, self.g_qubits = saved
|
|
312
|
+
self.user_gates[gname] = (params, qubits)
|
|
313
|
+
|
|
314
|
+
def _id_list_tuple(self) -> tuple[str, ...]:
|
|
315
|
+
ids = [self.match("ID").value]
|
|
316
|
+
while self.accept("COMMA"):
|
|
317
|
+
ids.append(self.match("ID").value)
|
|
318
|
+
return tuple(ids)
|
|
319
|
+
|
|
320
|
+
# ---- gate operations ----
|
|
321
|
+
def gate_op_stmt_top(self):
|
|
322
|
+
name_tok = self.match("ID")
|
|
323
|
+
gname = name_tok.value
|
|
324
|
+
param_count = None
|
|
325
|
+
arity = None
|
|
326
|
+
|
|
327
|
+
if self.accept("LPAREN"):
|
|
328
|
+
n_params = self._expr_list_count(allow_id=False) # top-level: no free IDs
|
|
329
|
+
self.match("RPAREN")
|
|
330
|
+
else:
|
|
331
|
+
n_params = 0
|
|
332
|
+
|
|
333
|
+
# resolve gate signature
|
|
334
|
+
if gname in BUILTINS:
|
|
335
|
+
param_count, arity = BUILTINS[gname]
|
|
336
|
+
elif gname in self.user_gates:
|
|
337
|
+
param_count, arity = len(self.user_gates[gname][0]), len(
|
|
338
|
+
self.user_gates[gname][1]
|
|
339
|
+
)
|
|
340
|
+
else:
|
|
341
|
+
self._unknown_gate(name_tok)
|
|
342
|
+
|
|
343
|
+
if n_params != param_count:
|
|
344
|
+
raise SyntaxError(
|
|
345
|
+
f"Gate '{gname}' expects {param_count} params, got {n_params} at {name_tok.line}:{name_tok.col}"
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
args, reg_sizes = self.qarg_list_top(arity)
|
|
349
|
+
# broadcast check: all full-register sizes must match if >1
|
|
350
|
+
sizes = {s for s in reg_sizes if s > 1}
|
|
351
|
+
if len(sizes) > 1:
|
|
352
|
+
raise SyntaxError(
|
|
353
|
+
f"Mismatched register sizes in arguments to '{gname}' at {name_tok.line}:{name_tok.col}"
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
self.match("SEMI")
|
|
357
|
+
|
|
358
|
+
def gate_op_stmt_in_body(self):
|
|
359
|
+
name_tok = self.match("ID")
|
|
360
|
+
gname = name_tok.value
|
|
361
|
+
|
|
362
|
+
if self.accept("LPAREN"):
|
|
363
|
+
n_params = self._expr_list_count(allow_id=True) # may use local params
|
|
364
|
+
self.match("RPAREN")
|
|
365
|
+
else:
|
|
366
|
+
n_params = 0
|
|
367
|
+
|
|
368
|
+
if gname in BUILTINS:
|
|
369
|
+
param_count, arity = BUILTINS[gname]
|
|
370
|
+
elif gname in self.user_gates:
|
|
371
|
+
param_count, arity = len(self.user_gates[gname][0]), len(
|
|
372
|
+
self.user_gates[gname][1]
|
|
373
|
+
)
|
|
374
|
+
else:
|
|
375
|
+
self._unknown_gate(name_tok)
|
|
376
|
+
|
|
377
|
+
if n_params != param_count:
|
|
378
|
+
raise SyntaxError(
|
|
379
|
+
f"Gate '{gname}' expects {param_count} params, got {n_params} at {name_tok.line}:{name_tok.col}"
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
# In gate bodies, qargs must be local gate-qubit identifiers (no indexing)
|
|
383
|
+
qids = [self._gate_body_qid()]
|
|
384
|
+
while self.accept("COMMA"):
|
|
385
|
+
qids.append(self._gate_body_qid())
|
|
386
|
+
if len(qids) != arity:
|
|
387
|
+
raise SyntaxError(
|
|
388
|
+
f"Gate '{gname}' expects {arity} qubit args in body, got {len(qids)} at {name_tok.line}:{name_tok.col}"
|
|
389
|
+
)
|
|
390
|
+
self.match("SEMI")
|
|
391
|
+
|
|
392
|
+
def _gate_body_qid(self) -> str:
|
|
393
|
+
if self.peek().type != "ID":
|
|
394
|
+
t = self.peek()
|
|
395
|
+
raise SyntaxError(f"Expected gate-qubit id at {t.line}:{t.col}")
|
|
396
|
+
name = self.match("ID").value
|
|
397
|
+
if name not in self.g_qubits:
|
|
398
|
+
t = self.peek(-1)
|
|
399
|
+
raise SyntaxError(
|
|
400
|
+
f"Unknown gate-qubit '{name}' in gate body at {t.line}:{t.col}"
|
|
401
|
+
)
|
|
402
|
+
return name
|
|
403
|
+
|
|
404
|
+
# qarg list at top-level: IDs may be full registers or indexed bits q[i]
|
|
405
|
+
def qarg_list_top(
|
|
406
|
+
self, expected_arity: int
|
|
407
|
+
) -> tuple[list[tuple[str, int | None]], list[int]]:
|
|
408
|
+
args = [self.qarg_top()]
|
|
409
|
+
while self.accept("COMMA"):
|
|
410
|
+
args.append(self.qarg_top())
|
|
411
|
+
if len(args) != expected_arity:
|
|
412
|
+
t = self.peek()
|
|
413
|
+
raise SyntaxError(
|
|
414
|
+
f"Expected {expected_arity} qubit args, got {len(args)} at {t.line}:{t.col}"
|
|
415
|
+
)
|
|
416
|
+
# return sizes for broadcast check
|
|
417
|
+
reg_sizes = [(self.qregs[name] if idx is None else 1) for (name, idx) in args]
|
|
418
|
+
return args, reg_sizes
|
|
419
|
+
|
|
420
|
+
def qarg_top(self) -> tuple[str, int | None]:
|
|
421
|
+
name_tok = self.match("ID")
|
|
422
|
+
name = name_tok.value
|
|
423
|
+
if name not in self.qregs:
|
|
424
|
+
raise SyntaxError(
|
|
425
|
+
f"Unknown qreg '{name}' at {name_tok.line}:{name_tok.col}"
|
|
426
|
+
)
|
|
427
|
+
if self.accept("LBRACKET"):
|
|
428
|
+
idx_tok = self.natural_number_tok_tok()
|
|
429
|
+
self.match("RBRACKET")
|
|
430
|
+
if int(idx_tok.value) >= self.qregs[name]:
|
|
431
|
+
raise SyntaxError(
|
|
432
|
+
f"Qubit index {idx_tok.value} out of range for '{name}[{self.qregs[name]}]' at {idx_tok.line}:{idx_tok.col}"
|
|
433
|
+
)
|
|
434
|
+
return (name, int(idx_tok.value))
|
|
435
|
+
return (name, None) # full register
|
|
436
|
+
|
|
437
|
+
# ---- measure/reset/barrier/if ----
|
|
438
|
+
def measure_stmt(self):
|
|
439
|
+
# two forms: measure qarg -> carg ; | carg = measure qarg ;
|
|
440
|
+
if self.peek().type == "MEASURE":
|
|
441
|
+
self.match("MEASURE")
|
|
442
|
+
q_t, q_sz = self._measure_qarg()
|
|
443
|
+
self.match("ARROW")
|
|
444
|
+
c_t, c_sz = self._measure_carg()
|
|
445
|
+
else:
|
|
446
|
+
# handled only when starts with MEASURE in statement(), so unreachable
|
|
447
|
+
raise SyntaxError("Internal: measure_stmt misuse")
|
|
448
|
+
if q_sz != c_sz:
|
|
449
|
+
t = self.peek()
|
|
450
|
+
raise SyntaxError(
|
|
451
|
+
f"Measurement size mismatch {q_sz} -> {c_sz} at {t.line}:{t.col}"
|
|
452
|
+
)
|
|
453
|
+
self.match("SEMI")
|
|
454
|
+
|
|
455
|
+
def _measure_qarg(self) -> tuple[str, int]:
|
|
456
|
+
name_tok = self.match("ID")
|
|
457
|
+
name = name_tok.value
|
|
458
|
+
if name not in self.qregs:
|
|
459
|
+
raise SyntaxError(
|
|
460
|
+
f"Unknown qreg '{name}' at {name_tok.line}:{name_tok.col}"
|
|
461
|
+
)
|
|
462
|
+
if self.accept("LBRACKET"):
|
|
463
|
+
idx = self.natural_number_tok()
|
|
464
|
+
self.match("RBRACKET")
|
|
465
|
+
if idx >= self.qregs[name]:
|
|
466
|
+
raise SyntaxError(f"Qubit index {idx} out of range for '{name}'")
|
|
467
|
+
return (f"{name}[{idx}]", 1)
|
|
468
|
+
return (name, self.qregs[name])
|
|
469
|
+
|
|
470
|
+
def _measure_carg(self) -> tuple[str, int]:
|
|
471
|
+
name_tok = self.match("ID")
|
|
472
|
+
name = name_tok.value
|
|
473
|
+
if name not in self.cregs:
|
|
474
|
+
raise SyntaxError(
|
|
475
|
+
f"Unknown creg '{name}' at {name_tok.line}:{name_tok.col}"
|
|
476
|
+
)
|
|
477
|
+
if self.accept("LBRACKET"):
|
|
478
|
+
idx = self.natural_number_tok()
|
|
479
|
+
self.match("RBRACKET")
|
|
480
|
+
if idx >= self.cregs[name]:
|
|
481
|
+
raise SyntaxError(f"Bit index {idx} out of range for '{name}'")
|
|
482
|
+
return (f"{name}[{idx}]", 1)
|
|
483
|
+
return (name, self.cregs[name])
|
|
484
|
+
|
|
485
|
+
def reset_stmt(self):
|
|
486
|
+
self.match("RESET")
|
|
487
|
+
# allow full reg or single index
|
|
488
|
+
name_tok = self.match("ID")
|
|
489
|
+
name = name_tok.value
|
|
490
|
+
if name not in self.qregs:
|
|
491
|
+
raise SyntaxError(
|
|
492
|
+
f"Unknown qreg '{name}' at {name_tok.line}:{name_tok.col}"
|
|
493
|
+
)
|
|
494
|
+
if self.accept("LBRACKET"):
|
|
495
|
+
idx = self.natural_number_tok()
|
|
496
|
+
self.match("RBRACKET")
|
|
497
|
+
if idx >= self.qregs[name]:
|
|
498
|
+
raise SyntaxError(f"Qubit index {idx} out of range for '{name}'")
|
|
499
|
+
self.match("SEMI")
|
|
500
|
+
|
|
501
|
+
def barrier_stmt(self):
|
|
502
|
+
self.match("BARrier".upper()) # tolerate case in tokenization
|
|
503
|
+
# barrier accepts one or more qargs (full regs and/or indices)
|
|
504
|
+
self.qarg_top()
|
|
505
|
+
while self.accept("COMMA"):
|
|
506
|
+
self.qarg_top()
|
|
507
|
+
self.match("SEMI")
|
|
508
|
+
|
|
509
|
+
def if_stmt(self):
|
|
510
|
+
self.match("IF")
|
|
511
|
+
self.match("LPAREN")
|
|
512
|
+
cname_tok = self.match("ID")
|
|
513
|
+
cname = cname_tok.value
|
|
514
|
+
if cname not in self.cregs:
|
|
515
|
+
raise SyntaxError(
|
|
516
|
+
f"Unknown creg '{cname}' at {cname_tok.line}:{cname_tok.col}"
|
|
517
|
+
)
|
|
518
|
+
self.match("EQ")
|
|
519
|
+
val_tok = self.natural_number_tok_tok()
|
|
520
|
+
self.match("RPAREN")
|
|
521
|
+
if int(val_tok.value) >= (1 << self.cregs[cname]):
|
|
522
|
+
raise SyntaxError(
|
|
523
|
+
f"if() value {val_tok.value} exceeds creg width {self.cregs[cname]}"
|
|
524
|
+
)
|
|
525
|
+
# must be a single gate op
|
|
526
|
+
self.gate_op_stmt_top()
|
|
527
|
+
|
|
528
|
+
# ---- expressions (with symbol policy) ----
|
|
529
|
+
def _expr_list_count(self, *, allow_id: bool) -> int:
|
|
530
|
+
# count expressions in list; expressions may reference IDs only if allow_id
|
|
531
|
+
count = 0
|
|
532
|
+
self._expr(allow_id)
|
|
533
|
+
count += 1
|
|
534
|
+
while self.accept("COMMA"):
|
|
535
|
+
self._expr(allow_id)
|
|
536
|
+
count += 1
|
|
537
|
+
return count
|
|
538
|
+
|
|
539
|
+
def _expr(self, allow_id: bool):
|
|
540
|
+
self._expr_addsub(allow_id)
|
|
541
|
+
|
|
542
|
+
def _expr_addsub(self, allow_id: bool):
|
|
543
|
+
self._expr_muldiv(allow_id)
|
|
544
|
+
while self.peek().type in ("PLUS", "MINUS"):
|
|
545
|
+
self.match(self.peek().type)
|
|
546
|
+
self._expr_muldiv(allow_id)
|
|
547
|
+
|
|
548
|
+
def _expr_muldiv(self, allow_id: bool):
|
|
549
|
+
self._expr_power(allow_id)
|
|
550
|
+
while self.peek().type in ("STAR", "SLASH"):
|
|
551
|
+
self.match(self.peek().type)
|
|
552
|
+
self._expr_power(allow_id)
|
|
553
|
+
|
|
554
|
+
def _expr_power(self, allow_id: bool):
|
|
555
|
+
self._expr_unary(allow_id)
|
|
556
|
+
if self.peek().type == "CARET":
|
|
557
|
+
self.match("CARET")
|
|
558
|
+
self._expr_power(allow_id)
|
|
559
|
+
|
|
560
|
+
def _expr_unary(self, allow_id: bool):
|
|
561
|
+
if self.peek().type in ("PLUS", "MINUS"):
|
|
562
|
+
self.match(self.peek().type)
|
|
563
|
+
self._expr_atom(allow_id)
|
|
564
|
+
|
|
565
|
+
def _expr_atom(self, allow_id: bool):
|
|
566
|
+
t = self.peek()
|
|
567
|
+
if t.type == "NUMBER":
|
|
568
|
+
self.match("NUMBER")
|
|
569
|
+
return
|
|
570
|
+
if t.type == "PI":
|
|
571
|
+
self.match("PI")
|
|
572
|
+
return
|
|
573
|
+
# ---- NEW BLOCK TO HANDLE MATH FUNCTIONS ----
|
|
574
|
+
if t.type in _MATH_FUNCS:
|
|
575
|
+
self.match(t.type) # Consume the function name (e.g., COS)
|
|
576
|
+
self.match("LPAREN")
|
|
577
|
+
self._expr(allow_id) # Parse the inner expression
|
|
578
|
+
# Note: QASM 2.0 math functions only take one argument
|
|
579
|
+
self.match("RPAREN")
|
|
580
|
+
return
|
|
581
|
+
# --------------------------------------------
|
|
582
|
+
if t.type == "ID":
|
|
583
|
+
# function call or plain ID
|
|
584
|
+
id_tok = self.match("ID")
|
|
585
|
+
ident = id_tok.value
|
|
586
|
+
if self.accept("LPAREN"):
|
|
587
|
+
# This now correctly handles user-defined functions (if any)
|
|
588
|
+
if self.peek().type != "RPAREN":
|
|
589
|
+
self._expr(allow_id)
|
|
590
|
+
while self.accept("COMMA"):
|
|
591
|
+
self._expr(allow_id)
|
|
592
|
+
self.match("RPAREN")
|
|
593
|
+
return
|
|
594
|
+
# bare identifier: only allowed if in gate body params and allow_id=True
|
|
595
|
+
if not allow_id or ident not in self.g_params:
|
|
596
|
+
raise SyntaxError(
|
|
597
|
+
f"Unknown symbol '{ident}' in expression at {id_tok.line}:{id_tok.col}"
|
|
598
|
+
)
|
|
599
|
+
return
|
|
600
|
+
if t.type == "LPAREN":
|
|
601
|
+
self.match("LPAREN")
|
|
602
|
+
self._expr(allow_id)
|
|
603
|
+
self.match("RPAREN")
|
|
604
|
+
return
|
|
605
|
+
raise SyntaxError(
|
|
606
|
+
f"Unexpected token {t.type} in expression at {t.line}:{t.col}"
|
|
607
|
+
)
|
|
608
|
+
|
|
609
|
+
# ---- numbers / errors ----
|
|
610
|
+
def natural_number_tok(self) -> int:
|
|
611
|
+
t = self.match("NUMBER")
|
|
612
|
+
if "." in t.value:
|
|
613
|
+
raise SyntaxError(
|
|
614
|
+
f"Expected natural number at {t.line}:{t.col}, got {t.value}"
|
|
615
|
+
)
|
|
616
|
+
return int(t.value)
|
|
617
|
+
|
|
618
|
+
def natural_number_tok_tok(self) -> Tok:
|
|
619
|
+
t = self.match("NUMBER")
|
|
620
|
+
if "." in t.value:
|
|
621
|
+
raise SyntaxError(
|
|
622
|
+
f"Expected natural number at {t.line}:{t.col}, got {t.value}"
|
|
623
|
+
)
|
|
624
|
+
return t
|
|
625
|
+
|
|
626
|
+
def _dupe(self, name: str):
|
|
627
|
+
t = self.peek()
|
|
628
|
+
raise SyntaxError(f"Redefinition of '{name}' at {t.line}:{t.col}")
|
|
629
|
+
|
|
630
|
+
def _unknown_gate(self, tok: Tok):
|
|
631
|
+
raise SyntaxError(f"Unknown gate '{tok.value}' at {tok.line}:{tok.col}")
|
|
632
|
+
|
|
633
|
+
|
|
634
|
+
# ---------- Public API ----------
|
|
635
|
+
def validate_qasm_raise(src: str) -> None:
|
|
636
|
+
toks = _lex(src)
|
|
637
|
+
Parser(toks).parse()
|
|
638
|
+
|
|
639
|
+
|
|
640
|
+
def is_valid_qasm(src: str) -> bool:
|
|
641
|
+
try:
|
|
642
|
+
validate_qasm_raise(src)
|
|
643
|
+
return True
|
|
644
|
+
except SyntaxError:
|
|
645
|
+
return False
|