QIR

The Quantum Intermediate Representation (QIR) is a hardware-agnostic, LLVM-based intermediate representation designed to unify quantum and classical code under a common compilation framework [1]. Proposed and maintained by the QIR Alliance (including Quantinuum), QIR extends LLVM IR with quantum constructs, such as opaque types and specialized quantum instruction functions, allowing a wide variety of quantum front-end languages to target diverse quantum hardware back-ends, whilst leveraging LLVM’s mature optimizations and infrastructure [2]. QIR programs adhere to different profiles. Each QIR profile is a dialect of QIR and is a feature specification for hardware providers.

Quantinuum Systems supports execution of native QIR programs. Prior to execution, QIR programs undergo server-side transpilation to Quantinuum’s Quantum Instruction Set (QIS). Quantinuum’s QIS is an extended LLVM IR that can represent native quantum instructions and control flow primitives.

QIR lifecycle on Quantinuum Systems

Server-side QIR lifecycle (post-submission actions in the hardware stack).

Quantinuum Nexus supports management and submission of QIR programs to Quantinuum systems [3]. A workflow specifying Nexus submission pathway for QIR is available here. Textual QIR representations must be compiled to QIR bitcode prior to program upload and job submission using a client-side tool, pyqir [4]. System Model H2 supports the (core) adaptive profile and Quantinuum Helios supports the (full) adaptive profile. The availability of QIR on Quantinuum Systems also enables partner integrations with Nvidia CUDA-Q and Microsoft Q#. Quantinuum’s Guppy provides a front-end compiler to generate HUGR, a compact graph-based representation. Alternate off-the-shelf languages, such as Nvidia CUDA-Q, must equivalently lower to QIR.

This document provides a QIR specification for System Model H2 and Quantinuum Helios. Additionally, a comparison is provided with Guppy to distinguish feature gaps.

Platform Specifications

QIR is supported on both Helios and H2. However, Helios affords users the ability to use optional QIR specifications, enabling access to Helios features also supported by Guppy. Table 1. provides a QIR specification for Helios and H2.

Additionally, a side-by-side comparison with Guppy is provided. Whilst Guppy is a human-writeable programming language, QIR is an intermediate representation format generated by front-end compilers. The Guppy compiler’s equivalent of QIR is HUGR.

Capabilities

System Model H2 (QIR)

Quantinuum Helios (QIR)

Quantinuum Helios (Guppy)

Adaptive Profile Specification

Quantum Transformations

Core

Measurements

Core

Forward Branching

Core

Program Output

Core

Classical Computations

Integer-only

Optional

IR defined functions and function calls

Optional

Backwards Branching

Optional

Multiple Target Branching

Optional

Multiple Return Points

Optional

Collections (Qubits)

Higher Order Functions

Dynamic Qubit Allocation

Integrated Random Number Generator

Quantinuum Extension

Wasm callout

Quantinuum Extension

Shot Index

Quantinuum Extension

Heralded Leakage Measure

Quantinuum Extension

Guppy compiles to a compact graph-based representation (HUGR) which is lowered to LLVM adhering to Quantinuum’s QIS. QIR is extended LLVM. A soft transpilation pass, applied to the QIR program, enables conversion of quantum operations to QIS. The Full Adaptive QIR profile enables use of new features on Quantinuum Helios. However, the Full Adaptive profile does not capture all of Guppy’s capabilities.

Core Adaptive Profile Specifications

Quantum Transformations

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. Targeting a program to a specific backend requires choosing a suitable profile and quantum instruction set (QIS). Quantinuum QIS is compatible with the Adaptive Profile. More information on QIS is available here.

Measurements

Both H2 and Helios support Measurement operations during and at the end of a program.

The irreversible attribute is required to be flagged, and declared @__quantum__qis__m_body(%Qubit*, %Result*) and @__quantum__qis__reset_body(%Qubit*) operations must be marked by this attribute.

declare void @__quantum__qis__m__body(%Qubit*, %Result*) #1
declare void @__quantum__qis__reset__body(%Qubit*) #1

attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="adaptive_profile" "required_num_qubits"="5" "required_num_results"="6" }
attributes #1 = { "irreversible" }

Forward Branching

