QASM Parser¶
QASMParser turns an OpenQASM 2.0 or 3.0 string into a QASMCircuit — the object every
other page's circuit.to_tuples() call feeds straight into DenseSVSimulator.run_circuit.
This page covers what QASM the parser actually understands.
Step 1. Parse your first circuit¶
import dense_evolution as de
qasm = 'OPENQASM 2.0; include "qelib1.inc"; qreg q[2]; h q[0]; cx q[0],q[1];'
circuit = de.QASMParser().parse(qasm)
circuit.n_qubits, circuit.to_tuples()
parse reads the register declaration (qreg q[2]) and every gate statement, and
returns a QASMCircuit — n_qubits is read straight from the register, to_tuples()
is the tuple list every simulator page on this site starts from.
Step 2. Registers, and bare register names¶
qasm = 'OPENQASM 2.0; include "qelib1.inc"; qreg q[3]; h q; x q[2];'
circuit = de.QASMParser().parse(qasm)
circuit.to_tuples()
q[2] is qubit 2, exactly as written. q with no index — legal OpenQASM, meaning "the
whole register" in some contexts — resolves here to qubit 0 of that register, not every
qubit in it; write out q[0], q[1], q[2] explicitly if that's what you mean.
Step 3. Parametric gates: real math, not just numbers¶
qasm = 'OPENQASM 2.0; include "qelib1.inc"; qreg q[1]; rx(pi/2) q[0]; ry(sqrt(2)) q[0];'
circuit = de.QASMParser().parse(qasm)
[(op['name'], round(op['params'][0], 4)) for op in circuit.ops]
A gate parameter isn't limited to a literal number — pi, tau, euler, and functions
like sqrt, sin, cos, exp, log all evaluate exactly as written, including
combined expressions like pi/2. circuit.ops (the pre-tuple form to_tuples() builds
from) is where the evaluated float ends up, one per gate.
Step 4. Range syntax¶
qasm = 'OPENQASM 2.0; include "qelib1.inc"; qreg q[4]; h q[0:3];'
circuit = de.QASMParser().parse(qasm)
circuit.to_tuples()
q[0:3] means qubits 0 through 2 — exclusive of the upper bound, the same convention
Python's own slicing uses. Applying a single-qubit gate like h to a range expands into
one gate application per qubit, not one gate call carrying three qubits at once.
Step 5. Gate name aliases¶
qasm = 'OPENQASM 2.0; include "qelib1.inc"; qreg q[2]; cnot q[0],q[1]; toffoli q[0],q[1],q[0];'
circuit = de.QASMParser().parse(qasm)
[op['name'] for op in circuit.ops]
cnot and toffoli aren't gate names this library's simulator knows about — they're
common alternate names some QASM sources use for cx and ccx. The parser normalizes
these (and cu1→cp, u1→p, fredkin→cswap) before a gate name ever reaches
GATES/PARAMETRIC_GATES, so both spellings run identically.
Step 6. OpenQASM 3.0¶
qasm = 'OPENQASM 3.0; qubit[3] q; bit[3] c; for int i in [0:2] { h q[i]; }'
circuit = de.QASMParser().parse(qasm)
circuit.to_tuples()
qubit[3] q and bit[3] c are QASM 3.0's register syntax — qreg/creg's replacement.
for int i in [0:2] { ... } is unrolled before parsing proper begins, substituting 0,
1, 2 into the loop body in turn — QASM 3.0's for range is inclusive of its upper
bound, unlike Step 4's q[a:b] qubit-range syntax, which is exclusive.
Step 7. Validate before running¶
from dense_evolution.circuits.parser import QASMCircuit
bad = QASMCircuit(n_qubits=2, n_cbits=0,
ops=[{'type': 'gate', 'name': 'h', 'qubits': [5], 'params': []}])
de.QASMParser().validate(bad)
validate is a light structural check — no qreg/gate-count mismatch, no qubit index
outside [0, n_qubits) — meant for a QASMCircuit you built or edited by hand rather
than one straight out of parse (a genuinely parsed circuit's n_qubits already
accounts for every qubit index its own gates reference). It does not check gate
semantics — a circuit using a gate name the simulator doesn't recognize still passes.
Step 8. Skip to_tuples() — a QASMCircuit is already iterable¶
qasm = 'OPENQASM 2.0; include "qelib1.inc"; qreg q[2]; h q[0]; cx q[0],q[1];'
circuit = de.QASMParser().parse(qasm)
list(circuit)
Every earlier step called circuit.to_tuples() explicitly, but QASMCircuit also
implements __iter__ over that same tuple list — passing circuit itself anywhere a
plain tuple list is expected (DenseSVSimulator.run_circuit, QuantumTranspiler.transpile,
Chunk.run_chunk) works without the extra call.
Details¶
The parameter evaluator is a safe AST whitelist, not eval()¶
Gate parameters like Step 3's sqrt(2) used to be evaluated with
eval(tok, {'__builtins__': {}}) — this does not stop attribute/dunder traversal of
the live object graph: ().__class__.__bases__[0].__subclasses__() needs no builtin
name at all, and from there any class already loaded in the process is reachable. Passed
as a gate parameter through the public parse() entry point, that expression executed
successfully before this fix. The evaluator now walks the parameter's AST against an
explicit whitelist (numeric literals, +-*/%**, unary +/-, and name/call lookups
restricted to a fixed math environment) — an ast.Attribute node is never one of the
handled cases, so any expression containing . always falls through to rejection,
structurally rather than by pattern-matching dangerous names.
A malformed parameter expression raises, not silently becomes 0.0¶
An unparseable or disallowed parameter expression (a typo, or the injection attempt
above) raises ValueError immediately. It used to fall back to 0.0 silently — turning
a typo into a different, valid-looking circuit (rx(0) instead of the intended angle)
with no signal anything was wrong, the same class of silent-wrong-behavior this
codebase's gate-name and parameter-batch validation elsewhere were also fixed to avoid.
Comments and unresolved control flow are stripped, not executed¶
/* block */ and // line comments are removed before parsing. if/while/def
blocks, and any for loop whose bounds aren't a literal or a previously-declared
int/const int variable (Step 6 needs resolvable bounds to unroll), are stripped
entirely rather than run — there's no runtime classical-bit state to execute them
against. A gate NAME(...) { ... } definition (common in Qiskit's OpenQASM 2.0 export
for composite gates) is stripped the same way; a later call to that gate name still
falls through as an unrecognized gate, same as any other unknown name.
to_tuples()'s field order, and why QASMCircuit is iterable at all¶
to_tuples() returns (name, *qubits, *params) — qubits always immediately after the
name, params trailing. __iter__ (Step 8) exists because QuantumTranspiler.transpile,
reached via Chunk.run_chunk(circuit), iterates its circuit argument directly — handed
a QASMCircuit straight from parse() instead of circuit.to_tuples(), that used to
raise TypeError: 'QASMCircuit' object is not iterable.
parser ¶
QASMCircuit
dataclass
¶
Parsed representation of an OpenQASM 2.0 / 3.0 circuit.
Attributes:
| Name | Type | Description |
|---|---|---|
n_qubits |
total qubit count declared in qreg / qubit statements
|
|
n_cbits |
total classical bit count declared in creg / bit statements
|
|
ops |
list of gate dicts — each dict has keys:
|
'type' : 'gate' 'name' : lowercase gate name (aliases resolved) 'qubits' : list[int] — absolute qubit indices 'params' : list[float] — evaluated rotation angles |
to_tuples ¶
Convert ops to the tuple format expected by DenseSVSimulator.run_circuit:
(name, qubit0, [qubit1, ...], [param0, ...])
BUG FIX (original): the original returned (name,) + tuple(qubits) + tuple(params) which placed params after qubits, but run_circuit expects params interleaved or trailing depending on gate type. For the standard (name, qubit, param) convention used throughout the simulator, this ordering is correct — preserved here but documented explicitly so callers know what to expect.
Source code in dense_evolution/circuits/parser.py
__iter__ ¶
Duck-type as an iterable of the same tuples to_tuples() returns,
so a QASMCircuit works anywhere a plain circuit list is expected
(QuantumTranspiler.transpile, Chunk.run_chunk, ...) without the
caller having to remember to call .to_tuples() first. Verified this
was a real gap, not a hypothetical one: for cmd in circuit inside
QuantumTranspiler.transpile — reached via Chunk.run_chunk(circuit)
— raised TypeError: 'QASMCircuit' object is not iterable when
handed a QASMCircuit straight from QASMParser().parse(), instead of
circuit.to_tuples().
Source code in dense_evolution/circuits/parser.py
QASMParser ¶
Robust OpenQASM 2.0 / 3.0 parser.
Supported features
- qreg / creg (QASM 2.0)
- qubit / bit (QASM 3.0)
- Parametric gates: rx, ry, rz, p, u1, u2, u3, cp, crz, ...
- Compound parameter expressions: pi/2, sqrt(2), cos(0.3), ...
- Block comments / ... / and line comments // ...
- Gate aliases: cu1→cp, u1→p, toffoli→ccx, cnot→cx, ...
- Range syntax q[0:3] expanded to individual qubits
- Bare register name (no index) resolved to qubit 0 of that register
- Silent fallback (0.0) for unparseable parameter expressions
parse ¶
Parse an OpenQASM 2.0 or 3.0 string into a QASMCircuit.
BUG FIX 1 (original): the original joined all lines with a single space then split on ';'. Multi-line gate definitions (gate foo ...) were not stripped before joining, causing 'gate foo ...' to appear as a runnable instruction. Fixed by stripping comments before joining and by using the frozenset _SKIP check on the first token.
BUG FIX 2 (original): bare register names (e.g. 'h q' instead of 'h q[0]') were silently dropped if the register had more than one qubit, because qubit_map only stored 'name[0]' → 0 for size-1 registers. Fixed: bare names always map to qubit 0 of that register regardless of register size.
BUG FIX 3 (original): range syntax q[0:3] was never handled — such tokens fell through to the digit-extraction fallback which returned only the last digit. Fixed in _resolve_qubits.
Source code in dense_evolution/circuits/parser.py
306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 | |
validate ¶
Light structural validation — does not verify gate semantics.
Source code in dense_evolution/circuits/parser.py
See Also¶
DenseSVSimulator—run_circuitis what everyto_tuples()output on this page is built to feed.- Gates — the
GATES/PARAMETRIC_GATEStables a parsed gate name resolves against once it reaches the simulator. QuantumTranspiler— runs on aQASMCircuit's tuples before the simulator ever sees them, decomposing anything it can't execute directly.