qoro-divi 0.2.0b1__py3-none-any.whl → 0.6.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.
Files changed (92) hide show
  1. divi/__init__.py +1 -2
  2. divi/backends/__init__.py +10 -0
  3. divi/backends/_backend_properties_conversion.py +227 -0
  4. divi/backends/_circuit_runner.py +70 -0
  5. divi/backends/_execution_result.py +70 -0
  6. divi/backends/_parallel_simulator.py +486 -0
  7. divi/backends/_qoro_service.py +663 -0
  8. divi/backends/_qpu_system.py +101 -0
  9. divi/backends/_results_processing.py +133 -0
  10. divi/circuits/__init__.py +13 -0
  11. divi/{exp/cirq → circuits/_cirq}/__init__.py +1 -2
  12. divi/circuits/_cirq/_parser.py +110 -0
  13. divi/circuits/_cirq/_qasm_export.py +78 -0
  14. divi/circuits/_core.py +391 -0
  15. divi/{qasm.py → circuits/_qasm_conversion.py} +73 -14
  16. divi/circuits/_qasm_validation.py +694 -0
  17. divi/qprog/__init__.py +27 -8
  18. divi/qprog/_expectation.py +181 -0
  19. divi/qprog/_hamiltonians.py +281 -0
  20. divi/qprog/algorithms/__init__.py +16 -0
  21. divi/qprog/algorithms/_ansatze.py +368 -0
  22. divi/qprog/algorithms/_custom_vqa.py +263 -0
  23. divi/qprog/algorithms/_pce.py +262 -0
  24. divi/qprog/algorithms/_qaoa.py +579 -0
  25. divi/qprog/algorithms/_vqe.py +262 -0
  26. divi/qprog/batch.py +387 -74
  27. divi/qprog/checkpointing.py +556 -0
  28. divi/qprog/exceptions.py +9 -0
  29. divi/qprog/optimizers.py +1014 -43
  30. divi/qprog/quantum_program.py +243 -412
  31. divi/qprog/typing.py +62 -0
  32. divi/qprog/variational_quantum_algorithm.py +1208 -0
  33. divi/qprog/workflows/__init__.py +10 -0
  34. divi/qprog/{_graph_partitioning.py → workflows/_graph_partitioning.py} +139 -95
  35. divi/qprog/workflows/_qubo_partitioning.py +221 -0
  36. divi/qprog/workflows/_vqe_sweep.py +560 -0
  37. divi/reporting/__init__.py +7 -0
  38. divi/reporting/_pbar.py +127 -0
  39. divi/reporting/_qlogger.py +68 -0
  40. divi/reporting/_reporter.py +155 -0
  41. {qoro_divi-0.2.0b1.dist-info → qoro_divi-0.6.0.dist-info}/METADATA +43 -15
  42. qoro_divi-0.6.0.dist-info/RECORD +47 -0
  43. {qoro_divi-0.2.0b1.dist-info → qoro_divi-0.6.0.dist-info}/WHEEL +1 -1
  44. qoro_divi-0.6.0.dist-info/licenses/LICENSES/.license-header +3 -0
  45. divi/_pbar.py +0 -73
  46. divi/circuits.py +0 -139
  47. divi/exp/cirq/_lexer.py +0 -126
  48. divi/exp/cirq/_parser.py +0 -889
  49. divi/exp/cirq/_qasm_export.py +0 -37
  50. divi/exp/cirq/_qasm_import.py +0 -35
  51. divi/exp/cirq/exception.py +0 -21
  52. divi/exp/scipy/_cobyla.py +0 -342
  53. divi/exp/scipy/pyprima/LICENCE.txt +0 -28
  54. divi/exp/scipy/pyprima/__init__.py +0 -263
  55. divi/exp/scipy/pyprima/cobyla/__init__.py +0 -0
  56. divi/exp/scipy/pyprima/cobyla/cobyla.py +0 -599
  57. divi/exp/scipy/pyprima/cobyla/cobylb.py +0 -849
  58. divi/exp/scipy/pyprima/cobyla/geometry.py +0 -240
  59. divi/exp/scipy/pyprima/cobyla/initialize.py +0 -269
  60. divi/exp/scipy/pyprima/cobyla/trustregion.py +0 -540
  61. divi/exp/scipy/pyprima/cobyla/update.py +0 -331
  62. divi/exp/scipy/pyprima/common/__init__.py +0 -0
  63. divi/exp/scipy/pyprima/common/_bounds.py +0 -41
  64. divi/exp/scipy/pyprima/common/_linear_constraints.py +0 -46
  65. divi/exp/scipy/pyprima/common/_nonlinear_constraints.py +0 -64
  66. divi/exp/scipy/pyprima/common/_project.py +0 -224
  67. divi/exp/scipy/pyprima/common/checkbreak.py +0 -107
  68. divi/exp/scipy/pyprima/common/consts.py +0 -48
  69. divi/exp/scipy/pyprima/common/evaluate.py +0 -101
  70. divi/exp/scipy/pyprima/common/history.py +0 -39
  71. divi/exp/scipy/pyprima/common/infos.py +0 -30
  72. divi/exp/scipy/pyprima/common/linalg.py +0 -452
  73. divi/exp/scipy/pyprima/common/message.py +0 -336
  74. divi/exp/scipy/pyprima/common/powalg.py +0 -131
  75. divi/exp/scipy/pyprima/common/preproc.py +0 -393
  76. divi/exp/scipy/pyprima/common/present.py +0 -5
  77. divi/exp/scipy/pyprima/common/ratio.py +0 -56
  78. divi/exp/scipy/pyprima/common/redrho.py +0 -49
  79. divi/exp/scipy/pyprima/common/selectx.py +0 -346
  80. divi/interfaces.py +0 -25
  81. divi/parallel_simulator.py +0 -258
  82. divi/qlogger.py +0 -119
  83. divi/qoro_service.py +0 -343
  84. divi/qprog/_mlae.py +0 -182
  85. divi/qprog/_qaoa.py +0 -440
  86. divi/qprog/_vqe.py +0 -275
  87. divi/qprog/_vqe_sweep.py +0 -144
  88. divi/utils.py +0 -116
  89. qoro_divi-0.2.0b1.dist-info/RECORD +0 -58
  90. /divi/{qem.py → circuits/qem.py} +0 -0
  91. {qoro_divi-0.2.0b1.dist-info → qoro_divi-0.6.0.dist-info/licenses}/LICENSE +0 -0
  92. {qoro_divi-0.2.0b1.dist-info → qoro_divi-0.6.0.dist-info/licenses}/LICENSES/Apache-2.0.txt +0 -0
