Introduction¶
Motivation¶
Recap of the Intel® Quantum Software Development Kit language¶
The Intel® Quantum Software Development Kit (SDK) is an extension of C++ with a
full LLVM-based C++ compiler, augmented to compile quantum-classical hybrid
programs using quantum hardware, simulator, or other quantum backend as an accelerator. The backend is
controlled by imperative gate calls inside a quantum kernel function—a C++
function with the quantum_kernel
function specifier. Quantum kernel
functions act as containers for quantum gate calls, analogous to a quantum
circuit. These functions are compiled into quantum basic blocks
(QBBs)—sequential lists of quantum instructions—which are
dispatched to the backend when the quantum_kernel
function is called. The compiler
and runtime also define and manage the interfaces between (both classical and
quantum) data and instructions so as to be nearly invisible to the user.

This style of using imperative function calls as containers for quantum gates is in contrast to a common approach for quantum programming using circuit generation frameworks. Quantum circuit generation frameworks use an object-oriented programming (OOP) paradigm, which allow users to construct circuit objects populated by gate objects. During classical runtime, a compiler or transpiler converts these circuit objects into a format readable by a qubit chip or simulator.
Circuit generation languages are extremely modular in that they allow
programmers to construct and manipulate circuit objects as first class values.
For example, many circuit generation languages support transformations that
invert an entire unitary circuit by inverting each gate and reversing their
order. Implementing such a transformation for quantum_kernel
functions would
require modifying the compiler directly, and is thus not very accessible for
users.
The Intel® Quantum SDK does not adopt the circuit generation paradigm, because embedding a circuit generation framework inside C++ would compromise the strict compiler guarantees maintained by the SDK needed to interface seamlessly with the quantum runtime and backend. However, the SDK does support a collection of features, known collectively as the Functional Language Extension for Quantum (FLEQ), that provide many of the benefits of circuit generation frameworks while preserving these compiler guarantees. To understand how FLEQ operates, the first step is to unravel the tension between compiler modularity and runtime guarantees by understanding the distinction between compile-time and runtime in the context of the SDK.
Compile-time vs. runtime¶
A compile-time construct, abstraction, or object is one that is completely resolved by the compiler such that the final binary has no trace of the construct. Two examples of compile-time constructs in C++ are classes (which are elaborated into structs and global functions in the compiler) and templates. In both cases, at some point in the compiler intermediate representation (IR), all trace of the construct has been removed. In principle, equivalent IR could have been generated by more rudimentary means only using the C language. Other terms associated with compile-time constructs are “static”, “resolvable” or “a priori”, i.e. a variable might be resolvable or known statically or a priori.
A runtime construct or object is one in which the final compiled binary does
maintain some signature of the construct, or where the construct must be
computed or known when the binary is executed. An example of a runtime construct
in C++ is runtime polymorphism, where several versions of
the same function or method might exist, and the decision of which should
be executed may depend on runtime factors. As such, every version of that
function must be compiled to binary with the addition of a vtable
to allow
for runtime selection. Many C++ standard library containers are also runtime
constructs as aspects like size and contents are not known a priori. Other terms
associated with runtime constructs are “dynamic” or “unresolvable (by the
compiler).”
Quantum kernel functions contain examples of both compile-time and runtime
constructs. Loops and branches using quantum gates and qubit gate
arguments are compile-time constructs as the compiler must fully resolve these
inside each quantum_kernel
function. If these were runtime constructs, the
runtime would have to take on prohibitive costs such as performing on-the-fly
qubit routing. On the other hand, classical parameters to gate arguments such as
rotation angles are runtime variables as they can be handled by the quantum
runtime library with little to no overhead for the quantum backend.
What is the Functional Language Extension for Quantum (FLEQ)?¶
Many of the limitations of the Intel® Quantum SDK can be traced back to the
compile-time versus runtime constraints imposed by quantum hardware.
For example, top-level
quantum_kernel
functions can not have qubit arguments, as all qubit references
must be resolved at compile time. Moreover, the SDK does not enable
meta-transformations on quantum_kernel
functions because they themselves are not
objects as in circuit generation languages.
However, OOP-style circuit generation is not a solution either. Member methods that manipulate circuit classes in C++ are runtime constructs, and have side effects that can be all but impossible to infer from IR. Circuit objects in C++ thus could not be transformed into QBBs at compile-time, in which case the SDK would be unable to meet compile-time and runtime requirements for issuing instructions to the quantum backend.
Seasoned C++ programmers might suggest adding modularity by passing quantum kernel functions via function pointers and lambdas (anonymous functions). This approach is in line with the spirit of the SDK, but must be done in such a way that the compiler can reason about the transformations and meet compile-time constraints.
To solve these problems, this document introduces the Functional Language Extension for Quantum (FLEQ) as a feature set for the Intel® Quantum Compiler (IQC). FLEQ allows for flexible, modular development of complex quantum logic while maintaining the compile-time constraints needed to generate QBBs. It is compatible with all other features of the SDK, and enhances the SDK’s seamless quantum-classical interfaces and efficient runtime execution. FLEQ adapts a functional programming paradigm that treats quantum kernels as first-class, immutable constructs that can be passed into and out of functions to facilitate robust, expressive, and easy-to-use compile-time reasoning.
FLEQ consists of:
An immutable type
QExpr
or quantum kernel expression that acts like a function pointer or lambda for quantum kernels.A set of compile-time methods for constructing quantum kernel expressions, with pure functional APIs that are guaranteed to have no side effects.
Features that alleviate many of the pain points to modular quantum code development with the SDK, including but not limited to:
Built-in meta-transformations such as unitary inversion and both unitary and classical control;
Support for custom generic and complex meta-transformations;
Adaptable and reusable submodule libraries using compile-time recursion and compile-time and runtime branching;
Support for algorithms and problem instances that abstract away gate-level implementation details; and
Compile-time and runtime debugging features.
In version 1.1 of the Intel® Quantum SDK, FLEQ is in its beta release and is still being actively developed. Bug reports, feature requests and general feedback are much appreciated. See Support for more details.
Basic concepts¶
Quantum kernel expressions (QExpr
)¶
The central component of FLEQ is the quantum kernel expression, or
QExpr
. A value of type QExpr
is an immutable compile-time representation of a block
of quantum logic, as well as associated classical
instructions.
A QExpr
value is a quantum program that has not yet been issued to the backend;
it can be thought of as an
unspecified or opaque function pointer to a quantum_kernel
function.
To see the difference between a QExpr
value and a quantum_kernel
function, consider a
quantum block that prepares a single qubit in the \(\ket{+}\) basis. As a quantum kernel
function, this block might look like:
qbit q;
quantum_kernel void prep_plus_qk() {
PrepZ(q);
H(q);
}
The same block of quantum instructions can be represented as a quantum kernel expression by writing
a function that returns a QExpr
value:
QExpr prep_plus_qexpr(qbit& q) {
return qexpr::_PrepZ(q) + qexpr::_H(q);
}
Syntax aside, (see Features for details) these two appear very similar, but there is a fundamental
difference. The quantum_kernel
function prep_plus_qk
is a fully specified sequence of quantum instructions; as such,
calling the function issues those instructions in the form of a QBB. The quantum kernel expression that is returned by prep_plus_qexpr(q)
ostensibly represents the same set of quantum instructions, but calling the function alone will not issue those instructions. Instead, the QExpr
returned by prep_plus_qexpr(q)
is just a representation of those instructions that can be manipulated by the programmer.
For example, the quantum kernel expression prep_plus_qexpr(q)
can be appended
with a Z
gate to take the plus state \(\ket{+}\) to the minus state
\(\ket{-}\): prep_plus_qexpr(q) + _Z(q)
. Furthermore, this \(\ket{+}\)-to-\(\ket{-}\) transformation can be generalized to any quantum kernel expression, as shown by a function of the form:
QExpr appendZ(QExpr e, qbit& q) {
return e + qexpr::_Z(q);
}
Then, the \(\ket{-}\) preparation sequence is the QExpr
value returned
by appendZ(prep_plus_qexpr(q), q)
. Note that all QExpr
values are
immutable, so it is not that appendZ
modifies its argument, but rather
returns a new unique QExpr
value.
Evaluating quantum kernel expressions¶
Once a QExpr
value has been constructed, the programmer must issue the
instructions it represents to the quantum backend. Doing so is
referred to as the evaluation of a QExpr
and is achieved by one of two
evaluation functions: void eval_hold(QExpr)
or void eval_release(QExpr)
.
For our examples above, the following main
function issues two identical
state preparation sequences to the backend:
qbit q;
quantum_kernel void prep_plus_qk() { ... }
QExpr prep_plus_qexpr(qbit& q) { ... }
int main() {
... // Initialization of the Intel Quantum SDK runtime omitted
prep_plus_qk(); // Invoke the qubit chip by calling a quantum_kernel function
eval_hold(prep_plus_qexpr(q)); // Invoke the qubit chip by evaluating a QExpr value
return 1;
}
The call to eval_hold
tells the compiler that its QExpr
argument should
be compiled and sent to the quantum runtime at this point in the code. The
difference between eval_hold
and eval_release
is analogous to the
release_quantum_state
directive (see the Language Extensions): eval_hold(e)
guarantees that the quantum state is preserved after executing the QBB produced
by compiling e
; eval_release(e)
makes no guarantees of the quantum state
after execution, and should be used when the user is only interested in
measurement results.
Compile-time lists (QList
and DataList
)¶
FLEQ enables modular quantum programming by allowing users to build quantum
kernel expressions whose contents depend on compile-time arguments. For example,
suppose a programmer wants to prepare each qubit in an array into a
\(\ket{+}\) state. To do this with quantum_kernel
functions, the programmer
would need make extensive use of C++ templates, since qubit arrays and loop
bounds must both be determined at compile-time. However, using quantum kernel
expressions, a user can write a function that takes as input a compile-time
qubit list and returns a QExpr
. This compile-time qubit list (QList
) and
the corresponding type of compile-time strings (DataList
) give programmers
flexibility while ensuring the compiler has what it needs to interact with the
backend.
A qubit list or QList
is a compile-time wrapper around static qbit
arrays. QList
values can be concatenated to form a new single QList
, or sliced into sub-lists
to form arbitrary orderings of qubits. Qubits can be individually addressed via the subscript ([]
)
operator, and the size of the array can be resolved at compile-time using member function size()
. A QList
can be declared using the listable
macro, as in
const int N = 5;
qbit listable(qs, N);
A DataList
value is compile-time string; like a QList
, it is a wrapper
around around statically defined char
arrays, and can be joined and sliced
into form arbitrary permutations of the string. Data lists also support a
variety of substring search functions; type conversion to int
, bool
and
double
types; and string comparison. The DataList
feature can even be used to develop
domain-specific languages (DSLs) that allow for higher-level representations
that abstract away gate-level implement details. See DataList
and Domain-specific languages using FLEQ for more details.
Getting started¶
Using FLEQ requires the quintrinsics.h
header that is required for the base use of the SDK.
In addition, the three core features of FLEQ are provided by three additional header files:
#include <clang/Quantum/quintrinsics.h> // always required
#include <clang/Quantum/qexpr.h> // required for QExpr features
#include <clang/Quantum/qlist.h> // required for QList features
#include <clang/Quantum/datalist.h> // required for DataList features
Programs using FLEQ are compiled using the same command-line flags and arguments as the base
SDK, with some additional optional flags specific to FLEQ (as described in the relevant
sections of Features). All other features and flags are fully compatible
with FLEQ, including the use of quantum_kernel
functions, with one
exception: quantum_kernel
functions that call basic gates cannot also contain QExpr
evaluation calls. See Known limitations.
We also note here that for FLEQ-generated QBBs, there is no appreciable difference between the use
of the -O0
and -O1
optimization flags for reasons described in Overview of FLEQ compilation;
see also Known limitations.