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 from left to right.
¶ Operator
Operator Types
()
,[]
,(type)(x)
Call, index, cast
**
Power
!
,-
,~
Unary
*
,/
,%
Multiplicative
+
,-
Additive
<<
,>>
Bit Shift
<
,<=
,>
,>=
Comparison
!=
,==
Equality
&
Bitwise AND
^
Bitwise XOR
|
Bitwise OR
&&
Logical AND
||
Logical OR
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 b in register {}
let alias = register[1:3];
for 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.
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.
Further reserved keywords¶
The keywords switch
, case
and default
are reserved for future
expansion of the language. These words are not valid identifiers.