Both Helios and H2 enables actions 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. Bounded iterations in a surface-level language, targetting H2, are unrolled at compile time, and upon unrolling, all values used to identify a qubit or result value can be replaced by a constant.

Programs targetting Helios and H2 are requred to call two different functions to convert %Result* into i1, respectively. Helios consumes a runtime function, i1 @__quantum__rt__read_result(%Result* readonly). H2 consumes a QIS function, i1 @__quantum__qis__read_result__body(%Result* readonly).

Program Output

Note

For System Model H2, the hardware QIR compiler must use phi-nodes to consolidate error codes into a single final return.

QIR and its profiles must clearly express program output. Both the Base and Adaptive Profiles requires the user to specify an ordering over a selection of measurement results. A list of runtime (rt) functions are available here. Quantinuum Systems uses Output Schema Labelling to dictate how results are formatted and identified via a header record [5]. Helios and H2 have distinct header records. Helios supports outputting classical values.

A QIR program that contains a two calls to the integer output recording function. Globally defined strings with fixed length byte arrays are used to represent the result register. Inline getelementptr to retrieve the underlying memmory address of the global structure. This results in a pointer to the first byte of the array, which can then be passed as an argument to functions that expect a char*.

@0 = internal constant [3 x i8] c"0_i\00"

define void @program_main() #0 {
entry:
    call void @__quantum__rt__int_record_output(i64 42, i8* getelementptr inbounds ([3 x i8], [3 x i8]* @0, i32 0, i32 0))
    ret void
}
declare void @__quantum__rt__int_record_output(i64, i8*)

The output for 3 shots would have the following form (using fabricated METADATA records).

HEADER\tschema_id\tlabeled
HEADER\tschema_version\t1.0
START
METADATA\tentry_point
METADATA\tqir_profiles\tbase_profile
METADATA\trequired_num_qubits\t5
METADATA\trequired_num_results\t5
METADATA\toutput_labeling_schema\tlabeled
OUTPUT\tINT\t42\t0_i
END\t0
START
OUTPUT\tINT\t41\t0_i
END\t0
START
OUTPUT\tINT\t42\t0_i
END\t0

More information on output result records is available here.

Optional Adaptive Profile Specifications

Classical Computations

Note

The only classical value types allowed are %Int, %Double, %Result, %Pauli, and tuples of these values.

Quantinuum Helios optionally supports classical computations on atomic data types, including integer and floating-point arithmetic. System Model H2 only support the integer computation capability. The behavior in the case of an overflow or underflow may be undefined. Data consumed in classical computations must be specified in the form of module flags. 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. 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.

The following calls are supported by the integer capability.

LLVM Instruction

Description

Platform

Notes

add

Adds two signed or unsigned integers.

All

Overflow behavior is undefined, no support for nuw and/or nsw.

sub

Subtracts two signed or unsigned integers.

All

Underflow behavior is undefined, no support for nuw and/or nsw.

mul

Multiplies two integers.

All

Overflow/underflow behavior is undefined, no support for nuw and/or nsw.

udiv

Divides two unsigned integers.

Helios

Division by zero leads to undefined behavior.

sdiv

Divides two signed integers.

Helios

Division by zero and overflow leads to undefined behavior.

urem

Computes the remainder of a division of two unsigned integers.

Helios

Division by zero leads to undefined behavior.

srem

Computes the remainder of a division of two signed integers.

Helios

Division by zero and overflow leads to undefined behavior.

and

Computes the bitwise logical AND of two integers.

All

or

Computes the bitwise logical OR of two integers.

All

xor

Computes the bitwise logical exclusive OR (XOR) of two integers.

All

shl

Computes a bitwise left shift of an integer.

All

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.

All

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.

All

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

,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.

All

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.

All

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.

All

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.

All

phi

Implement the φ node in the SSA graph representing the function.

All

Must be at the start of a basic block, or preceded by other phi instructions.

The following calls are supported by the floating-point capability.

LLVM Instruction

Description

Platform

fadd

Adds two floating-point values.

Helios

fsub

Subtracts two floating-point values.

Helios

fmul

Multiplies two floating-point values.

Helios

fdiv

Divides two floating-point values. Division by zero leads to undefined behavior, no support for NaN.

Helios

