qoro-divi 0.2.2b1__py3-none-any.whl → 0.3.1b0__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 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: Optional[int] = None, is_jupyter: bool = False
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, Optional
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: Optional[Literal["wires", "default", "qwc"]] = None,
70
- qem_protocol: Optional[QEMProtocol] = None,
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
@@ -5,3 +5,4 @@
5
5
  # TODO: delete whole module once Cirq properly supports parameters in openqasm 3.0
6
6
  from . import _qasm_export # Does nothing, just initiates the patch
7
7
  from ._qasm_import import cirq_circuit_from_qasm
8
+ from ._validator import is_valid_qasm
@@ -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