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 anangle
type of the same size. Bothuint * angle
andangle * uint
are valid and produce the same result, but onlyangle / uint
is valid; it is not allowed to divide an integer by an angle.Division
/
by another angle of the same size. This returns auint
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)
).
Operator |
Operator names |
Associativity |
---|---|---|
|
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 typetype
.a range expression in square brackets of the form
[start : (step :)? stop]
, wherestep
is equal to1
if omitted. As in other range expressions, the range is inclusive at both ends. Bothstart
andstop
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 betweenstart
andstop
. For example, ifstart
is auint[8]
andstop
is anint[16]
, the values to be assigned will all be of typeint[16]
.a value of type
bit[n]
, or the target of alet
statement that creates an alias to classical bits. The corresponding scalar type of the loop variable isbit
, as appropriate.a value of type
array[<scalar>, n]
, _i.e._ a one-dimensional array. Values of typescalar
must be able to be implicitly promoted to values of typetype
. 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:
A simple
switch
statement withcase
anddefault
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)
}
}
A
switch
where the cases areconst
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)
}
}
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)
}
}
A
switch
statement containing declarations atcase
statement scope, and a function call, also atcase
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 {
}
}
A
switch
statement containing a nestedswitch
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.