fcmp

Compares two floating-point

Helios

fpext .. to

Casts a value of floating-point type to a larger floating-point type.

Helios

fptrunc .. to

Casts a value of floating-point type to a smaller floating-point type.

Helios

IR-defined Functions and Function Calls

IR-defined functions are function implementations 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 flags.

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.

Backwards Branching

Note

phi-nodes in the Static Single Assignment (SSA) graph for a function, are required to reconcile variables from different control paths in hybrid quantum-classical programs.

This specification enables a compact expression of loops within user programs. 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. Iterations necessarily requires supporting the use of phi-nodes to identify a qubit or result value. For both iterations and conditionally exiting a loop, the value of a phi-node used to identify a qubit or result value never depends on any quantum measurements.

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. The phi-node is defined as 1 for the first iteration and updated to an incremented value at the start of each successive iteration. The loop terminates after the fourth iteration. Each iteration applies a CNOT gate between qubit 0 (control) and a target qubit from the range 1 to 4.

define void @simple_loop() {
entry:
  br label %loop_body
loop_body:                          ; preds = %loop_body, %entry
  %0 = phi i64 [ 1, %entry ], [ %1, %loop_body ]
  %qptr = inttoptr i64 %0 to %Qubit*
  call void @__quantum__qis__cnot__body(%Qubit* null, %Qubit* nonnull %qptr)
  %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
}

Multiple Target Branching

Support for 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. 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. Please see the LLVM language reference for more information about the switch instruction.

Multiple Return Points

A QIR program targetting Helios can use this specification if the optional IR Functions capability is supported. This specification enables initialization or IR-defined functions to support multiple return points, eliminating 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 module flags.

A return statement is necessarily always the last statement in a block. For each block that 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
}

Quantinuum Platform Utilities

Quantinuum provides additional function calls for users to consume within programs. A list of all function calls is provided in the table below.

Capabilities

Function

Description

Platform

RNG init

void @___random_seed(i64 %seed)

Create a new random number generator using a seed

All

Random Integer

i32 @___random_int()

Generate a random 32-bit signed integer

All

Random Double

double @___random_float()

Generate a random floating point value in the range [0,1)

All

Bounded Integer

i32 @___random_int_bounded(i32 %bound)

Generate a random 32-bit integer in the range [0, bound)

All

Random Advance

void ___random_advance(i64)

Advance the random number generator so we can regenerate the same random number

Helios

Shot Index

i64 @___get_current_shot()

Returns the index of the current shot

All

Wasm Context

i64 @___get_wasm_context

Get the current Wasm context

Helios

RNG Capability

Quantinuum Systems provides an integrated real-time Random Number Generation (RNG) capability. Only the void @___random_advance(i64) is exclusive to Helios. The RNG capability allows a user to instantiate an RNG instance with a specified seed. The RNG instance can be used to generate a 32-bit signed integer, a bounded 32-bit signed integer within the interval [0, b), and a random float value in the interval [0, 1).

All platform utilities relating to the RNG capability must be declared within the LLVM IR.

declare void @___random_seed(i64)
declare i32 @___random_int()
declare double @___random_float()
declare i32 @___random_int_bounded(i32)
declare void @___random_advance(i64)

Shot Index

Quantinuum provides a utility to return the index of the current shot as an 64-bit signed integer. The function must be declared in the program.

declare i64 @__get_current_shot()

WebAssembly Callout

Note

For QIR targetting System Model H2, the @__get_wasm_context function is not required.

The WebAssembly (Wasm) context utility enables wasm calls in the user program. i64 @__get_wasm_context. Each function defined in the *.wasm binary must be declared as program attributes. Wasm must be declared as a program attribute and specified for each wasm function to be called from *.wasm.

; WASM init function
declare i64 @___get_wasm_context(i64)

; WASM function declarations
declare i64 @round_to_int(i64, double) #2
declare i64 @abs_i64(i64, i64) #2
declare i64 @sqrt_floor(i64, double) #2
declare i64 @factorial(i64, i64) #2

; WASM attribute
attributes #2 = { "wasm" }

Supported QIR Functions

Available QIS functions

The table below specifies the available QIS functions to consume on the H2 and Helios platforms.

Operation

QIR

Platform

