Let/get, printing, and exiting

This section covers some useful builtin utilities for working with quantum kernel expressions.

Let/get

All variables of type QExpr are constant, which means that they cannot be assigned using ordinary C++ assignment statements. FLEQ provides several ways to assign temporary variables to help break up large quantum kernel expressions.

  1. Write a function that returns a QExpr, as illustrated throughout this document.

  2. Use qexpr::let and qexpr::get to assign a QExpr to a constant string name.

void qexpr::let(const char key[], QExpr e)

Associate a key value key to the quantum kernel expression e.

QExpr qexpr::get(const char key[])

Return the quantum kernel expression associated with key.

As an example:

qexpr::let("coin_toss", qexpr::_PrepZ(q) + qexpr::_H(q) + qexpr::_MeasZ(q,c));

The variable coin_toss can then be recalled later in the program with the get function:

qexpr::eval_hold(qexpr::cIfTrue(b, qexpr::get("coin_toss")));

The scope of a let call for a given key value is as local as possible. If called inside a function with no traditional C++ branching, the scope is contained within that function. Thus, if the function recurses, the definition corresponding to a given key value is as defined in that recursive iteration. For example, consider the following function:

QExpr recursive_let(qlist::QList qs, double angle){

  qexpr::let("rotation", qexpr::_RZ(qs[0], angle / (double)qs.size()));

  return qexpr::cIf(qs.size() > 0,
                  qexpr::get("rotation") + recursive_let(qs + 1, angle),
                  qexpr::identity()
                );
}

In the above, the _RZ angle for the key "rotation" is dependent on the size of qs as one would expect. If the function contains traditional C++ branching, then the scope of the key value is the containing branch of the code (i.e. within the same LLVM IR Basic Block). Best practice is to not use let and get with C++ branching (see Known limitations).

Printing

Since quantum kernel expressions can become quite large, especially with recursion and branching, it is often useful to check what a QExpr evaluates to without having to inspect intermediate LLVM files or the final circuit diagram. Likewise, one may also want to print messages at different points in the building of quantum logic for an evaluation call. For this, FLEQ introduces two compile-time printing functions:

QExpr qexpr::printQuantumLogic(QExpr e)

At compile-time, print out a representation of the quantum logic associated with the quantum kernel expression e, and then return e.

QExpr qexpr::printDataList(datalist::DataList d, QExpr e)

At compile-time, print out the data list d as resolved during the evaluation build (see DataList), and return the quantum kernel expression e.

Both functions return their quantum kernel expression argument e, but they trigger a compile-time message that is added to the FLEQ compilation print buffer for the argument’s evaluation call. Once that evaluation call is fully built, the print buffer is displayed. The ordering in which messages in the print buffer are displayed is a topological ordering of the print nodes in the underlying graph representation of the evaluation call (see Overview of FLEQ compilation). For example: nested calls to printDataList or printQuantumLogic will be displayed from the outside in:

qexpr::eval_hold(qexpr::printDataList("Prints First\n",
                 qexpr::printDataList("Prints Second\n",
                 qexpr::identity())));

However, when separate print functions are combined with a join, they are displayed in reverse order:

qexpr::eval_hold(qexpr::printDataList("Prints Second\n", qexpr::identity())
               + qexpr::printDataList("Prints First\n",  qexpr::identity()));

There are no guarantees on the order in which separate evaluation calls are built.

While printQuantumLogic prints a representation of the quantum logic of the passed QExpr, it does not capture the classical branching or any other classical structure, so each unique QBB attached to a given evaluation call is printed as a separate node. The representation used is that of the PCOAST graph as described in [PSI+23]; also see Overview of FLEQ compilation. The PCOAST graph is used as an intermediate representation (IR) for each generated QBB, and is synthesized into a quantum gate sequence later in IQC compilation.

A PCOAST graph is centered around Pauli operator representations of the quantum logic and as such does require some interpretation to understand. The basic features of the printed PCOAST graph for a given node are:

QBB IR name

The QBB name attached to this PCOAST graph as found in the LLVM IR. This is a means to verify the classical branching is translated appropriately, although it does require the ability of the user to read and parse the textual IR; see Debugging.

Qubit mapping

Provides the mapping of declared qubits to a numerical index.

Global Phase

The global phase associated with the contained quantum logic.

List of Elements

The list of non-Clifford PCOAST nodes in the PCOAST graph in sequential order.

End Frame

A residual Clifford unitary applied at the end of the quantum operator encoded in a Pauli frame/tableau [PSI+23].

These compile-time printing features can be turned on and off via the command-line flag, -F print=<OPT>.

OPT can be one of four options:

  • always - always print the buffer to screen for compilation failure and success

  • fail - only print the buffer on failure to compile an evaluation call

  • success - only print the buffer on successful compilation of an evaluation call

  • never - never print the buffer to screen for compilation failure or success

Exiting

Because of its functional methodology, branching in FLEQ requires all possibilities be handled, i.e. a default outcome must be explicitly defined. In many cases, one might want that default to represent an error or undesired behavior. To this end, FLEQ introduces two functions that exit and display an error message:

QExpr qexpr::exitAtCompile(datalist::Datalist err = "")

When evaluated, adds err to the print buffer and throws a compile-time error after the evaluation is built (to fully populate the print buffer) or a different failure point is found. This is understood as a failure with respects to the print flags. In the case of printQuantumLogic, an exitAtCompile node will be appear empty and will “poison” nodes which depend on it. For example, a qexpr::join between exitAtCompile and any other QExpr will also appear as empty. The only exception is qexpr::cIf where only the poisoned branch will appear as empty.

QExpr qexpr::exitAtRuntime(datalist::Datalist err = "")

Returns a QExpr that, when encountered during runtime, throws a runtime error with the error message err. exitAtRuntime will also poison nodes which depended on it up to qexpr::cIf.

Both are intended to be used in conjunction with qexpr::cIf(). The distinction between the two is when the exit is triggered. If the branching condition is not intended to be resolved by the compiler, one should use exitAtRuntime() so that a quantum runtime exit call is inserted in the appropriate branch(es) along with the passed exit message to be printed upon reaching that branch at runtime. However, if one expects said condition to be resolved by the compiler, one should use exitAtCompile.

For example, the following function compares a character against 0, 1, +, or -, and prepares a qubit in the specified state. If any other character is given as input, the function will return a compile-time error.

QExpr stateToQExpr(qbit& q, const char c) {
  return
      qexpr::cIf(c == '0', qexpr::_PrepZ(q),
      qexpr::cIf(c == '1', qexpr::_PrepZ(q) + qexpr::_X(q),
      qexpr::cIf(c == '+', qexpr::_PrepZ(q) + qexpr::_H(q),
      qexpr::cIf(c == '-', qexpr::_PrepZ(q) + qexpr::_X(q) + qexpr::_H(q),
      qexpr::exitAtCompile("Expected a character in the set {0, 1, +, -}.")
      ))));
}