@@ -0,0 +1,694 @@
1
+ # SPDX-FileCopyrightText: 2025 Qoro Quantum Ltd <divi@qoroquantum.de>
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ """QASM validation parser.
6
+
7
+ This module provides a lightweight, standalone QASM parser specifically designed
8
+ for validation purposes. While the codebase also uses Cirq's QASM parser (see
9
+ `divi.circuits._cirq._parser`) for actual circuit parsing and execution, this
10
+ validation parser serves distinct purposes:
11
+
12
+ 1. **Performance**: This parser is optimized for fast validation without building
13
+ full circuit representations. It performs syntax and semantic checks (symbol
14
+ resolution, gate signatures, register bounds, etc.) without the overhead of
15
+ constructing Cirq circuit objects.
16
+
17
+ 2. **Dependency independence**: This parser has no external dependencies beyond
18
+ the standard library, making it suitable for validation checks in contexts
19
+ where Cirq may not be available or desired.
20
+
21
+ 3. **Focused error reporting**: The parser is designed to provide clear, precise
22
+ error messages for validation failures, including line and column numbers,
23
+ which is useful for user-facing validation (e.g., before submitting circuits
24
+ to a backend service).
25
+
26
+ 4. **Use cases**: This parser is used for:
27
+ - Pre-submission validation of QASM strings (e.g., in `QoroService.submit_circuits`)
28
+ - Quick validity checks without full parsing (`is_valid_qasm`)
29
+ - Counting qubits from QASM without parsing to circuits (`validate_qasm_count_qubits`)
30
+
31
+ The Cirq parser (`_parser.py`) remains the primary parser for converting QASM
32
+ to executable circuit representations, while this module handles validation-only
33
+ workloads efficiently.
34
+ """
35
+
36
+ import re
37
+ from typing import NamedTuple
38
+
39
+ # ---------- Lexer ----------
40
+ _WS_RE = re.compile(r"\s+")
41
+ _LINE_COMMENT_RE = re.compile(r"//[^\n]*")
42
+ _BLOCK_COMMENT_RE = re.compile(r"/\*.*?\*/", re.DOTALL)
43
+
44
+ TOKEN_SPECS = [
45
+ ("ARROW", r"->"),
46
+ ("EQ", r"=="),
47
+ ("LBRACE", r"\{"),
48
+ ("RBRACE", r"\}"),
49
+ ("LPAREN", r"\("),
50
+ ("RPAREN", r"\)"),
51
+ ("LBRACKET", r"\["),
52
+ ("RBRACKET", r"\]"),
53
+ ("COMMA", r","),
54
+ ("SEMI", r";"),
55
+ ("STAR", r"\*"),
56
+ ("SLASH", r"/"),
57
+ ("PLUS", r"\+"),
58
+ ("MINUS", r"-"),
59
+ ("CARET", r"\^"),
60
+ ("STRING", r"\"[^\"\n]*\""),
61
+ ("NUMBER", r"\d+(?:\.\d+)?"),
62
+ ("ID", r"[A-Za-z_][A-Za-z0-9_]*"),
63
+ ]
64
+ TOKEN_REGEX = re.compile("|".join(f"(?P<{n}>{p})" for n, p in TOKEN_SPECS))
65
+
66
+ KEYWORDS = {
67
+ "OPENQASM",
68
+ "include",
69
+ "qreg",
70
+ "creg",
71
+ "qubit",
72
+ "bit",
73
+ "gate",
74
+ "barrier",
75
+ "measure",
76
+ "reset",
77
+ "if",
78
+ "pi",
79
+ "sin",
80
+ "cos",
81
+ "tan",
82
+ "exp",
83
+ "ln",
84
+ "sqrt",
85
+ "acos",
86
+ "atan",
87
+ "asin",
88
+ }
89
+
90
+
91
+ class Tok(NamedTuple):
92
+ type: str
93
+ value: str
94
+ pos: int
95
+ line: int
96
+ col: int
97
+
98
+
99
+ def _strip_comments(src: str) -> str:
100
+ return _LINE_COMMENT_RE.sub("", _BLOCK_COMMENT_RE.sub("", src))
101
+
102
+
103
+ def _lex(src: str) -> list[Tok]:
104
+ src = _strip_comments(src)
105
+ i, n = 0, len(src)
106
+ line, line_start = 1, 0
107
+ out: list[Tok] = []
108
+ while i < n:
109
+ m = _WS_RE.match(src, i)
110
+ if m:
111
+ chunk = src[i : m.end()]
112
+ nl = chunk.count("\n")
113
+ if nl:
114
+ line += nl
115
+ line_start = m.end() - (len(chunk) - chunk.rfind("\n") - 1)
116
+ i = m.end()
117
+ continue
118
+ m = TOKEN_REGEX.match(src, i)
119
+ if not m:
120
+ snippet = src[i : i + 20].replace("\n", "\\n")
121
+ raise SyntaxError(
122
+ f"Illegal character at {line}:{i-line_start+1}: {snippet!r}"
123
+ )
124
+ kind = m.lastgroup
125
+ val = m.group(kind)
126
+ col = i - line_start + 1
127
+ if kind == "ID" and val in KEYWORDS:
128
+ kind = val.upper()
129
+ out.append(Tok(kind, val, i, line, col))
130
+ i = m.end()
131
+ out.append(Tok("EOF", "", i, line, i - line_start + 1))
132
+ return out
133
+
134
+
135
+ # ---------- Built-in gates (name -> (num_params, num_qubits)) ----------
136
+ BUILTINS: dict[str, tuple[int, int]] = {
137
+ # 1q
138
+ "id": (0, 1),
139
+ "x": (0, 1),
140
+ "y": (0, 1),
141
+ "z": (0, 1),
142
+ "h": (0, 1),
143
+ "s": (0, 1),
144
+ "sdg": (0, 1),
145
+ "t": (0, 1),
146
+ "tdg": (0, 1),
147
+ "sx": (0, 1),
148
+ "sxdg": (0, 1),
149
+ "rx": (1, 1),
150
+ "ry": (1, 1),
151
+ "rz": (1, 1),
152
+ "u1": (1, 1),
153
+ "u2": (2, 1),
154
+ "u3": (3, 1),
155
+ "u": (3, 1), # allow 'u' alias
156
+ "U": (3, 1),
157
+ # 2q
158
+ "cx": (0, 2),
159
+ "cy": (0, 2),
160
+ "cz": (0, 2),
161
+ "iswap": (0, 2),
162
+ "swap": (0, 2),
163
+ "rxx": (1, 2),
164
+ "ryy": (1, 2),
165
+ "rzz": (1, 2),
166
+ "crx": (1, 2),
167
+ "cry": (1, 2),
168
+ "crz": (1, 2),
169
+ "cu1": (1, 2),
170
+ "cu3": (3, 2),
171
+ "ch": (0, 2),
172
+ # 3q
173
+ "ccx": (0, 3),
174
+ "cswap": (0, 3),
175
+ }
176
+
177
+ _MATH_FUNCS = {"SIN", "COS", "TAN", "EXP", "LN", "SQRT", "ACOS", "ATAN", "ASIN"}
178
+
179
+
180
+ # ---------- Parser with symbol checks ----------
181
+ class Parser:
182
+ def __init__(self, toks: list[Tok]):
183
+ self.toks = toks
184
+ self.i = 0
185
+ # symbols
186
+ self.qregs: dict[str, int] = {}
187
+ self.cregs: dict[str, int] = {}
188
+ self.user_gates: dict[str, tuple[tuple[str, ...], tuple[str, ...]]] = {}
189
+ # gate-def scope
190
+ self.in_gate_def = False
191
+ self.g_params: set[str] = set()
192
+ self.g_qubits: set[str] = set()
193
+
194
+ # -- helpers --
195
+ def peek(self, k=0) -> Tok:
196
+ j = self.i + k
197
+ return self.toks[j] if j < len(self.toks) else self.toks[-1]
198
+
199
+ def match(self, *types: str) -> Tok:
200
+ t = self.peek()
201
+ if t.type in types:
202
+ self.i += 1
203
+ return t
204
+ exp = " or ".join(types)
205
+ raise SyntaxError(f"Expected {exp} at {t.line}:{t.col}, got {t.type}")
206
+
207
+ def accept(self, *types: str) -> Tok | None:
208
+ if self.peek().type in types:
209
+ return self.match(*types)
210
+ return None
211
+
212
+ # -- entry --
213
+ def parse(self):
214
+ self.header()
215
+ while self.accept("INCLUDE"):
216
+ self.include_stmt()
217
+ while self.peek().type != "EOF":
218
+ start_line = self.peek().line
219
+ self.statement()
220
+ # After statement, check if it ended correctly
221
+ prev_tok = self.toks[self.i - 1] if self.i > 0 else None
222
+ # A statement is valid if it ends in a semicolon OR a closing brace (for gates)
223
+ if not prev_tok or (prev_tok.type != "SEMI" and prev_tok.type != "RBRACE"):
224
+ raise SyntaxError(
225
+ f"Statement at line {start_line} must end with a semicolon or a closing brace."
226
+ )
227
+ self.match("EOF")
228
+
229
+ # OPENQASM 2.0 ;
230
+ def header(self):
231
+ self.match("OPENQASM")
232
+ v = self.match("NUMBER")
233
+ if v.value not in ("2.0", "2", "3.0", "3"):
234
+ raise SyntaxError(
235
+ f"Unsupported OPENQASM version '{v.value}' at {v.line}:{v.col}"
236
+ )
237
+ self.match("SEMI")
238
+
239
+ def include_stmt(self):
240
+ self.match("STRING")
241
+ self.match("SEMI")
242
+
243
+ def statement(self):
244
+ t = self.peek().type
245
+ if t == "QREG":
246
+ self.qreg_decl()
247
+ elif t == "CREG":
248
+ self.creg_decl()
249
+ elif t == "QUBIT":
250
+ self.qubit_decl()
251
+ elif t == "BIT":
252
+ self.bit_decl()
253
+ elif t == "GATE":
254
+ self.gate_def()
255
+ elif t == "MEASURE":
256
+ self.measure_stmt()
257
+ elif t == "RESET":
258
+ self.reset_stmt()
259
+ elif t == "BARRIER":
260
+ self.barrier_stmt()
261
+ elif t == "IF":
262
+ self.if_stmt()
263
+ elif t == "ID":
264
+ self.gate_op_stmt_top()
265
+ else:
266
+ tok = self.peek()
267
+ raise SyntaxError(f"Unexpected token {tok.type} at {tok.line}:{tok.col}")
268
+
269
+ # ---- declarations ----
270
+ def qreg_decl(self):
271
+ self.match("QREG")
272
+ name = self.match("ID").value
273
+ self.match("LBRACKET")
274
+ n = self.natural_number_tok()
275
+ self.match("RBRACKET")
276
+ self.match("SEMI")
277
+ if name in self.qregs or name in self.cregs:
278
+ self._dupe(name)
279
+ self.qregs[name] = n
280
+
281
+ def creg_decl(self):
282
+ self.match("CREG")
283
+ name = self.match("ID").value
284
+ self.match("LBRACKET")
285
+ n = self.natural_number_tok()
286
+ self.match("RBRACKET")
287
+ self.match("SEMI")
288
+ if name in self.qregs or name in self.cregs:
289
+ self._dupe(name)
290
+ self.cregs[name] = n
291
+
292
+ def qubit_decl(self):
293
+ self.match("QUBIT")
294
+ if self.accept("LBRACKET"):
295
+ n = self.natural_number_tok()
296
+ self.match("RBRACKET")
297
+ name = self.match("ID").value
298
+ else:
299
+ name = self.match("ID").value
300
+ n = 1
301
+ self.match("SEMI")
302
+ if name in self.qregs or name in self.cregs:
303
+ self._dupe(name)
304
+ self.qregs[name] = n
305
+
306
+ def bit_decl(self):
307
+ self.match("BIT")
308
+ if self.accept("LBRACKET"):
309
+ n = self.natural_number_tok()
310
+ self.match("RBRACKET")
311
+ name = self.match("ID").value
312
+ else:
313
+ name = self.match("ID").value
314
+ n = 1
315
+ self.match("SEMI")
316
+ if name in self.qregs or name in self.cregs:
317
+ self._dupe(name)
318
+ self.cregs[name] = n
319
+
320
+ # ---- gate definitions ----
321
+ def gate_def(self):
322
+ self.match("GATE")
323
+ gname_tok = self.match("ID")
324
+ gname = gname_tok.value
325
+ if gname in BUILTINS:
326
+ raise SyntaxError(
327
+ f"Cannot redefine built-in gate '{gname}' at {gname_tok.line}:{gname_tok.col}"
328
+ )
329
+ if gname in self.user_gates:
330
+ self._dupe(gname)
331
+ params: tuple[str, ...] = ()
332
+ if self.accept("LPAREN"):
333
+ params = self._id_list_tuple()
334
+ self.match("RPAREN")
335
+ qubits = self._id_list_tuple()
336
+ # enter scope
337
+ saved = (self.in_gate_def, self.g_params.copy(), self.g_qubits.copy())
338
+ self.in_gate_def = True
339
+ self.g_params = set(params)
340
+ self.g_qubits = set(qubits)
341
+ self.match("LBRACE")
342
+ # body: only gate ops; they can use local qubit ids and local params in expr
343
+ while self.peek().type == "ID":
344
+ self.gate_op_stmt_in_body()
345
+ self.match("RBRACE")
346
+ # leave scope
347
+ self.in_gate_def, self.g_params, self.g_qubits = saved
348
+ self.user_gates[gname] = (params, qubits)
349
+
350
+ def _id_list_tuple(self) -> tuple[str, ...]:
351
+ ids = [self.match("ID").value]
352
+ while self.accept("COMMA"):
353
+ ids.append(self.match("ID").value)
354
+ return tuple(ids)
355
+
356
+ # ---- gate operations ----
357
+ def gate_op_stmt_top(self):
358
+ name_tok = self.match("ID")
359
+ gname = name_tok.value
360
+ param_count = None
361
+ arity = None
362
+
363
+ if self.accept("LPAREN"):
364
+ n_params = self._expr_list_count(allow_id=False) # top-level: no free IDs
365
+ self.match("RPAREN")
366
+ else:
367
+ n_params = 0
368
+
369
+ # resolve gate signature
370
+ if gname in BUILTINS:
371
+ param_count, arity = BUILTINS[gname]
372
+ elif gname in self.user_gates:
373
+ param_count, arity = len(self.user_gates[gname][0]), len(
374
+ self.user_gates[gname][1]
375
+ )
376
+ else:
377
+ self._unknown_gate(name_tok)
378
+
379
+ if n_params != param_count:
380
+ raise SyntaxError(
381
+ f"Gate '{gname}' expects {param_count} params, got {n_params} at {name_tok.line}:{name_tok.col}"
382
+ )
383
+
384
+ args, reg_sizes = self.qarg_list_top(arity)
385
+ # broadcast check: all full-register sizes must match if >1
386
+ sizes = {s for s in reg_sizes if s > 1}
387
+ if len(sizes) > 1:
388
+ raise SyntaxError(
389
+ f"Mismatched register sizes in arguments to '{gname}' at {name_tok.line}:{name_tok.col}"
390
+ )
391
+
392
+ self.match("SEMI")
393
+
394
+ def gate_op_stmt_in_body(self):
395
+ name_tok = self.match("ID")
396
+ gname = name_tok.value
397
+
398
+ if self.accept("LPAREN"):
399
+ n_params = self._expr_list_count(allow_id=True) # may use local params
400
+ self.match("RPAREN")
401
+ else:
402
+ n_params = 0
403
+
404
+ if gname in BUILTINS:
405
+ param_count, arity = BUILTINS[gname]
406
+ elif gname in self.user_gates:
407
+ param_count, arity = len(self.user_gates[gname][0]), len(
408
+ self.user_gates[gname][1]
409
+ )
410
+ else:
411
+ self._unknown_gate(name_tok)
412
+
413
+ if n_params != param_count:
414
+ raise SyntaxError(
415
+ f"Gate '{gname}' expects {param_count} params, got {n_params} at {name_tok.line}:{name_tok.col}"
416
+ )
417
+
418
+ # In gate bodies, qargs must be local gate-qubit identifiers (no indexing)
419
+ qids = [self._gate_body_qid()]
420
+ while self.accept("COMMA"):
421
+ qids.append(self._gate_body_qid())
422
+ if len(qids) != arity:
423
+ raise SyntaxError(
424
+ f"Gate '{gname}' expects {arity} qubit args in body, got {len(qids)} at {name_tok.line}:{name_tok.col}"
425
+ )
426
+ # Check for duplicate qubit arguments
427
+ if len(set(qids)) != len(qids):
428
+ raise SyntaxError(
429
+ f"Duplicate qubit arguments for gate '{gname}' at {name_tok.line}:{name_tok.col}"
430
+ )
431
+ self.match("SEMI")
432
+
433
+ def _gate_body_qid(self) -> str:
434
+ if self.peek().type != "ID":
435
+ t = self.peek()
436
+ raise SyntaxError(f"Expected gate-qubit id at {t.line}:{t.col}")
437
+ name = self.match("ID").value
438
+ if name not in self.g_qubits:
439
+ t = self.peek(-1)
440
+ raise SyntaxError(
441
+ f"Unknown gate-qubit '{name}' in gate body at {t.line}:{t.col}"
442
+ )
443
+ return name
444
+
445
+ # qarg list at top-level: IDs may be full registers or indexed bits q[i]
446
+ def qarg_list_top(
447
+ self, expected_arity: int
448
+ ) -> tuple[list[tuple[str, int | None]], list[int]]:
449
+ args = [self.qarg_top()]
450
+ while self.accept("COMMA"):
451
+ args.append(self.qarg_top())
452
+ if len(args) != expected_arity:
453
+ t = self.peek()
454
+ raise SyntaxError(
455
+ f"Expected {expected_arity} qubit args, got {len(args)} at {t.line}:{t.col}"
456
+ )
457
+ # return sizes for broadcast check
458
+ reg_sizes = [(self.qregs[name] if idx is None else 1) for (name, idx) in args]
459
+ return args, reg_sizes
460
+
461
+ def qarg_top(self) -> tuple[str, int | None]:
462
+ name_tok = self.match("ID")
463
+ name = name_tok.value
464
+ if name not in self.qregs:
465
+ raise SyntaxError(
466
+ f"Unknown qreg '{name}' at {name_tok.line}:{name_tok.col}"
467
+ )
468
+ if self.accept("LBRACKET"):
469
+ idx_tok = self.natural_number_tok_tok()
470
+ self.match("RBRACKET")
471
+ if int(idx_tok.value) >= self.qregs[name]:
472
+ raise SyntaxError(
473
+ f"Qubit index {idx_tok.value} out of range for '{name}[{self.qregs[name]}]' at {idx_tok.line}:{idx_tok.col}"
474
+ )
475
+ return (name, int(idx_tok.value))
476
+ return (name, None) # full register
477
+
478
+ # ---- measure/reset/barrier/if ----
479
+ def measure_stmt(self):
480
+ # two forms: measure qarg -> carg ; | carg = measure qarg ;
481
+ if self.peek().type == "MEASURE":
482
+ self.match("MEASURE")
483
+ q_t, q_sz = self._measure_qarg()
484
+ self.match("ARROW")
485
+ c_t, c_sz = self._measure_carg()
486
+ else:
487
+ # handled only when starts with MEASURE in statement(), so unreachable
488
+ raise SyntaxError("Internal: measure_stmt misuse")
489
+ if q_sz != c_sz:
490
+ t = self.peek()
491
+ raise SyntaxError(
492
+ f"Measurement size mismatch {q_sz} -> {c_sz} at {t.line}:{t.col}"
493
+ )
494
+ self.match("SEMI")
495
+
496
+ def _measure_qarg(self) -> tuple[str, int]:
497
+ name_tok = self.match("ID")
498
+ name = name_tok.value
499
+ if name not in self.qregs:
500
+ raise SyntaxError(
501
+ f"Unknown qreg '{name}' at {name_tok.line}:{name_tok.col}"
502
+ )
503
+ if self.accept("LBRACKET"):
504
+ idx = self.natural_number_tok()
505
+ self.match("RBRACKET")
506
+ if idx >= self.qregs[name]:
507
+ raise SyntaxError(f"Qubit index {idx} out of range for '{name}'")
508
+ return (f"{name}[{idx}]", 1)
509
+ return (name, self.qregs[name])
510
+
511
+ def _measure_carg(self) -> tuple[str, int]:
512
+ name_tok = self.match("ID")
513
+ name = name_tok.value
514
+ if name not in self.cregs:
515
+ raise SyntaxError(
516
+ f"Unknown creg '{name}' at {name_tok.line}:{name_tok.col}"
517
+ )
518
+ if self.accept("LBRACKET"):
519
+ idx = self.natural_number_tok()
520
+ self.match("RBRACKET")
521
+ if idx >= self.cregs[name]:
522
+ raise SyntaxError(f"Bit index {idx} out of range for '{name}'")
523
+ return (f"{name}[{idx}]", 1)
524
+ return (name, self.cregs[name])
525
+
526
+ def reset_stmt(self):
527
+ self.match("RESET")
528
+ # allow full reg or single index
529
+ name_tok = self.match("ID")
530
+ name = name_tok.value
531
+ if name not in self.qregs:
532
+ raise SyntaxError(
533
+ f"Unknown qreg '{name}' at {name_tok.line}:{name_tok.col}"
534
+ )
535
+ if self.accept("LBRACKET"):
536
+ idx = self.natural_number_tok()
537
+ self.match("RBRACKET")
538
+ if idx >= self.qregs[name]:
539
+ raise SyntaxError(f"Qubit index {idx} out of range for '{name}'")
540
+ self.match("SEMI")
541
+
542
+ def barrier_stmt(self):
543
+ self.match("BARrier".upper()) # tolerate case in tokenization
544
+ # barrier accepts one or more qargs (full regs and/or indices)
545
+ self.qarg_top()
546
+ while self.accept("COMMA"):
547
+ self.qarg_top()
548
+ self.match("SEMI")
549
+
550
+ def if_stmt(self):
551
+ self.match("IF")
552
+ self.match("LPAREN")
553
+ cname_tok = self.match("ID")
554
+ cname = cname_tok.value
555
+ if cname not in self.cregs:
556
+ raise SyntaxError(
557
+ f"Unknown creg '{cname}' at {cname_tok.line}:{cname_tok.col}"
558
+ )
559
+ self.match("EQ")
560
+ val_tok = self.natural_number_tok_tok()
561
+ self.match("RPAREN")
562
+ if int(val_tok.value) >= (1 << self.cregs[cname]):
563
+ raise SyntaxError(
564
+ f"if() value {val_tok.value} exceeds creg width {self.cregs[cname]}"
565
+ )
566
+ # must be a single gate op
567
+ self.gate_op_stmt_top()
568
+
569
+ # ---- expressions (with symbol policy) ----
570
+ def _expr_list_count(self, *, allow_id: bool) -> int:
571
+ # count expressions in list; expressions may reference IDs only if allow_id
572
+ count = 0
573
+ self._expr(allow_id)
574
+ count += 1
575
+ while self.accept("COMMA"):
576
+ self._expr(allow_id)
577
+ count += 1
578
+ return count
579
+
580
+ def _expr(self, allow_id: bool):
581
+ self._expr_addsub(allow_id)
582
+
583
+ def _expr_addsub(self, allow_id: bool):
584
+ self._expr_muldiv(allow_id)
585
+ while self.peek().type in ("PLUS", "MINUS"):
586
+ self.match(self.peek().type)
587
+ self._expr_muldiv(allow_id)
588
+
589
+ def _expr_muldiv(self, allow_id: bool):
590
+ self._expr_power(allow_id)
591
+ while self.peek().type in ("STAR", "SLASH"):
592
+ self.match(self.peek().type)
593
+ self._expr_power(allow_id)
594
+
595
+ def _expr_power(self, allow_id: bool):
596
+ self._expr_unary(allow_id)
597
+ if self.peek().type == "CARET":
598
+ self.match("CARET")
599
+ self._expr_power(allow_id)
600
+
601
+ def _expr_unary(self, allow_id: bool):
602
+ while self.peek().type in ("PLUS", "MINUS"):
603
+ self.match(self.peek().type)
604
+ self._expr_atom(allow_id)
605
+
606
+ def _expr_atom(self, allow_id: bool):
607
+ t = self.peek()
608
+ if t.type == "NUMBER":
609
+ self.match("NUMBER")
610
+ return
611
+ if t.type == "PI":
612
+ self.match("PI")
613
+ return
614
+ if t.type in _MATH_FUNCS:
615
+ self.match(t.type) # Consume the function name (e.g., COS)
616
+ self.match("LPAREN")
617
+ self._expr(allow_id) # Parse the inner expression
618
+ # Note: QASM 2.0 math functions only take one argument
619
+ self.match("RPAREN")
620
+ return
621
+ if t.type == "ID":
622
+ # function call or plain ID
623
+ id_tok = self.match("ID")
624
+ ident = id_tok.value
625
+ if self.accept("LPAREN"):
626
+ if self.peek().type != "RPAREN":
627
+ self._expr(allow_id)
628
+ while self.accept("COMMA"):
629
+ self._expr(allow_id)
630
+ self.match("RPAREN")
631
+ return
632
+ # bare identifier: only allowed if in gate body params and allow_id=True
633
+ if not allow_id or ident not in self.g_params:
634
+ raise SyntaxError(
635
+ f"Unknown symbol '{ident}' in expression at {id_tok.line}:{id_tok.col}"
636
+ )
637
+ return
638
+ if t.type == "LPAREN":
639
+ self.match("LPAREN")
640
+ self._expr(allow_id)
641
+ self.match("RPAREN")
642
+ return
643
+ raise SyntaxError(
644
+ f"Unexpected token {t.type} in expression at {t.line}:{t.col}"
645
+ )
646
+
647
+ # ---- numbers / errors ----
648
+ def natural_number_tok(self) -> int:
649
+ t = self.match("NUMBER")
650
+ if "." in t.value:
651
+ raise SyntaxError(
652
+ f"Expected natural number at {t.line}:{t.col}, got {t.value}"
653
+ )
654
+ return int(t.value)
655
+
656
+ def natural_number_tok_tok(self) -> Tok:
657
+ t = self.match("NUMBER")
658
+ if "." in t.value:
659
+ raise SyntaxError(
660
+ f"Expected natural number at {t.line}:{t.col}, got {t.value}"
661
+ )
662
+ return t
663
+
664
+ def _dupe(self, name: str):
665
+ t = self.peek()
666
+ raise SyntaxError(f"Redefinition of '{name}' at {t.line}:{t.col}")
667
+
668
+ def _unknown_gate(self, tok: Tok):
669
+ raise SyntaxError(f"Unknown gate '{tok.value}' at {tok.line}:{tok.col}")
670
+
671
+
672
+ # ---------- Public API ----------
673
+ def validate_qasm(src: str) -> None:
674
+ """Validate QASM syntax, raising SyntaxError on error."""
675
+ toks = _lex(src)
676
+ Parser(toks).parse()
677
+
678
+
679
+ def validate_qasm_count_qubits(src: str) -> int:
680
+ """Validate QASM and return the total number of qubits, raising SyntaxError on error."""
681
+ toks = _lex(src)
682
+ parser = Parser(toks)
683
+ parser.parse()
684
+ # Sum all qubit register sizes to get total qubit count
685
+ return sum(parser.qregs.values())
686
+
687
+
688
+ def is_valid_qasm(src: str) -> bool:
689
+ """Check if QASM is valid, returning True/False without raising exceptions."""
690
+ try:
691
+ validate_qasm(src)
692
+ return True
693
+ except SyntaxError:
694
+ return False