Native

\(R_{xy} \left( \theta, \phi \right)\)

__quantum__qis__rxy__body(double, double, %Qubit*)

All

\(R_{z} \left( \theta \right)\)

void @__quantum__qis__rz__body(double, %Qubit*)

All

\(R_{zz} \left( \theta \right)\)

void @__quantum__qis__rzz__body(double, %Qubit*, %Qubit*)

All

\(R_{xxyyzz} \left( \theta \right)\)

void @__quantum__qis__rxxyyzz__body(double, %Qubit*, %Qubit*)

H2

Measure

void @__quantum__qis__mz__body(%Qubit*, %Result* writeonly)

All

Reset

void @__quantum__qis__reset__body(%Qubit*)

All

Pauli-x

__quantum__qis__x__body(%Qubit*)

All

Pauli-y

__quantum__qis__y__body(%Qubit*)

All

Pauli-z

__quantum__qis__z__body(%Qubit*)

All

s

__quantum__qis__s__body(%Qubit*)

All

s_conjugate

__quantum__qis__s__adj(%Qubit*)

All

t

void @__quantum__qis__t__body(%Qubit*)

All

t conjugate

void @__quantum__qis__t__adj(%Qubit*)

All

\(R_{x}\)

void @__quantum__qis__rx__body(%Qubit*)

All

\(R_{y}\)

void @__quantum__qis__ry__body(%Qubit*)

All

CZ

void @__quantum__qis__cz__body(%Qubit*, %Qubit*)

All

CX

void @__quantum__qis__cx__body(%Qubit*, %Qubit*)

All

CCX

void @__quantum__qis__ccx__body(%Qubit*, %Qubit*, %Qubit*)

All

Measure & Reset

@__quantum__qis__mresetz__body(%Qubit*, %Result* writeonly)

All

The function :code:void @__quantum__qis__cnot__body(%Qubit*, %Qubit*) is a synonym for @__quantum__qis__cnot__body.

Available RT functions

The table below specifies the available Runtime (RT) functions to consume on the H2 and Helios platforms.

Operation

QIR

Platform

Description

Runtime Initialize

void @__quantum__rt__initialize(i8*)

Helios

Intialize Quantum Runtime

Qubit Allocation

%Qubit* @__quantum__rt__qubit_allocate()

Helios

Allocate a physical ion

Qubit Deallocation

void @__quantum__rt__qubit_release(%Qubit*)

Helios

Release a physical ion

Read Result

i1 @__quantum__rt__read_result(%Result* readonly)

All

Read result and return “i1” value.

Result Record

void @__quantum__rt__result_record_output(%Result*, i8*)

All

Output Reordering Functions

Int Result Record

void @__quantum__rt__int_record_output(i64, i8*)

All

Output Reordering Functions

Double Result Record

void @__quantum__rt__double_record_output(double, i8*)

Helios

Output Reordering Functions

Bool Result Record

void @__quantum__rt__bool_record_output(i1, i8*)

Helios

Output Reordering Functions

Tuple Result Record

@__quantum__rt__tuple_record_output(i64, i8*)

All

Output Reordering Functions

Array Result Record

void @__quantum__rt__array_record_output(i64, i8*)

All

Output Reordering Functions

Gate-set Decomposition

QIR

Decomposition

Remarks

__quantum__qis__ccx__body( %Qubit* %q,  %Qubit* %q,  %Qubit* %q)

See below

__quantum__qis__cx__body( %Qubit* %q,  %Qubit* %q)

See below

__quantum__qis__cz__body( %Qubit* %q,  %Qubit* %q)

See below

__quantum__qis__h__body( %Qubit* %q)

rxy(π/2, -π/2, %q); rz(π, %q)

__quantum__qis__rx__body(double,  %Qubit* %q)

rxy(double, 0, %q)

__quantum__qis__ry__body(double,  %Qubit* %q)

rxy(double, π/2, %q)

__quantum__qis__s__adj( %Qubit* %q)

rz(-π/2, %q)

__quantum__qis__s__body( %Qubit* %q)

rz(π/2, %q)

__quantum__qis__t__adj( %Qubit* %q)

rz(-π/4, %q)

__quantum__qis__t__body( %Qubit* %q)

