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.

Diagram showing the relationship between .cpp files, the Intel Quantum Compiler, and the Intel Quantum Runtime.

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:

  1. An immutable type QExpr or quantum kernel expression that acts like a function pointer or lambda for quantum kernels.

  2. A set of compile-time methods for constructing quantum kernel expressions, with pure functional APIs that are guaranteed to have no side effects.

  3. Features that alleviate many of the pain points to modular quantum code development with the SDK, including but not limited to:

    1. Built-in meta-transformations such as unitary inversion and both unitary and classical control;

    2. Support for custom generic and complex meta-transformations;

    3. Adaptable and reusable submodule libraries using compile-time recursion and compile-time and runtime branching;

    4. Support for algorithms and problem instances that abstract away gate-level implementation details; and

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