Skip to main content
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 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

Comparison table

DecoratorGate setAncilla managementMeasurementsBuild methodResult typeCertification
@coherentExact onlyCertified by b01tForbidden.build_exact()ExactProgramSAFE
@primitiveExact onlyCaller-managedForbidden.build_exact()ExactProgramPRIMITIVE
@parametricExact + rotationsCaller-managedForbidden.build()IRProgram
@adaptiveExact + rotationsCaller-managedAllowed.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()