rz(π/4, %q)

__quantum__qis__x__body( %Qubit* %q)

rxy(π, 0, %q)

__quantum__qis__y__body( %Qubit* %q)

rxy(π, π/2, %q)

__quantum__qis__z__body( %Qubit* %q)

rz(π, %q)

__quantum__qis__mresetz__body( %Qubit* %q,  %Qubit* %q)

mz(%q, %reset); reset(%q)

From Q# QDK

__quantum__qis__rxxyyzz__body(double, double, double,  %Qubit* %q,  %Qubit* %q)

error

Unsupported

__quantum__qis__zz__body( %Qubit* %q,  %Qubit* %q)

error

Use rzz(π/2) instead

__quantum__qis__u1q__body(double, double,  %Qubit* %q)

Synonym for rxy

Legacy

__quantum__qis__cnot__body( %Qubit* %q,  %Qubit* %q)

Synonym for cx

Legacy

__quantum__qis__m__body( %Qubit* %q,  %Qubit* %q)

Synonym for mz

Q# QDK only

Controlled X gate (CX)

__quantum__qis__cx__body(control, target) is decomposed to

__quantum__qis__rxy__body(-π/2, π/2, target);
__quantum__qis__rzz__body(π/2, control, target);
__quantum__qis__rz__body(-π/2, control);
__quantum__qis__rxy__body(π/2, π, target);
__quantum__qis__rz__body(-π/2, target);

Controlled Z gate (CZ)

__quantum__qis__cz__body(control, target) is decomposed to

__quantum__qis__rxy__body(π, π/2, control);
__quantum__qis__rzz__body(π/2, control, target);
__quantum__qis__rxy__body(π, -π/2, control);
__quantum__qis__rz__body(π/2, target);
__quantum__qis__rz__body(π/2, control);

Toffoli gate (CCX)

__quantum__qis__ccx__body(control1, control2, target) is decomposed to

__quantum__qis__rxy__body(π, -π/2, target);
__quantum__qis__rzz__body(π/2, control2, target);
__quantum__qis__rxy__body(π/4, π/2, target);
__quantum__qis__rzz__body(π/2, control1, target);
__quantum__qis__rxy__body(π/4, 0, target);
__quantum__qis__rzz__body(π/2, control2, target);
__quantum__qis__rxy__body(π/4, -π/2, target);
__quantum__qis__rzz__body(π/2, control1, target);
__quantum__qis__rxy__body(π, π/4, control1);
__quantum__qis__rxy__body(-3π/4, π, target);
__quantum__qis__rzz__body(π/4, control1, control2);
__quantum__qis__rz__body(π, target);
__quantum__qis__rxy__body(π, -π/4, control1);
__quantum__qis__rz__body(-3π/4, control2);
__quantum__qis__rz__body(π/4, control1);

Output Schema Labelling

These runtime functions determine how values should be collected to be emitted as output. Each of these functions follow the naming pattern __quantum__rt__*_record_output where the initial part indicates the type of output to be recorded.

Result

void @__quantum__rt__result_record_output(%Result*, i8*)

Produces output records of the format "OUTPUT\tRESULT\t0\tlabel" or "OUTPUT\tRESULT\t1\tlabel", representing measurement results. The fourth element is a string label associated to the result value which is included in the corresponding output record.

Boolean

void @__quantum__rt__bool_record_output(i1, i8*)

Produces output records of the format "OUTPUT\tBOOL\tfalse\tlabel" or "OUTPUT\tBOOL\ttrue\tlabel". The fourth element (label) is a string label associated to the Boolean value which is included in the corresponding output record.

Integer

void @__quantum__rt__int_record_output(i64, i8*)

Produces output records of the format "OUTPUT\tINT\tn\tlabel" where n is the string representation of the integer value, such as "OUTPUT\tINT\t42\tlabel". The fourth element (label) is a string label associated to the integer value, which is included in the corresponding output record.

Double

void @__quantum__rt__double_record_output(double, i8*)

Produces output records of the format "OUTPUT\tDOUBLE\td\tlabel" where d is the string representation of the double value, such as "OUTPUT\tDOUBLE\t3.14159\tlabel". The fourth element (label) is a string label associated to the double value which is included in the corresponding output record.

