Not all quantum programs fit the exact gate set that @coherent requires. When you need arbitrary rotation angles — like rx(0.785, q) — use @parametric. When your program needs to measure qubits and branch on the results, use @adaptive. Both decorators produce an IRProgram via .build(), the broad-IR compilation path.
@parametric: coherent programs with rotations
@parametric marks a function as a coherent (unitary) program that may use rotation gates with arbitrary float angles. The gate set includes everything in @coherent plus rx, ry, rz, cry, and crz.
Compile it to an IRProgram with .build(), passing register specs for the QReg parameters. Classical parameters like theta and phi are regular Python values — you pass them at build time as additional arguments after the register specs.
@parametric and @coherent both produce unitary programs, but @coherent uses the exact gate set and provides a machine-checked safety certification. Use @parametric when you need rotation angles; use @coherent when you want the strongest correctness guarantees.
@adaptive: measurement and classical feedback
@adaptive marks a function as a program that may measure qubits and branch on classical results. Use measure(q) to measure a single qubit or measure_all(reg) to measure all qubits in a register. Use if_then() from b01t.kit to branch on a measurement result.
Build an @adaptive function the same way — with .build() and register specs:
@adaptive functions cannot be called from inside a @coherent or @parametric function. Adaptive programs break unitarity by branching on measurement outcomes. If you try it, DSLValidationError is raised: coherent functions cannot call adaptive functions.
Control flow
The b01t.kit module provides three control-flow helpers for use inside @parametric and @adaptive functions.
repeat(count, body)
Repeat a body lambda a fixed number of times at build time. Useful for Grover iterations, variational circuit layers, or any fixed-repetition pattern.
for_each(data, body)
Iterate over classical data at build time, emitting parameterized gates for each element. The body receives an index and the current data element.
if_then(cond, then_body, else_body)
Branch on a classical condition — typically a measurement result from an earlier measure() or measure_all() call. Only valid in @adaptive functions.
if_then() raises DSLValidationError if called inside a @coherent function. Classical branching is not allowed in unitary programs.
Complete example: Grover search
The Grover search demo combines @parametric and @adaptive. The oracle and diffusion are @parametric (they are unitary subroutines), and the outer search loop is @adaptive (it measures at the end).
Calling @parametric from @adaptive
@parametric functions are unitary subroutines, so they can be called freely from inside @adaptive functions. The Grover example above shows this pattern: grover_step is @parametric and is called inside grover_search which is @adaptive.
The repeat() helper works in both @parametric and @adaptive contexts.
When to use which decorator