Classical instructions

We envision two levels of classical control: simple, low-level instructions embedded as part of a quantum circuit and high-level external functions which perform more complex classical computations. The low-level functions allow basic computations on lower-level parallel control processors. These instructions are likely to have known durations and many such instructions might be executed within the qubit coherence time. The external, or extern, functions execute complex blocks of classical code that may be neither fast nor guaranteed to return. In order to connect with classical compilation infrastucture, extern functions are defined outside of OpenQASM. The compiler toolchain is expected to link extern functions when building an executable. This strategy allows the programmer to use existing libraries without porting them into OpenQASM. extern functions run on a global processor concurrently with operations on local processors, if possible. extern functions can write to the global controller’s memory, which may not be directly accessible by the local controllers.

Low-level classical instructions

Generalities

All types support the assignment operator =. The left-hand-side (LHS) and right-hand-side (RHS) of the assignment operator must be of the same type. For real-time values assignment is by copy of the RHS value to the assigned variable on the LHS.

int[32] a;
int[32] b = 10; // Combined declaration and assignment

a = b; // Assign b to a
b = 0;
a == b; // False
a == 10; // True

Classical bits and registers

Classical registers, bit, uint and angle support bitwise operators and the corresponding assignment operators with registers of the same size: and &, or |, xor ^. They support left shift << and right shift >> by an unsigned integer, and the corresponding assignment operators. The shift operators shift bits off the end. They also support bitwise negation ~, popcount [1], and left and right circular shift, rotl and rotr, respectively.

bit[8] a = "10001111";
bit[8] b = "01110000";

a << 1; // Bit shift left produces "00011110"
rotl(a, 2) // Produces "00111110"
a | b; // Produces "11111111"
a & b; // Produces "00000000"

For uint and angle, the results of these operations are defined as if the operations were applied to their defined bit representations:

angle[4] a = 9 * (pi / 8);  // "1001"
a << 2;  // Produces pi/2, which is "0100"
a >> 2;  // Produces pi/4, which is "0010"

uint[6] b = 37;  // "100101"
popcount(b);  // Produces 3.
rotl(b, 3);   // Produces 44, which is "101100"

Comparison (Boolean) Instructions

Integers, angles, bits, and classical registers can be compared (\(>\), \(>=\), \(<\), \(<=\), \(==\), \(!=\)) and yield Boolean values. Boolean values support logical operators: and &&, or ||, not !. The keyword in tests if an integer belongs to an index set, for example i in {0,3} returns true if i equals 0 or 3 and false otherwise.

bool a = false;
int[32] b = 1;
angle[32] d = pi;
float[32] e = pi;

a == false; // True
a == bool(b); // False
c >= b; // True
d == pi; // True
// Susceptible to floating point casting errors
e == float(d);

Note

Angles are naturally defined on a ring, and so comparisons may not work exactly how you might expect. For example, 2 * ang >= ang is not necessarily true; if ang represents the angle \(3\pi/2\), then 2*ang == pi and pi < ang.

This is the same behavior as unsigned integers in many languages (including OpenQASM 3), but the types of operation commonly performed on angle are particularly likely to trigger these modulo arithmetic effects.

Integers

Integer types support addition +, subtraction -, multiplication *, integer division [2] /, modulo %, and power **, as well as the corresponding assignments +=, -=, *=, /=, %=, and **=.

int[32] a = 2;
int[32] b = 3;

a * b; // 6
b / a; // 1
b % a; // 1
a ** b; // 8
a += 4; // a == 6

Angles

In addition to the bitwise operations mentioned above, angles support:

  • Addition + and subtraction - by other angles of the same size, which returns an angle of the same size.

  • Multiplication * and division / by unsigned integers of the same size. The result is an angle type of the same size. Both uint * angle and angle * uint are valid and produce the same result, but only angle / uint is valid; it is not allowed to divide an integer by an angle.

  • Division / by another angle of the same size. This returns a uint of the same size.

  • Unary negation -, which represents the mathematical operation \(-a \equiv 2\pi - a\).

  • Compound assignment operators +=, -= and /= with angles of the same size as both left- and right operands. These have the same effect as if the equivalent binary operation had been written out in full.

  • The compound assignment operators *= and /= with an unsigned integer of the same size as the right operand. This has the same effect as if the multiplication or division had been written as a binary operation and assigned.

