qcdiff
Semantic diff for OpenQASM 3 quantum circuits. qcdiff compares two QASM files at the gate level instead of the text level and prints a readable report of what changed: gates added, removed, or re-parameterized. Built for CI pipelines and code review of transpiler output.
$ qcdiff old.qasm new.qasm
qubits: 3 -> 3 | bits: 3 -> 3 | 2q-gates: 2 -> 2 | gate-defs: 0 -> 0
ops: 8 -> 8 | +0 -0 ~1
h q[0]
cx q[0],q[1]
cx q[1],q[2]
~ rz q[2] (0.5) => (0.75)
barrier q[0],q[1],q[2]
measure q[0] -> c[0]
measure q[1] -> c[1]
measure q[2] -> c[2]
Textual diff is useless for circuits: equivalent programs serialize with different whitespace, comments, and declaration order, and transpilers rewrite files wholesale. qcdiff parses both files with Qiskit's OpenQASM 3 parser, extracts a normalized operation stream, and aligns the two streams so that:
- comment, whitespace, and declaration-order changes do not count as differences;
- an inserted or removed gate shows as one
+/-line; - a parameter-only change shows as one
~line with old and new values; - summary counters (qubit/bit counts, two-qubit gate count, op totals) show the scale of the change before you read a single line.
What qcdiff is not
qcdiff tells you what changed, not whether the circuits are equivalent. Two circuits can differ textually and still implement the same unitary; for formal equivalence checking use mqt-qcec. The two tools answer different questions and work well together.
Install
Library
The CLI is a thin wrapper over the qcdiff-core crate, so the extraction and diff machinery can be embedded in other tools:
use ;
let old = extract_source?;
let new = extract_source?;
let result = diff;
println!;
Usage
Exit codes: 0 streams identical, 1 differences found, 2 parse or usage error. That makes it usable directly as a CI gate:
# GitHub Actions: fail when the transpiled circuit drifts from the checked-in
# reference (e.g. after a qiskit upgrade).
- name: Circuit regression check
run: qcdiff reference/ghz.qasm build/ghz.qasm --quiet
Limitations
- OpenQASM 3 only. OpenQASM 2 files are rejected with exit 2; convert first (
qiskit.qasm3.dumps(QuantumCircuit.from_qasm_file(...))). - Control flow is opaque.
if/for/while/switchblocks participate as single units keyed on the condition, with the body content-hashed: a body edit reports as one modified unit, not a nested diff. - Gate definitions (
gate my_gate ...) likewise diff as named units, not body-by-body. - No commutation awareness: reordering two commuting gates reports as a change.
- Register renames are changes. Renaming
qtoqrwith identical structure reports every touching op as changed. - No line numbers. The semantic layer of
openqasm3_parser0.7 does not carry source spans, so the diff shows stream order, not file lines. - Index expressions and a few exotic expression forms are compared by content hash rather than pretty-printed.
How it works
- Parse both files with
oq3_semantics(syntax + semantic analysis; any parse or semantic error aborts with the parser's diagnostics). - Lower the program to a linear stream of operations (gate calls with modifiers folded into the name, measures, resets, barriers, delays, and opaque control-flow units), each as
(kind, name, operands, params)with canonical, whitespace-free rendering of operands and parameter expressions. - Align the streams with a patience diff over
(kind, name, operands)keys, then compare parameters within aligned pairs: equal pairs are unchanged, differing pairs are reported as~modified.
License
Apache-2.0