Tuple

void @__quantum__rt__tuple_record_output(i64, i8*)

Produces output records of the format "OUTPUT\tTUPLE\tn\tlabel" where n is the string representation of the integer value, such as "OUTPUT\tTUPLE\t4\tlabel". The fourth element (label) is a string label associated to the tuple which is included in the corresponding output record. This record indicates the existence and length of a tuples.

A QIR program that returns a tuple of a measurement result and a double value uses the following output recording functions.

@0 = internal constant [4 x i8] c"0_t\00"
@1 = internal constant [6 x i8] c"1_t0r\00"
@2 = internal constant [6 x i8] c"2_t1d\00"
call void @__quantum__rt__tuple_record_output(i64 2, i8* getelementptr inbounds ([4 x i8], [4 x i8]* @0, i32 0, i32 0))
call void @__quantum__rt__result_record_output(%Result* %2, i8* getelementptr inbounds ([6 x i8], [6 x i8]* @1, i32 0, i32 0))
call void @__quantum__rt__double_record_output(double %3, i8* getelementptr inbounds ([6 x i8], [6 x i8]* @2, i32 0, i32 0))
ret void

The output for 3 shots would have the following form (using fabricated METADATA records).

HEADER\tschema_id\tlabeled
HEADER\tschema_version\t1.0
START
METADATA\tentry_point
METADATA\tqir_profiles\tbase_profile
METADATA\trequired_num_qubits\t5
METADATA\trequired_num_results\t5
METADATA\toutput_labeling_schema\tlabeled
OUTPUT\tTUPLE\t2\t0_t
OUTPUT\tRESULT\t0\t1_t0r
OUTPUT\tDOUBLE\t0.42\t2_t1d
END\t0
START
OUTPUT\tTUPLE\t2\t0_t
OUTPUT\tRESULT\t1\t1_t0r
OUTPUT\tDOUBLE\t0.42\t2_t1d
END\t0
START
OUTPUT\tTUPLE\t2\t0_t
OUTPUT\tRESULT\t0\t1_t0r
OUTPUT\tDOUBLE\t0.25\t2_t1d
END\t0

Array

void @__quantum__rt__array_record_output(i64, i8*)

Produces output records of the format "OUTPUT\tARRAY\tn\tlabel" where n is the string representation of the integer value, such as "OUTPUT\tARRAY\t4\tlabel". The fourth element (label) is a string label associated to the array which is included in the corresponding output record. This record indicates the existence and length of an array.

For two arrays of measurement results, the QIR program contains array output recording calls where the first argument indicates the length of the array, followed by the corresponding output recording calls that represent each one of the array items (shown with static result allocation):

@0 = internal constant [5 x i8] c"0_0a\00"
@1 = internal constant [7 x i8] c"1_0a0r\00"
@2 = internal constant [5 x i8] c"2_1a\00"
@3 = internal constant [7 x i8] c"3_1a0r\00"
@4 = internal constant [7 x i8] c"4_1a1r\00"
call void @__quantum__rt__array_record_output(i64 1, i8* getelementptr inbounds ([5 x i8], [5 x i8]* @0, i32 0, i32 0))
call void @__quantum__rt__result_record_output(%Result* %2, i8* getelementptr inbounds ([7 x i8], [7 x i8]* @1, i32 0, i32 0))
call void @__quantum__rt__array_record_output(i64 2, i8* getelementptr inbounds ([5 x i8], [5 x i8]* @2, i32 0, i32 0))
call void @__quantum__rt__result_record_output(%Result* nonnull inttoptr (i64 0 to %Result*), i8* getelementptr inbounds ([7 x i8], [7 x i8]* @3, i32 0, i32 0))
call void @__quantum__rt__result_record_output(%Result* nonnull inttoptr (i64 0 to %Result*), i8* getelementptr inbounds ([7 x i8], [7 x i8]* @4, i32 0, i32 0))
ret void

The output for 3 shots would have the following form (using fabricated METADATA records).