In all of these cases, except for unary negation, the bit pattern of the result of these operations is the same as if the operations had been carried out between two uint types of the same size with the same bit representations, including both upper and lower overflow. Explicitly:

angle[4] a = 7 * (pi / 8);  // "0111"
angle[4] b = pi / 8;        // "0001"
angle[4] c = 5 * (pi / 4);  // "1010"
uint[4] two = 2;

a + b;    // angle[4] │ pi           │ "1000"
b - a;    // angle[4] │ 5 * (pi / 4) │ "1010"
a / two;  // angle[4] │ 3 * (pi / 8) │ "0011"
two * c;  // angle[4] │ pi / 2       │ "0100"
c / b;    // uint[4]  │ 10           │ "1010"
pi * 2;   // angle[4] │ 0            │ "0000"

Unary negation of an angle a is defined to produce the same value as 0 - a, such that a + (-a) is always equal to zero. This is the same as the C99 definition for unsigned integers. In bitwise operations, the negation can be written as (~a) + 1. Explicitly:

angle[4] a = pi / 4;  // "0010"
angle[4] b = -a;  // 7*(pi/4) │ "1110"

Floating-point numbers

Floating-point numbers support addition, subtraction, multiplication, division, and power and the corresponding assignment operators.

angle[20] a = pi / 2;
angle[20] b = pi;
a + b; // 3/2 * pi
a ** b; // 4.1316...
angle[10] c;
c = angle(a + b); // cast to angle[10]

Note

Real hardware may well not have access to floating-point operations at runtime. OpenQASM 3 compilers may reject programs that require runtime operations on these values if the target backend does not support them.

Complex numbers

Complex numbers support addition, subtraction, multiplication, division, power and the corresponding assignment operators. These binary operators follow analogous semantics to those described in Annex G (section G.5) of the C99 specification (note that OpenQASM 3.0 has no imaginary type, only complex). These operations use the floating-point semantics of the underlying component floating-point types, including their NaN propagation, and hardware-dependent rounding mode and subnormal handling.

complex[float[64]] a = 10.0 + 5.0im;
complex[float[64]] b = -2.0 - 7.0im;
complex[float[64]] c = a + b;   // c = 8.0 - 2.0im
complex[float[64]] d = a - b;   // d = 12.0+12.0im;
complex[float[64]] e = a * b;   // e = 15.0-80.0im;
complex[float[64]] f = a / b;   // f = (-55.0+60.0im)/53.0
complex[float[64]] g = a ** b;  // g = (0.10694695640729072+0.17536481119721312im)

Evaluation order

OpenQASM evaluates expressions in natural mathematical order, following the defined operator-precedence and -associativity table below. Operators of greater precedence are evaluated before operators of less precedence. The order of evaluation for operators of the same precedence is set by the associativity: left-associative operators evaluate from left to right (i.e. a + b + c evaluates as (a + b) + c) while right-associative operators evaluate from right to left (i.e. a ** b ** c evaluates as a ** (b ** c)).

Table 3 [operator-precedence] operator precedence in OpenQASM ordered from highest precedence to lowest precedence. Higher precedence operators will be evaluated first.

Operator

Operator names

Associativity

(), [], (type)(x)

Call, index, cast

left

**

Power

right

!, -, ~

Unary

right

*, /, %

Multiplicative

left

+, -

Additive

left

<<, >>

Bit Shift

left

<, <=, >, >=

Comparison

left

!=, ==

Equality

left

&

Bitwise AND

left

^

Bitwise XOR

left

|

Bitwise OR

left

&&

Logical AND

left

||

Logical OR

left

Looping and branching

If-else statements

The statement if ( bool ) <true-body> branches to program if the Boolean evaluates to true and may optionally be followed by else <false-body>. Both true-body and false-body can be a single statement terminated by a semicolon, or a program block of several statements { stmt1; stmt2; }.

bool target = false;
qubit a;
h a;
bit output = measure qubit

// example of branching
if (target == output) {
   // do something
} else {
   // do something else
}

For loops

