Every b01t program starts with a decorator. The decorator you choose determines which gates are available, what safety guarantees the resulting program carries, and which build method you call to materialise the circuit. There are four decorators: @coherent and @primitive for the exact gate set, and @parametric and @adaptive for programs that require arbitrary-angle rotations or classical feedback.
Overview
@coherent
@primitive
@parametric
@adaptive
@coherent is the primary decorator for safe quantum programming. It enforces the exact gate set, requires all ancillae to be managed through the compute/phase/uncompute discipline, and produces an ExactProgram with Certification.SAFE. Every @coherent program that builds successfully is a proven unitary channel.from b01t import coherent, QReg, cx, z, Certification
from b01t.kit import ancilla, compute, phase, uncompute
@coherent
def phase_oracle(sys: QReg):
with ancilla(1) as anc:
compute(lambda: cx(sys[0], anc[0]))
phase(lambda: z(anc[0]))
uncompute()
prog = phase_oracle.build_exact(("sys", 1))
assert prog.certification == Certification.SAFE
Use @coherent when:
- You are writing a quantum subroutine intended for production use
- You need the Hero Theorem guarantee (proven unitary channel)
- Your circuit uses only gates from the exact gate set
- You want b01t to certify ancilla cleanliness for you
@primitive targets the same exact gate set as @coherent, and the resulting program is coherent (unitary), but scratch registers are caller-managed rather than certified by b01t. The program builds to Certification.PRIMITIVE.from b01t import primitive, QReg, h, cx, Certification
@primitive
def bell_pair(q: QReg):
h(q[0])
cx(q[0], q[1])
prog = bell_pair.build_exact(("q", 2))
assert prog.certification == Certification.PRIMITIVE
Use @primitive when:
- You are writing a low-level building block with no ancillae, or where you are managing scratch manually
- You understand the ancilla contract and are taking responsibility for it
- You want the exact gate set restriction without the ancilla-discipline overhead
@parametric allows arbitrary-angle rotation gates (rx, ry, rz, cry, crz) in addition to the exact gate set. It produces a DSLFunction that builds to an IRProgram via .build(). The resulting program uses Effect.COHERENT and does not carry a Certification value.from b01t import parametric, QReg, h, rz
import math
@parametric
def phase_shift(q: QReg, theta: float):
h(q[0])
rz(theta, q[0])
h(q[0])
prog = phase_shift.build(("q", 1))
Use @parametric when:
- Your circuit requires rotation gates with angles that are not multiples of π/4
- You are implementing variational algorithms (VQE, QAOA) or quantum machine learning
- You do not need the Hero Theorem guarantee
@adaptive extends @parametric with mid-circuit measurement and classical feedback via measure, measure_all, and if_then. It produces a DSLFunction that builds to an IRProgram via .build() with Effect.ADAPTIVE.from b01t import adaptive, QReg, h, cx, measure
from b01t.kit import if_then
@adaptive
def teleport(src: QReg, bell: QReg, dst: QReg):
h(bell[0])
cx(bell[0], bell[1])
cx(src[0], bell[0])
h(src[0])
m0 = measure(src[0])
m1 = measure(bell[0])
if_then(m1, lambda: cx(bell[1], dst[0]))
if_then(m0, lambda: h(dst[0]))
prog = teleport.build(("src", 1), ("bell", 2), ("dst", 1))
Use @adaptive when:
- Your circuit performs mid-circuit measurements
- You need classical feedback to condition later gates on measurement outcomes
- You are implementing quantum error correction or measurement-based protocols
Comparison table
| Decorator | Gate set | Ancilla management | Measurements | Build method | Result type | Certification |
|---|
@coherent | Exact only | Certified by b01t | Forbidden | .build_exact() | ExactProgram | SAFE |
@primitive | Exact only | Caller-managed | Forbidden | .build_exact() | ExactProgram | PRIMITIVE |
@parametric | Exact + rotations | Caller-managed | Forbidden | .build() | IRProgram | — |
@adaptive | Exact + rotations | Caller-managed | Allowed | .build() | IRProgram | — |
.build_exact() vs .build()
The build method you use depends on which decorator you used.
.build_exact() — for @coherent and @primitive
build_exact() is a method on ExactDSLFunction (the type returned by @coherent and @primitive). It takes one or more register specifications as (name, size) tuples, runs the validation pipeline, and returns an immutable ExactProgram.
from b01t import coherent, QReg, h, cx
@coherent
def my_circuit(a: QReg, b: QReg):
h(a[0])
cx(a[0], b[0])
# Pass one (name, size) tuple per register argument
prog = my_circuit.build_exact(("a", 1), ("b", 1))
print(prog.name) # "my_circuit"
print(prog.regs) # (QReg("a", 1), QReg("b", 1))
print(prog.certification) # Certification.SAFE
The order of (name, size) tuples must match the order of the function’s register parameters. b01t binds each QReg argument by position.
.build() — for @parametric and @adaptive
build() is a method on DSLFunction (returned by @parametric and @adaptive). It has the same call signature as build_exact() but returns an IRProgram instead.
from b01t import parametric, QReg, rx, rz
import math
@parametric
def variational_layer(q: QReg, theta: float, phi: float):
rx(theta, q[0])
rz(phi, q[0])
prog = variational_layer.build(("q", 1))
print(prog.effect) # Effect.COHERENT
print(prog.is_safe) # True (if ancilla discipline was followed)
Do not call a @coherent or @primitive function with .build() — those are ExactDSLFunction instances and only expose .build_exact(). Likewise, .build_exact() is not available on @parametric or @adaptive functions.
Calling decorated functions inside other decorated functions
You can call one decorated function from inside another, with some restrictions:
- A
@coherent function can call another @coherent function freely.
- A
@coherent function cannot call a @primitive function outside a compute() or phase() block. If you need to use a @primitive subroutine at the top level, declare the caller @primitive as well.
- A
@parametric or @adaptive function can call @coherent and @primitive functions (they are subsumed).
- A
@parametric function cannot call an @adaptive function — coherent functions cannot contain measurements.
from b01t import coherent, primitive, QReg, x, cx
from b01t.kit import ancilla, compute, phase, uncompute, adjoint
@primitive
def flip(q: QReg):
x(q[0])
@coherent
def use_flip(sys: QReg):
with ancilla(1) as anc:
# @primitive can be called inside compute/phase blocks
compute(lambda: flip(anc))
phase(lambda: None)
uncompute()