qcdiff-core 0.1.0

Semantic diff for OpenQASM 3 quantum circuits: parse, normalize, and diff operation streams (library behind the qcdiff CLI)
Documentation

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
--- 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

cargo install qcdiff

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 qcdiff_core::{diff, extract_source};

let old = extract_source(&std::fs::read_to_string("old.qasm")?, Some("old.qasm"))?;
let new = extract_source(&std::fs::read_to_string("new.qasm")?, Some("new.qasm"))?;
let result = diff(&old, &new);
println!("+{} -{} ~{}", result.added, result.removed, result.modified);

Usage

qcdiff old.qasm new.qasm            # full diff
qcdiff old.qasm new.qasm --quiet    # summary only (CI logs)
qcdiff old.qasm new.qasm --no-color

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/switch blocks 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 q to qr with identical structure reports every touching op as changed.
  • No line numbers. The semantic layer of openqasm3_parser 0.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

  1. Parse both files with oq3_semantics (syntax + semantic analysis; any parse or semantic error aborts with the parser's diagnostics).
  2. 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.
  3. 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