The statement for <type> <name> in <values> <body> loops over the items in values, assigning each value to the variable name in subsequent iterations of the loop body. values can be:

  • a discrete set of scalar types, defined using the array-literal syntax, such as {1, 2, 3}. Each value in the set must be able to be implicitly promoted to the type type.

  • a range expression in square brackets of the form [start : (step :)? stop], where step is equal to 1 if omitted. As in other range expressions, the range is inclusive at both ends. Both start and stop must be given. All three values must be of integer or unsigned-integer types. The scalar type of elements in the resulting range expression is the same as the type of result of the implicit promotion between start and stop. For example, if start is a uint[8] and stop is an int[16], the values to be assigned will all be of type int[16].

  • a value of type bit[n], or the target of a let statement that creates an alias to classical bits. The corresponding scalar type of the loop variable is bit, as appropriate.

  • a value of type array[<scalar>, n], i.e. a one-dimensional array. Values of type scalar must be able to be implicitly promoted to values of type type. Modification of the loop variable does not change the corresponding value in the array.

It is valid to use an indexing expression (e.g. my_array[1:3]) to arrive at one of the types given above. In the cases of sets, bit[n], classical aliases and array, the iteration order is guaranteed to be in sequential index order, that is iden[0] then iden[1], and so on.

The loop body can either be a single statement terminated by a semicolon, or a program block in curly braces {} containing several statements.

Assigning a value to the loop variable within an iteration over the body does not affect the next value that the loop variable will take.

The scope of the loop variable is limited to the body of the loop. It is not accessible after the loop.

int[32] b = 0;
// loop over a discrete set of values
for int[32] i in {1, 5, 10} {
    b += i;
}
// b == 16, and i is not in scope.

// loop over every even integer from 0 to 20 using a range, and call a
// subroutine with that value.
for int i in [0:2:20]
   subroutine(i);

// high precision typed loop variable
for uint[64] i in [4294967296:4294967306] {
   // do something
}

// Loop over an array of floats.
array[float[64], 4] my_floats = {1.2, -3.4, 0.5, 9.8};
for float[64] f in my_floats {
   // do something with 'f'
}

// Loop over a register of bits.
bit[5] register;
for bit b in register {}
let alias = register[1:3];
for bit b in alias {}

While loops

The statement while ( bool ) <body> executes program until the Boolean evaluates to false [3]. Variables in the loop condition statement may be modified within the while loop body. The body can be either a single statement terminated by a semicolon, or a program block in curly braces {} of several statements:

qubit q;
bit result;

int i = 0;
// Keep applying hadamards and measuring a qubit
// until 10, |1>s are measured
while (i < 10) {
    h q;
    result = measure q;
    if (result) {
        i += 1;
    }
}

Breaking and continuing loops

The statement break; moves control to the statement immediately following the closest containing for or while loop.

The statement continue; causes execution to jump to the next step in the closest containing for or while loop. In a while loop, this point is the evaluation of the loop condition. In a for loop, this is the assignment of the next value of the loop variable, or the end of the loop if the current value is the last in the set.

int[32] i = 0;

while (i < 10) {
    i += 1;
    // continue to next loop iteration
    if (i == 2) {
        continue;
    }

    // some program

    // break out of loop
    if (i == 4) {
        break;
    }

    // more program
}

It is an error to have a break; or continue; statement outside a loop, such as at the top level of the main circuit or of a subroutine.

OPENQASM 3.0;

break;  // Invalid: no containing loop.

def fn() {
   continue; // Invalid: no containing loop.
}

Terminating the program early

The statement end; immediately terminates the program, no matter what scope it is called from.

The Switch statement

A switch statement is a form of flow control that provides for a predicated selection of zero, one or more statements to be executed based on a discriminating controlling value. The discriminating controlling value can be either explicit - as it is the case for case statements - or none of the above - which is the case for default statements.

A switch statement is not a loop. It does not iterate over a sequence of values.

switch statements may appear anywhere in a program where statements are allowed.

An OpenQASM3 switch statement shall use the following keywords:

  • switch

  • case

  • default

An OpenQASM3 switch statement shall be the following grammar:

  • The switch keyword.

  • A right paren ( literal.

  • A controlling expression.

  • A left paren ) literal.

  • A left brace { literal.

  • A sequence of one or more case statements (defined below).

  • Either zero or one default statement(s) (defined below).

  • A right brace } literal.

The controlling expression of a switch statement shall be of integer type. Implicit conversions to an integer type are not allowed.

A case statement shall be the following grammar:

  • The case keyword.

  • An integer-constant-list-expression controlling label.

  • A left-brace literal: {.

  • A sequence of zero, one or more OpenQASM3 statements.

  • A right-brace literal: }.

The integer-constant-list-expression is a sequence of one or more integer const expressions separated by comma , literals.