HEADER\tschema_id\tlabeled
HEADER\tschema_version\t1.0
START
METADATA\tentry_point
METADATA\tqir_profiles\tbase_profile
METADATA\trequired_num_qubits\t5
METADATA\trequired_num_results\t5
METADATA\toutput_labeling_schema\tlabeled
OUTPUT\tARRAY\t1\t0_0a
OUTPUT\tRESULT\t0\t1_0a0r
OUTPUT\tARRAY\t2\t2_1a
OUTPUT\tRESULT\t1\t3_1a0r
OUTPUT\tRESULT\t1\t4_1a1r
END\t0
START
OUTPUT\tARRAY\t1\t0_0a
OUTPUT\tRESULT\t1\t1_0a0r
OUTPUT\tARRAY\t2\t2_1a
OUTPUT\tRESULT\t1\t3_1a0r
OUTPUT\tRESULT\t1\t4_1a1r
END\t0
START
OUTPUT\tARRAY\t1\t0_0a
OUTPUT\tRESULT\t0\t1_0a0r
OUTPUT\tARRAY\t2\t2_1a
OUTPUT\tRESULT\t1\t3_1a0r
OUTPUT\tRESULT\t1\t4_1a1r
END\t0

Program Structure

The program structure is desseminated below for Helios-compliant and H2-compliant QIR programs.

An Adaptive Profile-compliant program is defined in the form textual LLVM IR. The program structure is demonstrated below for a CX teleportation. This program adheres to the QIR Core Adaptive Profile + Classical Computation Specification.

Type Definitions

; type definitions

%Result = type opaque
%Qubit = type opaque

Global Constants

global constants that store string labels needed for certain output schemas. Optionally, if classical computations are supported, global constants of the supported classical data types should be defined here.

@0 = internal constant [5 x i8] c"0_t0\00"
@1 = internal constant [5 x i8] c"0_t1\00"

Program Logic

QIR programs consist of multiple blocks. If IR-defined functions are supported, additional functions can be called . There are two mandatory block entry and body.

The entry block initializes the quantum runtime.

define i64 @TeleportChain() local_unnamed_addr #0 {
entry:
  ; calls to initialize the execution environment
  call void @__quantum__rt__initialize(i8* null)
  br label %body

The body block consists of quantum QIS and runtime calls. A runtime function is used to convert the %Result value into i1. This conversion is necessary for forward branching.

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

then_1 is the first conditional block consisting of 1 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

If the conditional block then_1 completes execution, the continue_1 block measures and resets qubit with index 2.

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

The then_2 block is called conditionally, based on the measurement value from continue_1. Completion of then_2 calls 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

The then_3 block is called conditionally, based on the measurement value from continue_3. Subsequent then and continue blocks are incrementally called up to the limit of 4.

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

The exit block calls quantum runtime functions to record measurement results in a specified order.

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
}

QIS Function Declaration

QIS function declaration for functions consumed by the program.

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

Runtime Function Declaration

Runtime function declaration used for initialization and output reordering [6].

declare void @__quantum__rt__initialize(i8*)
declare i1 @__quantum__rt__read_result(%Result* readonly)
declare void @__quantum__rt__result_record_output(%Result*, i8*)

Attributes

One or more attribute groups used to store information about the entry point, and optionally additional information about other function declarations. If Wasm is used, it must be declared as an attribute.

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

Metadata a compiler or backend may need to process the bitcode, including module flags that indicate which features of the Adaptive Profile are consumed.

!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", !10}
!5 = !{i32 5, !"float_computations", !11}
!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}
!10 = !{!"i32", !"i64"}
!11 = !{!"float", !"double"}

Platform Examples

The LLVM IR files can be downloaded here.

Helios

H2

BitCode Generation

One LLVM IR file is loaded into the python session as a string and then converted into QIR bitcode.

with open("teleported_cx.ll", "r") as file_io:
    llvm_ir = file_io.read()

print(llvm_ir)
; 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", !10}
!5 = !{i32 5, !"float_computations", !11}
!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}
!10 = !{!"i32", !"i64"}
!11 = !{!"float", !"double"}

The function pyqir.Module allows textual QIR to be converted into bitcode.

The function pyqir.Module allows textual QIR to be converted into bitcode.

The function pyqir.Module allows textual QIR to be converted into bitcode.

import pyqir

qir_bitcode = pyqir.Module.from_ir(pyqir.Context(), llvm_ir, "teleported_cx").bitcode

References