This profile defines a subset of the QIR specification to support a coherent set of functionalities and capabilities that might be offered by a quantum backend. Like all profile specifications, this document is primarily intended for compiler backend authors as well as contributors to the targeting stage of the QIR compiler.
The Adaptive Profile specifies supersets of the Base Profile that enable control flow based on mid-circuit measurements and classical computations while quantum resources remain coherent. A backend can support this profile by supporting a minimum set of features beyond the Base Profile and can opt in for features beyond that.
To support the Adaptive Profile without any of its optional features, a backend must support the following mandatory capabilities:
- It can execute a sequence of quantum instructions that transform the quantum state.
- A backend must support applying a measurement operation at any point in the execution of the program. Qubits not undergoing the measurement should not have their state affected.
- A backend must be able to apply quantum instructions conditionally on a
measurement outcome. Specifically, forward branching using the LLVM branch
instruction
br
must be supported, along with the necessary runtime function to convert a measurement to ani1
value and the LLVM instructions for computations oni1
values defined in detail below. - It must produce one of the specified output schemas.
This means that at minimum, backends supporting Adaptive Profile programs should support mid-circuit measurement, turning measurements into booleans, and branching based on those booleans. The QIR Adaptive Profile in particular also requires that branches can be arbitrarily nested. This includes a requirement that it must be possible to perform a measurement within a branch, and then branch again on that measurement. However, it is not a requirement to support loops within the control flow graph.
Beyond the providing the required capabilities above, a backend can opt into one or more of the following optional capabilities to support more advanced adaptive computations:
- Computations on classical (non-composite) data types, specifically on integers or floating-point numbers.
- IR-defined functions and calls of these functions at any point in the program.
- Support for control flow loops (backwards branching).
- Multiple target branching.
- Multiple return points.
The use of these optional features is represented as a module flag in the program IR. Any backend that supports capabilities 1-4, and as many of capabilities 5-9 as it desires, is considered as supporting Adaptive Profile programs. Static analysis/verification tools should be able to determine what capabilities of the Adaptive Profile a backend is implementing and should run a verification pass to ensure Adaptive Profile programs that are using capabilities not supported by a backend are rejected with an informative message. More details about each of the aforementioned capabilities are outlined in the following sections.
The set of available instructions that transform the quantum state may vary depending on the targeted backend. The profile specification defines how to leverage and combine the available instructions to express a program, but does not dictate which quantum instructions may be used. Targeting a program to a specific backend requires choosing a suitable profile and quantum instruction set (QIS). Both can be chosen largely independently, though certain instruction sets may be incompatible with this (or other) profile(s). The section on the quantum instruction set defines the requirements for a QIS to be compatible with the Adaptive Profile. More information about the role of the QIS, recommendations for front- and backend providers, as well as the distinction between runtime functions and quantum instructions, can be found in this document.
As for the Base Profile, a measurement function is a QIS function marked with an
irreversible
attribute that
populates a value of type %Result
. The available measurement functions are
defined by the executing backend as part of the QIS. Unlike the Base Profile,
the Adaptive Profile relieves a restriction that qubits can only be measured at
the end of the program and that no quantum operations can be performed after or
conditionally on a measurement outcome.
Within the Adaptive Profile, there are no restrictions on when these measurements are performed during program execution. Correspondingly, it must be possible to measure individual qubits, or subsets of qubits depending on the supported QIS, without impacting the state of the non-measured qubits. Furthermore, it must be possible to use the measured qubit(s) afterwards and apply additional quantum instructions to the same qubit(s).
Additionally, the Adaptive Profile requires that it must be possible to take
action based on a measurement result. Specifically, it must be possible to
execute subsequent quantum instructions conditionally on the produced %Result
value. To that end, a runtime function must be provided that converts a
%Result
value into a value of type i1
; see the section on Runtime
Functions for further clarification. Additionally, the LLVM
branch instruction br
must be supported; see the section on Classical
Instructions for further clarification.
The Adaptive Profile allows for arbitrary forward branching based on i1
values. While arbitrary nesting of branches must be supported, an Adaptive
Profile program must ensure that the program terminates. Unless the profile
makes use of the optional capability of backward branching (Bullet 7), the
control flow structure of a program is hence a tree without any cycles. A static
analysis to determine the validity of the program should check for cycles in the
control flow graph and reject any program with a cycle as invalid unless support
for backward branching is enabled.
While the Adaptive Profile requires that it must be possible to use a qubit
after it was measured, the availability of a reset
instruction depends on the
QIS supported by the backend. If a single-qubit measurement is supported as part
of the QIS, it is always possible to reset that qubit, if needed for qubit reuse
later in the program, using forward branching as illustrated for example by the
following IR snippet:
...
tail call void @__quantum__qis__mz__body(%Qubit* null, %Result* writeonly null)
%0 = tail call i1 @__quantum__rt__read_result(%Result* readonly null)
br i1 %0, label %then, label %continue
then: ; preds = ...
tail call void @__quantum__qis__x__body(%Qubit* null)
br label %continue
continue:
...
Although forward branching can be useful when combined with purely classical operations within a quantum program, the real utility is being able to conditionally perform quantum instructions depending on measurement outcomes, for example when performing real-time error-correction as part of a quantum programs.
The specifications of QIR and all its profiles need to accurately reflect the program intent. This includes being able to define and customize the program output. The Base Profile and Adaptive Profile specifications hence require explicitly expressing which values/measurements are returned by the program and in which order. How to express this is defined in the section on output recording.
The defined output schemas provide different options for how a backend may express the computed value(s). The exact schema can be freely chosen by the backend and is identified by a string label in the produced schema. Each output schema contains sufficient information to allow quantum programming frameworks to generate a user-friendly presentation of the returned values in the requested order, such as, e.g., a histogram of all results when running the program multiple times.
While the general output recording mechanism and the output schemas are the same
for all profiles, the Adaptive Profile makes it possible to output classical
values, if - and only if - computations on classical data types are supported
(see the section on output recording for more details). If
computations on classical data types are supported, the program may furthermore
produce a non-zero exit codes indicating a runtime failure (e.g. for division by
zero). Injecting suitable logic to produce a non-zero exit code in the case of
classical computations that lead to an incorrect program output is generally up
to the compiler, and the backend is not required to detect runtime failures.
Unless the backend supports the optional capability of having multiple return
statements in a program, it is up to the compiler to make use of phi
-nodes to
propagate any error code to the single final return statement at the end of the
entry-point function.
A backend can choose to support an extended Adaptive Profile that may include instructions for classical computations on atomic data types, including integer and floating-point arithmetics. The behavior in the case of an overflow or underflow may be undefined. All LLVM instructions that must be available to support an extended Adaptive Profile including classical computations are listed in the section on classical instructions.
Which data types are used in classical computations as part of an extended Adaptive Profile must be indicated in the form of module flags. The module flag(s) specifically also include information about the bitwidth(s) of the used data type(s). It is the responsibility of the backend to reject the program if any of the used data types or widths thereof are not supported.
Support for a classical data type implies that local variables of that data type may exist at any point in the program. Specifically, it is then also permitted to pass such variables as arguments to QIS functions, runtime functions, or IR-defined functions (Bullet 6) if available, as illustrated in the examples section. Passing constant values of any data type as arguments to QIS and runtime functions is always permitted, regardless of whether classical computations on that data type are supported.
In addition to local variables, global constants may be defined for any of the supported classical data types. Such global constants may be used anywhere in the program where a value of this type can be used.
A backend can choose to support an extended Adaptive Profile that includes IR-defined functions, that is functions whose implementation is defined as part of the program IR. An Adaptive Profile program that includes IR-defined functions must indicate this in the form of a module flag.
IR-defined functions may take arguments of type %Qubit*
and %Result*
, and it
must have void
return
type. If classical computations are
supported in addition to IR-defined functions, then values of the supported data
type(s) may also be passed as arguments to, and returned from, an IR-defined
function. Since the adaptive profile does not include support for composite data
types, such as tuples and arrays, they cannot be passed to or returned from
IR-defined functions.
The body of an IR-defined function may use any of the available classical
instructions. It may call QIS functions and other
IR-defined functions. However, any form of direct or indirect recursion is
forbidden. In contrast to the entry point function,
an IR-defined function may not contain any calls to output
recording or initialization functions, but it may call other
runtime functions. Just like for the entry point function, values of type
%Qubit*
and %Result*
may only occur in calls to other functions; qubit
values of these types that are passed as arguments cannot be assigned to local
variables, that is they cannot be aliased.
Opting into this capability relieves the restriction on backwards branching so that a more compact representation of loops can be expressed in programs. Any use of this optional capability must be indicated in the form of module flags in the program IR.
The Adaptive Profile distinguishes two kinds of loops; iterations over sequences of known length, and conditionally terminating loops.
Iterations are primarily useful for a more compact representation of the
code, since code size may be a limiting factor. Since the Adaptive Profile does
not allow for any composite data types, supporting iterations necessarily
requires supporting the use of phi
-nodes to identify a qubit or result value.
For example, the following IR expresses a loop that flips the state of qubit 1
to 4, if qubit 0 is in a non-zero state:
...
define void @simple_loop() {
entry:
br label %loop_body
loop_body: ; preds = %loop_body, %entry
%0 = phi i64 [ 1, %entry ], [ %1, %loop_body ]
call void @__quantum__qis__cnot__body(%Qubit* null, %Qubit* nonnull inttoptr (i64 %0 to %Qubit*))
%1 = add i64 %0, 1
%2 = icmp sle i64 %1, 4
br i1 %2, label %loop_body, label %loop_exit
loop_exit: ; preds = %loop_body
...
}
...
In the case of an iteration, the value of a phi-node used to identify a qubit or result value must never depend on any quantum measurements. The same requirement exists for the condition for exiting the loop. This means that iterations can always be unrolled at compile time, and upon unrolling, all values used to identify a qubit or result value can be replaced by a constant. A static analysis tool can accurately enforce both of these requirements.
Conditionally terminating loops may be used if indicated by the appropriate module flag. These loops may break or continue depending on values that depend on a quantum measurement, as illustrated by the following code snippet.
...
br label %loop
loop:
call void @__quantum__qis__h__body(%Qubit* null)
call void @__quantum__qis__mz__body(%Qubit* null, %Result* writeonly null)
%0 = call i1 @__quantum__rt__read_result(%Result* readonly null)
br i1 %0, label %cont, label %loop
cont:
...
In contrast to iterations, they cannot be eliminated at compile time without assuming a hard limit for the number of iterations after which the execution is terminated with an error. If a backend supports the use of conditionally terminating loops, it is up to backend to force the termination of programs that would otherwise run indefinitely.
It can be desirable to support control flow constructs that indicate how a
computation can lead to branching to one of many different execution paths.
Having such a construct exposed in the IR allows for more aggressive
optimizations across different blocks without any dependencies between them. A
backend may hence opt into supporting switch
instructions to facilitate such
optimizations. An Adaptive Profile program that includes switch
instructions
must indicate this in the form of a module flag.
For this capability to be practically useful, integer computations must be
supported as well (Bullet 5). For example, the snippet below causes a jump
to a block onzero
, onone
, ontwo
, or otherwise
respectively, depending on
the value of an integer variable %val
:
switch i32 %val, label %otherwise [ i32 0, label %onzero
i32 1, label %onone
i32 2, label %ontwo ]
The variable %val
may, for example, depend on measurement results or a global
constant. We refer to the LLVM language
reference for more
information about the switch instruction.
A backend my choose to support multiple return points in an entry point
function, and IR-defined functions if the optional capability in Bullet 6 is
supported. This eliminates the need to create phi
nodes for the purpose of
propagating the computed output to a single final block. Any use of this
optional capability must be indicated in the form of module
flags in the program IR.
A return statement is necessarily always the last statement in a block. For each block that contains returns a zero exit code in the entry point function, that same block must also contain the necessary calls to output recording functions to ensure the correct program output is recorded. If the block returns a non-zero exit code, calls to these functions may be omitted, implying that no output will be recorded in this case.
For example, an Adaptive Profile program that uses this optional capability may contain a logic like this:
@0 = internal constant [2 x i8] c"0\00"
define i64 @main() local_unnamed_addr #0 {
entry:
tail call void @__quantum__qis__mz__body(%Qubit* null, %Result* writeonly null)
%0 = tail call i1 @__quantum__rt__read_result(%Result* readonly null)
br i1 %0, label %error, label %exit
error:
; qubits should be in a zero state at the end of the program
ret i64 1
exit:
call void @__quantum__rt__result_record_output(%Result* null, i8* getelementptr inbounds ([2 x i8], [2 x i8]* @0, i32 0, i32 0))
ret i64 0
}
An Adaptive Profile-compliant program is defined in the form of a single LLVM bitcode file that contains the following:
- the definitions of the opaque
Qubit
andResult
types - global constants that store string labels needed for certain output schemas that may be ignored if the output schema does not make use of them
- optionally, and only if classical computations (Bullet 5) are supported, global constants of the supported classical data types
- the entry point definition that contains the program logic
- optionally, and only if IR-defined functions (Bullet 6) are supported, additional functions that are called from the entry point function
- declarations of the QIS functions used by the program
- declarations of runtime functions used for initialization and output recording
- one or more attribute groups used to store information about the entry point, and optionally additional information about other function declarations
- module flags that contain information that a compiler or backend may need to process the bitcode, including module flags that indicate which features of the Adaptive Profile are used
The human-readable LLVM IR for the bitcode can be obtained using standard LLVM tools. For clarity, this specification contains examples of the human-readable IR emitted by LLVM 13. While the bitcode representation is portable and usually backward compatible, there may be visual differences in the human-readable format depending on the LLVM version. These differences are irrelevant when using standard tools to load, manipulate, and/or execute bitcode.
The code below illustrates how a simple program implementing a teleport chain looks within a minimal Adaptive Profile representation:
; type definitions
%Result = type opaque
%Qubit = type opaque
; global constants (labels for output recording)
@0 = internal constant [5 x i8] c"0_t0\00"
@1 = internal constant [5 x i8] c"0_t1\00"
; entry point definition
define i64 @TeleportChain() local_unnamed_addr #0 {
entry:
; calls to initialize the execution environment
call void @__quantum__rt__initialize(i8* null)
br label %body
body: ; preds = %entry
tail call void @__quantum__qis__h__body(%Qubit* null)
tail call void @__quantum__qis__cnot__body(%Qubit* null, %Qubit* nonnull inttoptr (i64 1 to %Qubit*))
tail call void @__quantum__qis__h__body(%Qubit* nonnull inttoptr (i64 2 to %Qubit*))
tail call void @__quantum__qis__cnot__body(%Qubit* nonnull inttoptr (i64 2 to %Qubit*), %Qubit* nonnull inttoptr (i64 4 to %Qubit*))
tail call void @__quantum__qis__h__body(%Qubit* nonnull inttoptr (i64 3 to %Qubit*))
tail call void @__quantum__qis__cnot__body(%Qubit* nonnull inttoptr (i64 3 to %Qubit*), %Qubit* nonnull inttoptr (i64 5 to %Qubit*))
tail call void @__quantum__qis__cnot__body(%Qubit* nonnull inttoptr (i64 1 to %Qubit*), %Qubit* nonnull inttoptr (i64 2 to %Qubit*))
tail call void @__quantum__qis__h__body(%Qubit* nonnull inttoptr (i64 1 to %Qubit*))
tail call void @__quantum__qis__mz__body(%Qubit* nonnull inttoptr (i64 1 to %Qubit*), %Result* writeonly null)
tail call void @__quantum__qis__reset__body(%Qubit* nonnull inttoptr (i64 1 to %Qubit*))
%0 = tail call i1 @__quantum__rt__read_result(%Result* readonly null)
br i1 %0, label %then__1, label %continue__1
; conditional quantum gate (only one in this block, but many can appear and the full quantum instruction set should be usable)
then__1: ; preds = %body
tail call void @__quantum__qis__z__body(%Qubit* nonnull inttoptr (i64 4 to %Qubit*))
br label %continue__1
continue__1: ; preds = %then__1, %body
tail call void @__quantum__qis__mz__body(%Qubit* nonnull inttoptr (i64 2 to %Qubit*), %Result* writeonly nonnull inttoptr (i64 1 to %Result*))
tail call void @__quantum__qis__reset__body(%Qubit* nonnull inttoptr (i64 2 to %Qubit*))
%1 = tail call i1 @__quantum__rt__read_result(%Result* readonly nonnull inttoptr (i64 1 to %Result*))
br i1 %1, label %then__2, label %continue__2
then__2: ; preds = %continue__1
tail call void @__quantum__qis__x__body(%Qubit* nonnull inttoptr (i64 4 to %Qubit*))
br label %continue__2
continue__2: ; preds = %then__2, %continue__1
tail call void @__quantum__qis__cnot__body(%Qubit* nonnull inttoptr (i64 4 to %Qubit*), %Qubit* nonnull inttoptr (i64 3 to %Qubit*))
tail call void @__quantum__qis__h__body(%Qubit* nonnull inttoptr (i64 4 to %Qubit*))
tail call void @__quantum__qis__mz__body(%Qubit* nonnull inttoptr (i64 4 to %Qubit*), %Result* writeonly nonnull inttoptr (i64 2 to %Result*))
tail call void @__quantum__qis__reset__body(%Qubit* nonnull inttoptr (i64 4 to %Qubit*))
%2 = tail call i1 @__quantum__rt__read_result(%Result* readonly nonnull inttoptr (i64 2 to %Result*))
br i1 %2, label %then__3, label %continue__3
then__3: ; preds = %continue__2
tail call void @__quantum__qis__z__body(%Qubit* nonnull inttoptr (i64 5 to %Qubit*))
br label %continue__3
continue__3: ; preds = %then__3, %continue__2
tail call void @__quantum__qis__mz__body(%Qubit* nonnull inttoptr (i64 3 to %Qubit*), %Result* writeonly nonnull inttoptr (i64 3 to %Result*))
tail call void @__quantum__qis__reset__body(%Qubit* nonnull inttoptr (i64 3 to %Qubit*))
%3 = tail call i1 @__quantum__rt__read_result(%Result* readonly nonnull inttoptr (i64 3 to %Result*))
br i1 %3, label %then__4, label %continue__4
then__4: ; preds = %continue__3
tail call void @__quantum__qis__x__body(%Qubit* nonnull inttoptr (i64 5 to %Qubit*))
br label %continue__4
continue__4: ; preds = %continue__3, %then__4
tail call void @__quantum__qis__mz__body(%Qubit* null, %Result* writeonly nonnull inttoptr (i64 4 to %Result*))
tail call void @__quantum__qis__reset__body(%Qubit* null)
tail call void @__quantum__qis__mz__body(%Qubit* nonnull inttoptr (i64 5 to %Qubit*), %Result* writeonly nonnull inttoptr (i64 5 to %Result*))
tail call void @__quantum__qis__reset__body(%Qubit* nonnull inttoptr (i64 5 to %Qubit*))
br label %exit
exit:
call void @__quantum__rt__result_record_output(%Result* nonnull inttoptr (i64 4 to %Result*), i8* getelementptr inbounds ([5 x i8], [5 x i8]* @0, i32 0, i32 0))
call void @__quantum__rt__result_record_output(%Result* nonnull inttoptr (i64 5 to %Result*), i8* getelementptr inbounds ([5 x i8], [5 x i8]* @1, i32 0, i32 0))
ret i64 0
}
; declarations of QIS functions
declare void @__quantum__qis__cnot__body(%Qubit*, %Qubit*) local_unnamed_addr
declare void @__quantum__qis__h__body(%Qubit*) local_unnamed_addr
declare void @__quantum__qis__x__body(%Qubit*) local_unnamed_addr
declare void @__quantum__qis__z__body(%Qubit*) local_unnamed_addr
declare void @__quantum__qis__reset__body(%Qubit*) local_unnamed_addr
declare void @__quantum__qis__mz__body(%Qubit*, %Result* writeonly) #1
; declarations of runtime functions
declare void @__quantum__rt__initialize(i8*)
declare i1 @__quantum__rt__read_result(%Result* readonly)
declare void @__quantum__rt__result_record_output(%Result*, i8*)
; attributes
attributes #0 = { "entry_point" "qir_profiles"="adaptive_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="6" "required_num_results"="6" }
attributes #1 = { "irreversible" }
; module flags
!llvm.module.flags = !{!0, !1, !2, !3, !4, !5, !6, !7, !8, !9}
!0 = !{i32 1, !"qir_major_version", i32 1}
!1 = !{i32 7, !"qir_minor_version", i32 0}
!2 = !{i32 1, !"dynamic_qubit_management", i1 false}
!3 = !{i32 1, !"dynamic_result_management", i1 false}
!4 = !{i32 5, !"int_computations", !""}
!5 = !{i32 5, !"float_computations", !""}
!6 = !{i32 1, !"ir_functions", i1 false}
!7 = !{i32 1, !"backwards_branching", i2 0}
!8 = !{i32 1, !"multiple_target_branching", i1 false}
!9 = !{i32 1, !"multiple_return_points", i1 false}
The program performs gate teleportation involving mid-circuit measurements and conditionally applied quantum instructions.
For the sake of clarity, the code above does not contain any debug symbols. Debug symbols contain information that is used by a debugger to relate failures during execution back to the original source code. While we expect this to be primarily useful for execution on simulators, debug symbols are both easy to ignore and may be useful to generate helpful error messages for compilation failures that happen only late in the process. We hence see no reason to disallow them from occurring in the bitcode but will not detail their use any further. We defer to existing resources for more information about how to generate and use debug symbols.
The bitcode contains the definition of the LLVM function that should be invoked when the program is executed, referred to as "entry point" in the rest of this profile specification. The name of this function may be chosen freely, as long as it is a valid global identifier according to the LLVM standard. The entry point is identified by a custom function attribute; as mentioned in the section on attributes, this is the same set of attributes as in the base profile.
An entry point function may not take any parameters and must must return an
exit code in the form of a 64-bit integer. The exit code 0
must be used to
indicate a successful execution of the quantum program. Any other value of the
exit code indicates a failure during execution. The program IR must use exit
codes within the range 1
to 63
to indicate a failure; exit codes that are
larger than 63
are reserved for execution failures detected by the executing
backend. The recorded output always contains the exit code for each shot. If a
shot fails, no other output values may be recorded. See the output
schemas for more detail.
The Adaptive Profile program makes no restrictions on the structure of basic
blocks within the entry point
function, other than that a block cannot jump to a previously encountered block
in the control flow graph unless it makes use of the optionally supported
capability in Bullet 7. Execution starts at the entry block and follows the
control flow graph defined
by the block terminators ending when a block terminates in a return (ret
)
statement. Block names/block identifiers may be chosen arbitrarily, and the
order in which blocks are listed in the function definition is irrelevant for
execution.
The entry block contains the necessary call to initialize the execution environment as the first instruction to ensure that all used qubits are set to a zero-state. Calls to runtime functions for initialization may only appear at the beginning of the program. All subsequent instructions are either function calls, or any of the supported LLVM instructions. Calls to output recording functions may only be followed by calls to other output recording functions and the return statement. Calls to QIS functions, other runtime functions, or IR-defined functions (Bullet 6) if available, can occur at any point after initialization and before output recording.
For a quantum instruction set to be fully compatible with the Adaptive Profile, it must satisfy the following three requirements:
- All functions must return
void
, or any of the supported classical data types (Bullet 5). - Functions may take values of any type as arguments. Functions that measure
qubits must take the qubit pointer(s) as well as the result pointer(s) as
arguments and return
void
. - Functions that perform a measurement of one or more qubit(s) must be marked
with a custom function attribute named
irreversible
. The use of attributes in general is outlined in the corresponding section.
For more information about the relationship between a profile specification and the quantum instruction set, we refer to the paragraph on Bullet 1 in the introduction of this document. For more information about how and when the QIS is resolved, as well as recommendations for front- and backend developers, we refer to the document on compilation stages and targeting.
The following table lists all classical instructions the must be supported to execute a minimal Adaptive Profile program, that is a program that does not make use of any optional capabilities:
LLVM Instruction | Context and Purpose | Rules for Usage |
---|---|---|
call |
Used within a basic block to invoke any one of the QIS-, IR-, and runtime functions. | May optionally be preceded by a tail marker. |
br |
Used to branch from one basic block to another. | The branching is the final instruction in any basic block and may conditionally jump to different blocks depending on an i1 value. |
ret |
Used to return the exit code of the program. | Must occur (only) as the last instruction of the final block in an entry point, unless multiple return statements (optional capability) are supported. |
inttoptr |
Used to cast an i64 integer value to either a %Qubit* or a %Result* . |
May be used as part of a function call only. |
getelementptr inbounds |
Used to create an i8* to pass a constant string for the purpose of labeling an output value. |
May be used as part of a call to an output recording function only. |
See also the section on data types and values for more information about the creation and usage of LLVM values.
Additional LLVM instructions, as listed below, must be supported to enable classical computations (Bullet 4) and multiple target branching (Bullet 8).
If a backend chooses to support integer computations, then the following LLVM instructions must be supported:
LLVM Instruction | Context and Purpose | Note |
---|---|---|
add |
Adds two signed or unsigned integers. | Overflow behavior is undefined, no support for nuw and/or nsw . |
sub |
Subtracts two signed or unsigned integers. | Underflow behavior is undefined, no support for nuw and/or nsw . |
mul |
Multiplies two integers. | Overflow/underflow behavior is undefined, no support for nuw and/or nsw . |
udiv |
Divides two unsigned integers. | Division by zero leads to undefined behavior. |
sdiv |
Divides two signed integers. | Division by zero and overflow leads to undefined behavior. |
urem |
Computes the remainder of a division of two unsigned integers. | Division by zero leads to undefined behavior. |
srem |
Computes the remainder of a division of two signed integers. | Division by zero and overflow leads to undefined behavior. |
and |
Computes the bitwise logical AND of two integers. | |
or |
Computes the bitwise logical OR of two integers. | |
xor |
Computes the bitwise logical exclusive OR (XOR) of two integers. | |
shl |
Computes a bitwise left shift of an integer. | Behavior when shifting more bits that the bitwidth of the integer is undefined, no support for nuw . |
lshr |
Computes a bitwise right shift of an unsigned integer. | Behavior when shifting more bits that the bitwidth of the integer is undefined, no support for exact . |
ashr |
Computes a bitwise right shift of a signed integer. | Behavior when shifting more bits that the bitwidth of the integer is undefined, no support for exact . |
icmp |
Compares two signed or unsigned integers. | All condition codes as listed in the LLVM Language Reference must be supported. |
zext .. to |
Extends an integer value to create an integer of greater bitwidth by filling the added bits with zero. | May be used at any point in the program if classical computations on both the input and the output type are supported. May only be used as part of a call to an output recording function if computations on the output type are not supported. |
sext .. to |
Extends an integer value to create an integer of greater bitwidth by filling the added bits with the sign bit of the integer. | May be used at any point in the program if classical computations on both the input and the output type are supported. May only be used as part of a call to an output recording function if computations on the output type are not supported. |
trunc .. to |
Truncates the highest order bits of an integer to create an integer of smaller bitwidth. | Behavior if the truncation changes the value of the integer is undefined, no support for nuw and/or nsw . May be used at any point in the program if classical computations on both the input and the output type are supported. May only be used as part of a call to an output recording function if computations on the output type are not supported. |
select |
Evaluates to one of two integer values depending on a boolean condition. | |
phi |
Implement the φ node in the SSA graph representing the function. | Must be at the start of a basic block, or preceded by other phi instructions. |
For more information about any of these instructions, we refer to the corresponding section in the LLVM Language Reference.
If a backend chooses to support floating point computations, then the following LLVM instructions must be supported:
LLVM Instruction | Context and Purpose | Note |
---|---|---|
fadd |
Adds two floating-point values. | |
fsub |
Subtracts two floating-point values. | |
fmul |
Multiplies two floating-point values. | |
fdiv |
Divides two floating-point values. | Division by zero leads to undefined behavior, no support for NaN . |
fpext .. to |
Casts a value of floating-point type to a larger floating-point type. | May be used at any point in the program if classical computations on both the input and the output type are supported. May only be used as part of a call to an output recording function if computations on the output type are not supported. |
fptrunc .. to |
Casts a value of floating-point type to a smaller floating-point type. | May be used at any point in the program if classical computations on both the input and the output type are supported. May only be used as part of a call to an output recording function if computations on the output type are not supported. |
If the backend chooses to support multiple target branching, the following LLVM instruction must be supported:
LLVM Instruction | Context and Purpose | Note |
---|---|---|
switch |
Transfers control flow to one of several different blocks depending on an integer value. | The integer value that determines the block to jump to must be a constant value unless integer computations are supported. |
See also the LLVM Language
Reference for more
information about the switch
instruction.
The following runtime functions must be supported by all backends:
Function | Signature | Description |
---|---|---|
__quantum__rt__initialize | void(i8*) |
Initializes the execution environment. Sets all qubits to a zero-state if they are not dynamically managed. |
__quantum__rt__read_result | i1(%Result* readonly) |
Reads the value of the given measurement result and converts it to a boolean value. |
__quantum__rt__tuple_record_output | void(i64,i8*) |
Inserts a marker in the generated output that indicates the start of a tuple and how many tuple elements it has. The second parameter defines a string label for the tuple. Depending on the output schema, the label is included in the output or omitted. |
__quantum__rt__array_record_output | void(i64,i8*) |
Inserts a marker in the generated output that indicates the start of an array and how many array elements it has. The second parameter defines a string label for the array. Depending on the output schema, the label is included in the output or omitted. |
__quantum__rt__result_record_output | void(%Result*,i8*) |
Adds a measurement result to the generated output. The second parameter defines a string label for the result value. Depending on the output schema, the label is included in the output or omitted. |
__quantum__rt__bool_record_output | void(i1,i8*) |
Adds a boolean value to the generated output. The second parameter defines a string label for the result value. Depending on the output schema, the label is included in the output or omitted. |
If a backend chooses to support integer computations, then the following additional runtime function must be available:
Function | Signature | Description |
---|---|---|
__quantum__rt__int_record_output | void(i64,i8*) |
Records an integer value in the generated output. The second parameter defines the string label for the value. Depending on the output schema, the label is included in the output or omitted. |
If a backend chooses to support floating-point computations, then the following additional runtime function must be available:
Function | Signature | Description |
---|---|---|
__quantum__rt__float_record_output | void(f64,i8*) |
Records a floating-point value in the generated output. The second parameter defines the string label for the value. Depending on the output schema, the label is included in the output or omitted. |
The program output of a quantum application is defined by a sequence of calls to
runtime functions that record the values produced by the computation,
specifically calls to the runtime functions ending in record_output
listed in
the tables above. In the case of the Adaptive Profile,
these calls are contained within each block terminating in a return (ret
)
statement in the entry point function. Unless the backend supports multiple
return points (Bullet 9), there is a single block that contains all calls to
output recording functions followed by the final return statements. Multiple
return statements in the application code can be replaced with suitable phi
nodes by the compiler to propagate the data into that block.
For all output recording functions, the i8*
argument must be a non-null
pointer to a global constant that contains a null-terminated string. A backend
may ignore that argument if it guarantees that the order of the recorded output
matches the order defined by the entry point. Conversely, certain output schemas
do not require the recorded output to be listed in a particular order. For those
schemas, the i8*
argument serves as a label that permits the compiler or tool
that generated the labels to reconstruct the order intended by the program.
Compiler frontends must
always generate these labels in such a way that the bitcode does not depend on
the output schema; while choosing how to best label the program output is up to
the frontend, the choice of output schema, on the other hand, is up to the
backend. A backend may reject a program as invalid or fail execution if a label
is missing.
Both the labeling schema and the output schema are identified by a metadata
entry in the produced output. For the output schema, that
identifier matches the one listed in the corresponding specification. The
identifier for the labeling schema, on the other hand, is defined by the value
of the "output_labeling_schema"
attribute attached to the entry point.
Within the Adaptive Profile, local variables are created when reading mid-circuit measurements (Bullet 2) or to store classical computations (Bullet 5) if supported. This implies the following:
- Variables of boolean type may be defined, even if the backend supports none of the optional extensions to the Adaptive Profile.
- Values of type
%Qubit*
and%Result*
may only occur in function calls; specifically, local variables of these types cannot be created regardless of which optional extensions are supported by the targeted backend. See also the subsequent paragraphs for more detail. - Variables of numeric data types may be defined if the backend supports the extension in Bullet 5. Such variables may be used in the supported LLVM instructions acting on that data type, or in function calls.
- Call arguments can be constant values, classical variables, as well as
inttoptr
casts orgetelementptr
instructions that are inlined into a call instruction.
Constants of any type are permitted as arguments to QIS and runtime functions.
Constant values of type i64
, for example, are used and permitted as part of
calls to output recording functions regardless of whether integer computations
are supported; see the section on output recording for more
details.
The %Qubit*
and %Result*
data types must be supported by all backends.
Qubits and results can occur only as arguments in function calls and are
represented as a pointer of type %Qubit*
and %Result*
respectively. To be
compliant with the Adaptive Profile specification, the program must not make use
of dynamic qubit or result management, meaning qubits and results must be
identified by an integer value that is bitcast to a pointer to match the
expected type. How such an integer value is interpreted and specifically how it
relates to hardware resources is ultimately up to the executing backend.
The integer value that is cast must be either a constant, or a phi node of
integer type if iterations are used/supported.
If the cast value is a phi node, it must not directly or indirectly depend on
any quantum measurements. The integer constant that is cast must be in the
interval [0, numQubits)
for %Qubit*
and [0,numResults)
for %Result*
,
where numQubits
and numResults
are the required number of qubits and results
defined by the corresponding entry point attributes. Since
backends may look at the values of the required_num_qubits
and
required_num_results
attributes to determine whether a program can be
executed, it is recommended to index qubits and results consecutively so that
there are no unused values within these ranges.
The attribute usage and requirements of the Adaptive Profile remain the same as
defined in the Base Profile. The one change
from base profile is that there are additional relevant attributes (the base
profile only included inlinehint
, nofree
, norecurse
, readnone
,
readonly
, writeonly
, and argmemonly
) that can be used on functions,
depending on the exact instructions supported via using the extensions in the
Adaptive Profile.
The Adaptive Profile requires the same mandatory module flags as specified in the Base Profile. Additionally, the following module flags may be defined to indicate the use of optional capabilities. A lack of these module flags indicates that these capabilities are not used in the program.
- a flag with the string identifier
"int_computations"
that contains a string value where the string value is a comma-separated list of the supported/used integer precision(s). For example,!0 = !{i32 5, !"int_computations", !"i32,i64"}
. Classical computations on integers of all listed precisions must be supported by the executing backend. An empty value indicates that no integer computations are supported/used. - a flag with the string identifier
"float_computations"
that contains a string value where the string value is a comma-separated list of the supported/used floating-point precision(s). For example,!0 = !{i32 5, !"float_computations", !"f32,f64"}
. The precision must be one of the LLVM recognized values (f16, f32, f64, f80, or f128), and classical computations on floating point numbers of all listed precisions must be supported by the executing backend. An empty value indicates that no floating-point computations are supported/used. - A flag named
"ir_functions"
that contains a constanttrue
orfalse
value of typei1
value indicating if subroutines may be expressed a functions which can be called from the entry-point. - A flag named
"backwards_branching"
with ani2
value indicating which kinds of loops are supported. A value of0
indicates that the control flow graph does not contain any loops. A value of1
indicates that iterations as defined here are used. A value of2
indicates that conditionally terminating loops as described here may occur. A value of3
indicates that both iterations and conditional loops may occur. - A flag named
"multiple_target_branching"
with a constanttrue
orfalse
value of typei1
indicating if the program uses theswitch
instruction in llvm. - A flag named
"multiple_return_points"
with a constanttrue
orfalse
value of typei1
indicating whether multiple return statements can apper in a function within the IR as defined here.
Two forms of error messages can occur as a result of the submission of adaptive profile programs to a backend:
- Compile-time error messages.
- runtime error messages.
The compile-time error messages can occur when a backend doesn't support some of the optional features from Bullets 5-9. If upon inspecting a module flag, the backend determines that the Adaptive Profile program uses features not supported by the backend, then a compile-time error message should be provided.
The runtime error messages can occur when opting into features such as the classical computations in Bullets 5. An Adaptive Profile program that undergoes a real-time classical error (for example unchecked division by zero) has undefined behavior, and a backend is free to execute an undefined behavior. Programs can also check computations and provide error code by returning a value supported by a classical data type in a program, assuming a classical type specified in Bullet 5 is supported.
For example, consider a backend that supports integer computations and provides a runtime function for random number generation. Then an Adaptive Profile program may contain code like the following to do randomized benchmarking:
%0 = call i64 @__quantum__rt__rand_range(i64 0, i64 2)
%1 = icmp eq i64 %0, 0
br i1 %1, label %zero_rand_sequence, label %one_rand_sequence
By combining mid-circuit measurements with instructions on classical data types, you can conditionally apply gates based on logic using multiple mid-circuit measurements and boolean computations:
tail call void @__quantum__qis__h__body(%Qubit* null)
tail call void @__quantum__qis__mz__body(%Qubit* null, %Result* writeonly null)
tail call void @__quantum__qis__reset__body(%Qubit* null)
%0 = tail call i1 @__quantum__rt__read_result(%Result* readonly null)
tail call void @__quantum__qis__h__body(%Qubit* nonnull inttoptr (i64 1 to %Qubit*))
tail call void @__quantum__qis__mz__body(%Qubit* nonnull inttoptr (i64 1 to %Qubit*), %Result* writeonly nonnull inttoptr (i64 1 to %Result*))
tail call void @__quantum__qis__reset__body(%Qubit* nonnull inttoptr (i64 1 to %Qubit*))
%1 = tail call i1 @__quantum__rt__read_result(%Result* readonly nonnull inttoptr (i64 1 to %Result*))
%2 = and i1 %0, %1
br i1 %2, label %then, label %continue
then:
tail call void @__quantum__qis__x__body(%Qubit* nonnull inttoptr (i64 2 to %Qubit*))
br label %continue
continue:
...
Consider a backend that supports IR-defined functions and provides a cnot
instruction as part of the QIS, but not swap
. Defining and calling a swap
function may then greatly reduce code size for a program that involves frequent
use of swaps between qubits:
define void @swap(%Qubit* %arg1, %Qubit* %arg2) {
call void __quantum__qis__cnot__body(%Qubit* %arg1, %Qubit* %arg2)
call void __quantum__qis__cnot__body(%Qubit* %arg2, %Qubit* %arg1)
call void __quantum__qis__cnot__body(%Qubit* %arg1, %Qubit* %arg2)
}
define void @main() {
...
call void @swap(%Qubit* null, %Qubit* nonnull inttoptr (1 to %Qubit*))
...
}
Moreover, classical functions can be defined in the IR assuming that a backend has opted into supporting classical computations:
define i64 @triple(i64 %0) {
%1 = mul i64 %0, 3
ret i64 %1
}
define void @main() {
...
%0 = call void @triple(i64 2)
...
}