Quantum teleportation
Reference: the Quon constructs in this recipe are defined normatively — syntax, typing contract, constraints, and a minimal example — in the Language reference.
What we’re building and why
Section titled “What we’re building and why”Quantum teleportation is the protocol that transfers an unknown quantum state from one party to another using only a shared Bell pair and two classical bits. It is not science-fiction transport — it is the foundational primitive for quantum networking, distributed quantum computing, and any architecture that needs to move quantum information without physically moving qubits. In a compiler, it is also the canonical example of dynamic circuits: circuits where a measurement outcome classically conditions later gates.
The protocol has three phases. First, Alice and Bob share a Bell pair — one
qubit each, entangled. Second, Alice performs a Bell-basis measurement on her
message qubit and her half of the pair, obtaining two classical bits. Third,
Alice sends those bits to Bob, who applies a correction (Pauli X, Pauli Z,
both, or neither) to his qubit — which now holds the original message state.
What makes this non-trivial for a compiler is the feed-forward: the correction
depends on classical measurement results that are only known at runtime. The
circuit cannot be a single static sequence — it must branch. Quon models this
with if bit then circuit else circuit inside a run block, which lowers to
the quantum.dynamic dialect’s cond_apply operation. The linear type system
tracks which qubits have been measured (and are therefore classical) versus
which are still quantum and available for further gates.
This program is deliberately self-contained: main prepares the message qubit
in |1⟩, teleports it to Bob, and measures Bob. The expected result is 1 on
every shot, deterministically — which means the feed-forward corrections are
working, because without them Bob’s measurement would be a fair coin.
Typed annotations
Section titled “Typed annotations”The circuit library is entirely Clifford — the whole protocol is Clifford, which is why it is so robust and efficiently simulable on a stabilizer tableau. Each helper circuit carries a type that the compiler verifies before lowering:
fn prep(): Circuit<3, 3, 3, Clifford> = circuit { X @0 |> H @1 |> CNOT @(1, 2) }fn bell_basis(): Circuit<2, 2, 2, Clifford> = circuit { CNOT @(0, 1) |> H @0 }fn pauli_x(): Circuit<1, 1, 1, Clifford> = circuit { X @0 }fn pauli_z(): Circuit<1, 1, 1, Clifford> = circuit { Z @0 }fn id_one(): Circuit<1, 1, 1, Clifford> = circuit { I @0 }The types tell a story:
prepisCircuit<3, 3, 3, Clifford>— three qubits in, three out, depth 3. The depth comes fromX @0(depth 1)|>H @1(depth 1)|>CNOT @(1,2)(depth 1), all sequential. NoteX @0andH @1act on different qubits, so they could be parallel — but|>is sequential composition, so the depth is the sum, not the max. (Parallel composition viaparwould give depth 2, but that is not what this program uses.)bell_basisisCircuit<2, 2, 2, Clifford>— the self-inverse adjoint of the Bell preparation. It is written out explicitly rather than usingadjoint(...)so the lowering needs no adjoint synthesis. Because it is its own inverse, applying it twice is the identity.- Correction circuits are
Circuit<1, 1, 1, Clifford>— each acts on a single qubit with depth 1. Theif ... then ... elseselection picks one at runtime, but the typechecker proves all branches have the same type.id_oneis the identity circuit, the “do nothing” branch.
Source
Section titled “Source”This is the executable
test/verify/teleport.qn
fixture. The broader typechecker example is
frontend/tests/fixtures/teleport.qn.
The program begins with five helper circuit definitions. The prep circuit
allocates all three qubits at once and sets up the initial state: X on qubit 0
flips the message to |1⟩, while H and CNOT entangle the
Alice–Bob pair (qubits 1 and 2) into a Bell state. The bell_basis circuit is
the self-inverse adjoint of Bell preparation — written out explicitly so the
lowering needs no adjoint synthesis. The remaining three circuits are the
Pauli corrections and the identity, each a single-qubit Clifford circuit that
the if-branch will select at runtime.
fn prep(): Circuit<3, 3, 3, Clifford> = circuit { X @0 |> H @1 |> CNOT @(1, 2) }fn bell_basis(): Circuit<2, 2, 2, Clifford> = circuit { CNOT @(0, 1) |> H @0 }fn pauli_x(): Circuit<1, 1, 1, Clifford> = circuit { X @0 }fn pauli_z(): Circuit<1, 1, 1, Clifford> = circuit { Z @0 }fn id_one(): Circuit<1, 1, 1, Clifford> = circuit { I @0 }The run block is where the protocol unfolds. First, prep is applied to a
fresh 3-qubit register, binding the three qubits to msg, alice, and bob.
Then bell_basis measures Alice’s two qubits in the Bell basis — this is the
joint measurement that produces the two classical correction bits. The
measure calls consume m2 and a2, producing classical bits x_bit and
z_bit. The if expressions then select a correction circuit based on those
bits and apply it to bob, threading the linear qubit through each branch.
Because these are user-defined Clifford circuits selected by if, the lowering
produces feed-forward cond_apply operations: the classical bits condition which
circuit is applied. Finally, Bob’s corrected qubit is measured and returned —
and since the message was |1⟩, the result should be 1 on every shot.
fn main(): Q<Bit> = run { (msg, alice, bob) <- prep() @ qreg(3) (m2, a2) <- bell_basis() @ (msg, alice) x_bit <- measure(m2) z_bit <- measure(a2) b2 <- (if z_bit then pauli_x() else id_one()) @ bob b3 <- (if x_bit then pauli_z() else id_one()) @ b2 result <- measure(b3) return result}Compile and simulate
Section titled “Compile and simulate”./target/release/quonc test/verify/teleport.qn --emit-qasm > /tmp/teleport.qasmQUONC=target/release/quonc python test/verify/teleport.pyThe checked-in
teleport.py
verifier also runs
teleport_plus.qn,
so both the X and Z correction paths are tested.
--dump-ir MLIR excerpts
Section titled “--dump-ir MLIR excerpts”The feed-forward if expressions lower to the quantum.dynamic dialect:
// Schematic: the classical bits condition circuit application.%x_bit = quantum.dynamic.measure %m2 : i1%z_bit = quantum.dynamic.measure %a2 : i1
// cond_apply: applies the selected circuit to bob's qubit.%b2 = quantum.dynamic.cond_apply %z_bit, @pauli_x, @id_one, %bob%b3 = quantum.dynamic.cond_apply %x_bit, @pauli_z, @id_one, %b2The cond_apply operation takes a classical condition bit, two circuit values
(the then-branch and else-branch), and a qubit to apply the result to. After
lowering, measurement_deferral and classical_region_fusion passes run on the
dynamic IR to optimize classical control flow. gate_cancellation on the circ
portion (prep and bell_basis) finds nothing to cancel — the sequence is already
minimal. The identity circuit id_one in the else-branch compiles to a no-op
that the optimizer can erase, leaving only the active correction.
What the compiler proves
Section titled “What the compiler proves”- All three qubits consumed exactly once. The linear context tracks
msg,alice, andbobthrough every bind. Afterbell_basisconsumesmsgandalice(via measurement), onlybobremains quantum. The corrections consumeboband produce a new linear value (b2, thenb3), which is then measured. No qubit leaks or is double-used. - Correction circuits are Clifford. Both
pauli_xandpauli_zhave typeCircuit<1, 1, 1, Clifford>. The typechecker proves every branch of theifhas the same type, so the conditional circuit application is type-safe regardless of the runtime measurement outcome. - Borrow-free design. Unlike some teleportation implementations that use
borrowblocks for ancilla, this program allocates all qubits up front viaqreg(3)in therunblock. No ancilla escape is possible — the linear type system ensures the register is fully consumed. - Classical bits are unrestricted.
x_bitandz_bitareBitvalues, not linear quantum values. They can be inspected inifconditions, returned in tuples, and duplicated freely — the linear constraint applies only to quantum values (Qubit,QReg).
Expected result
Section titled “Expected result”The shown program prepares |1> and should recover 1 on Bob’s qubit with
more than 99% fidelity. The companion case prepares |+>, rotates back to the
Z basis, and should recover 0 above the same threshold. Together they show
that both classical feed-forward corrections preserve the teleported state.
Dynamic circuits on the neutral-atom path
Section titled “Dynamic circuits on the neutral-atom path”This page’s fixed/QASM path measures then classically conditions a
correction. For a genuine “measure mid-circuit, then keep scheduling more
entangling layers” shape on the neutral-atom schedule model — where the NA
schedule interleaves an ancilla’s measure/reset round between two rounds
of entangling layers, rather than only measuring at the very end — see
examples/na_qec/repetition_d3_memory.qn
and its walkthrough in
samples/neutral-atom/README.md.
Feed-forward correction lowering (branching a later gate on the
mid-circuit outcome) is still limited on the NA path, so that sample’s
schedule measures and resets every round but never conditions a subsequent
gate on the outcome — it does not reproduce this page’s X/Z
corrections.
What to try next
Section titled “What to try next”- Teleport a superposition. Change
prepto prepare|+>(useH @0instead ofX @0) and verify the output is still|+>after correction. - Add a third correction. The protocol has four outcomes (00, 01, 10, 11)
corresponding to no correction, X, Z, or XZ. The current code nests two
ifexpressions to handle all four. Try unrolling it into amatchor four-way branch if the language supports it. - Measure before correction. Remove the
ifand just measurebobdirectly — the result will be random, demonstrating that the corrections are essential.
→ Next: Bernstein–Vazirani — recover a hidden bit string with a single oracle query.