A default statement shall be the following grammar:

  • The default keyword.

  • A left-brace literal: {.

  • A sequence of zero, one or more OpenQASM3 statements.

  • A right-brace literal: }.

A switch statement shall be in scope only within the scope where it is defined.

The left and right braces of a switch statement shall not create brace-enclosed scope.

Declarations or statements at switch statement scope but outside of a case or default statement are ill-formed. The compiler shall raise an error diagnostic for such cases.

A case or default statement creates brace-enclosed scope.

Declarations of types that automatically acquire global scope in OpenQASM3 - such as gates, functions, arrays, qubits and defcals - are not allowed at case or default switch statement scope. Use of such declarations is ill-formed and requires a compiler diagnostic.

Duplicate values within any integer-constant-list-expression for controlling labels of case statements are not allowed. The compiler shall issue an error diagnostic in such cases.

A case or default statement ending with a right-brace } terminates the execution of the switch statement. After executing all the statements of the case or default statement, control is then transferred to the first statement following the closing right brace of the enclosing switch statement.

A switch statement shall contain at least one case statement. A switch statement with no case statements shall raise an error diagnostic.

A switch statement is not required to contain a default statement. If a switch statement does not contain a default statement and a runtime value is provided to the controlling expression that does not match any case, then the switch becomes effectively a no-op.

Examples:

  1. A simple switch statement with case and default statements:

OPENQASM 3.0;

int i = 15;

switch (i) {
case 1, 3, 5 {
  // OpenQASM3 statement(s)
}
case 2, 4, 6 {
  // OpenQASM3 statement(s)
}
case -1 {
  // OpenQASM3 statement(s)
}
default {
  // OpenQASM3 statement(s)
}
}
  1. A switch where the cases are const expressions:

OPENQASM 3.0;

const int A = 0;
const int B = 1;
int i = 15;

switch (i) {
case A {
  // OpenQASM3 statement(s)
}
case B {
  // OpenQASM3 statement(s)
}
case B+1 {
  // OpenQASM3 statement(s)
}
default {
  // OpenQASM3 statement(s)
}
}
  1. A switch statement with binary literals in the case statements:

OPENQASM 3.0;

bit[2] b;
switch (int(b)) {
case 0b00 {
  // OpenQASM3 statement(s)
}
case 0b01 {
  // OpenQASM3 statement(s)
}
case 0b10 {
  // OpenQASM3 statement(s)
}
case 0b11 {
  // OpenQASM3 statement(s)
}
}
  1. A switch statement containing declarations at case statement scope, and a function call, also at case statement scope:

OPENQASM 3.0;

def foo(int i, qubit[8] d) -> bit {
  return measure d[i];
}

int i = 15;

int j = 1;
int k = 2;

bit c1;

qubit[8] q0;

switch (i) {
case 1 {
  j = k + foo(k, q0);
}
case 2 {
  float[64] d = j / k;
}
case 3 {
}
default {
}
}
  1. A switch statement containing a nested switch statement.

OPENQASM 3.0;

def foo(qubit[8] q) -> int {
  int r = 0;
  bit k;

  for int i in [0 : 7] {
        k = measure q[i];
        r += k;
  }

  return r;
}

qubit[8] q;

int j = 30;
int i = foo(q);

switch (i) {
case 1, 2, 5, 12 {
}
case 3 {
  switch (j) {
  case 10, 15, 20 {
    h q;
  }
}
}

Extern function calls

extern functions are declared by giving their signature using the statement extern name(inputs) -> output; where inputs is a comma-separated list of type names and output is a single type name. The parentheses may be omitted if there are no inputs.

extern functions can take of any number of arguments whose types correspond to the classical types of OpenQASM. Inputs are passed by value. They can return zero or one value whose type is any classical type in OpenQASM except real constants. If necessary, multiple return values can be accommodated by concatenating registers. The type and size of each argument must be known at compile time to define data flow and enable scheduling. We do not address issues such as how the extern functions are defined and registered.

extern functions are invoked using the statement name(inputs); and the result may be assigned to output as needed via an assignment operator (=, +=, etc). inputs are literals and output is a variable, corresponding to the types in the signature. The functions are not required to be idempotent. They may change the state of the process providing the function. In our computational model, extern functions may run concurrently with other classical and quantum computations. That is, invoking an extern function will schedule a classical computation, but does not wait for that computation to terminate.