Types and Casting¶
Identifiers¶
Identifiers must begin with a letter [A-Za-z], an underscore or an element from the Unicode character categories Lu/Ll/Lt/Lm/Lo/Nl [con22]. The set of permissible continuation characters consists of all members of the aforementioned character sets with the addition of decimal numerals [0-9]. Identifiers may not override a reserved identifier.
Variables¶
Variables must be named according to the rules for identifiers (See Identifiers). Variables may be assigned values within a program. Variables representing any classical type can be initialized on declaration. Any classical variable or Boolean that is not explicitly initialized is undefined. Classical types can be mutually cast to one another using the typename. See Casting specifics for more details on casting.
Declaration and initialization must be done one variable at a time for both quantum and classical
types. Comma seperated declaration/initialization (int x, y, z
) is NOT allowed for any type. For
example, to declare a set of qubits one must do
qubit q0;
qubit q1;
qubit q2;
and to declare a set of classical variables
int[32] a;
float[32] b = 5.5;
bit[3] c;
bool my_bool = false;
We use the notation s:m:f
to denote the width and precision of fixed point numbers,
where s
is the number of sign bits, m
is the number of integer bits, and f
is the
number of fractional bits. It is necessary to specify low-level
classical representations since OpenQASM operates at the intersection of
gates/analog control and digital feedback and we need to be able to
explicitly transform types to cross these boundaries. Classical types
are scoped to the braces within which they are declared.
Quantum types¶
Qubits¶
There is a quantum bit (qubit
) type that is interpreted as a reference to a
two-level subsystem of a quantum state. The statement qubit name;
declares a reference to a quantum bit. These qubits are referred
to as “virtual qubits” (in distinction to “physical qubits” on
actual hardware; see below). The statement qubit[size] name;
declares a quantum register with size
qubits.
Sizes must always be compile-time constant non-negative
integers.
Quantum registers cannot be resized after declaration.
The label name[j]
refers to a qubit of this register, where
\(j\in \{0,1,\dots,\mathrm{size}(\mathrm{name})-1\}\) is an integer.
Note
To be compliant with the base OpenQASM 3.0 specification, an implementation
is only required to allow this “quantum-register indexing” with a
compile-time constant value (those with const
types). Implementations are permitted to treat indexing into a quantum
register with a value of non-const
type as an error. Consult your
compiler and hardware documentation for details.
// Valid statements
include "stdgates.inc";
qubit[5] q1;
const uint SIZE = 4;
uint runtime_u = 2;
qubit[SIZE] q2; // Declare a 4-qubit register.
x q1[0];
z q2[SIZE - 2]; // The index operand is of type `const uint`.
// Validity is implementation-defined.
x q1[runtime_u];
// Indexing with a value with a non-`const` type (`uint`, in this case) is
// not guaranteed to be supported.
The keyword qreg
is included
for backwards compatibility and will be removed in the future.
Qubits are initially in an undefined state. A quantum reset
operation is one
way to initialize qubit states.
All qubits are global variables. Qubits cannot be declared within gates or subroutines. This simplifies OpenQASM significantly since there is no need for quantum memory management. However, it also means that users or compiler have to explicitly manage the quantum memory.
// Declare a qubit
qubit gamma;
// Declare a qubit with a Unicode name
qubit γ;
// Declare a qubit register with 20 qubits
qubit[20] qubit_array;
Physical Qubits¶
Physical qubits refer to particular hardware qubits. Therefore, they are only fully defined with respect to a target device that has a published device topology. The hardware provider determines the integer-labels associated with each qubit within the device’s topology.
While virtual qubits can be named, hardware qubits are referenced by the syntax $[NUM]
, where
NUM
is a non-negative integer. Any integer included in the published device topology is a valid
physical qubit identifier. Note that this implies that physical qubit identifier indices may be
non-consecutive, depending on the device.
Like virtual qubits, physical qubits are global variables, but unlike virtual qubits, they must not be declared.
Qubit parameters in a defcal
declaration must be physical qubits. Calibrations are valid only
for a particular set of physical qubits. See also pulse gates.
Physical qubits cannot be used in gate
statements. See also
gate definitions.
Physical qubits are also used in lower parts of the compilation stack when emitting physical
circuits. A physical circuit is one which only references physical qubits, and every operation
used in the circuit has an associated defcal
, which we refer to as hardware-native gates and
measurements.
// CNOT gate between physical qubits 0 and 1
CX $0, $1;
// Define the pulse-level instruction sequence for ``h`` on physical qubit 0
defcal h $0 { ... }
Physical qubit constraints¶
Physical qubits, by definition, reference particular hardware qubits. Circuit equivalence does not hold over permutations of physical qubit labels. Thus, physical qubits cannot be remapped by a compiler or hardware provider without opt-in from the programmer.
Note that while physical circuits require physical qubits, the converse need not be true. A circuit
that would require routing or gate decomposition to run (i.e., does not have a defcal
for every
operation in the circuit) would by definition not be a physical circuit. However, physical qubits
can still be used in such circuits.
For example, a program defines the H
gate with the gate
statement, without a corresponding
defcal
of H
. H
is therefore a supported gate, but not a hardware-native gate. The
compiler can decompose the statement H $0;
to hardware-native gates, while still respecting
strict qubit mapping.
It is possible to write a partially-constrained program with both physical and virtual qubits. Such programs are also non-physical circuits, and may or may not be supported by compilers or hardware providers.
Classical scalar types¶
Classical bits and registers¶
There is a classical bit type that takes values 0 or 1. Classical
registers are static arrays of bits. The classical registers model part
of the controller state that is exposed within the OpenQASM program. The
statement bit name;
declares a single classical bit, while bit[size] name;
declares a register of
size
bits. The label name[j]
refers to a bit of this register, where \(j\in
\{0,1,\dots,\mathrm{size}(\mathrm{name})-1\}\) is an integer.
Bit registers may also be declared as creg name[size]
. This is included for backwards
compatibility and may be removed in the future.
For convenience, classical registers can be assigned a text string containing zeros and ones of the same length as the size of the register. It is interpreted to assign each bit of the register to corresponding value 0 or 1 in the string, where the least-significant bit is on the right.
// Declare a register of 20 bits
bit[20] bit_array;
// Declare and assign a register of bits with decimal value of 15
bit[8] name = "00001111";
The scalar type bit
logically represents a single bit of data, but no restrictions or suggestions are made for how an implementation should choose to store a variable declared as a scalar bit.
The type bit[n]
behaves as if it is stored in memory as a contiguous bit-packed sequence; this is reflected in its casting rules to and from the other types.
There are no defined semantics for how bit
or bit[n]
types should behave if used for a parameter or return value in an extern
call that implies an FFI call, and it is discouraged for implementations to allow this.
Neither scalar bit
s nor bit[n]
registers can be the base type of an array
.
The indexing operation on a bit[n]
with an integer value returns a value of type bit
, and a value of type bit
can be assigned to a single integer offset into a bit[n]
type.
Values being used in an expression (r-values, in C parlance) with types bool
and (scalar) bit
are interchangeable; whenever a value can be implicitly cast to type bool
, it can also implicitly be cast to type bit
and vice versa.
For example, it is legal to initialize and set bit
values using the integer literals 0
, 1
, and the Boolean literals false
and true
.
Similarly, it is legal to use a bit
-valued expression as the condition of an if
statement.
Note
Despite having the same bit length, the types bit
and bit[1]
are distinct.
bit
is a scalar that is interchangeable with bool
, while bit[1]
is a register type of length one.
The literals false
, true
, 0
and 1
can be assigned to a value of type bit
, while the literals "0"
and "1"
are the corresponding literals for bit[1]
.
The distinction is important in the type-checking of broadcast expressions, and in the implied semantics of the type.
Integers¶
There are n-bit signed and unsigned integers. The statements int[size] name;
and uint[size] name;
declare
signed 1:n-1:0 and unsigned 0:n:0 integers of the given size. The sizes
and the surrounding brackets can be omitted (e.g. int name;
) to use
a precision that is specified by the particular target architecture.
If provided, the size
of an integer must be at least 1.
Bit-level operations cannot be used on types without a specified width, and
unspecified-width types are different to all specified-width types for
the purposes of casting.
Because register indices are integers, they
can be cast from classical registers containing measurement outcomes and
may only be known at run time. An n-bit classical register containing
bits can also be reinterpreted as an integer, and these types can be
mutually cast to one another using the type name, e.g. int[16](c)
. As noted, this
conversion will be done assuming little-endian bit ordering. The example
below demonstrates how to declare, assign and cast integer types amongst
one another.
// Declare a 32-bit unsigned integer
uint[32] my_uint = 10;
// Declare a 16 bit signed integer
int[16] my_int;
my_int = int[16](my_uint);
// Declare a machine-sized integer
int my_machine_int;
Floating point numbers¶
IEEE 754 floating point registers may be declared with float[size] name;
, where float[64]
would
indicate a standard double-precision float. Note that some hardware
vendors may not support manipulating these values at run-time.
Similar to integers, floating-point registers can be declared with an unspecified size. The resulting precision is then set by the particular target architecture, and the unspecified-width type is different to all specified-width types for the purposes of casting.
// Declare a single-precision 32-bit float
float[32] my_float = π;
// Declare a machine-precision float.
float my_machine_float = 2.3;
Void type¶
Subroutines and externs that do not return a value implicitly return void
.
The void
type is unrealizable and uninstantiable, and thus cannot be
attached to an identifier or used as a cast operator. The keyword void
is
reserved for potential future use.
Angles¶
OpenQASM 3 includes a new type to represent classical angles: angle
.
This type is intended to make manipulations of angles more efficient at runtime,
when the hardware executing the program does not have built-in support for
floating-point operations. The manipulations on angle
values are designed
to be significantly less expensive when done using integer hardware than the
equivalent software emulation of floating-point operations, by using the
equivalence of angles modulo \(2\pi\) to remove the need for large dynamic
range.
In brief, the type angle[size]
is manipulated very similarly to a single
unsigned integer, where the value 1
represents an angle of
\(2\pi/2^{\text{size}}\), and the largest representable value is
this subtracted from \(2\pi\). Addition with other angles, and
multiplication and division by unsigned integers is defined by standard
unsigned-integer arithmetic, with more details found in the section on
classical instructions.
The statement angle[size] name;
declares a new angle called
name
with size
bits in its representation, where size
must be a positive integer. Angles can be assigned
values using the constant π
or pi
, such as:
// Declare a 20-bit angle with the value of "π/2"
angle[20] my_angle = π / 2;
// Declare a machine-sized angle
angle my_machine_angle;
The bit representation of the type angle[size]
is such that if
angle_as_uint
is the integer whose representation as a uint[size]
has
the same bit pattern, the value of the angle (using exact mathematical
operations on the field of real numbers) would be
This “mathematical” value is the value used in casts from floating-point values
(if available), whereas casts to and from bit[size]
types reinterpret the
bits directly. This means that, unless a
is sufficiently small:
float[32] a;
angle[32](bit[32](uint[32](a))) != angle[32](a)
Explicitly, the most significant bit (bit index size - 1
) correpsonds to
\(\pi\), and the least significant bit (bit index 0
) corresponds to
\(2^{-\text{size} + 1}\pi\). For example, with the most-significant bit on
the left in the bitstrings:
angle[4] my_pi = π; // "1000"
angle[6] my_pi_over_two = π/2; // "010000"
angle[8] my_angle = 7 * (π / 8); // "01110000"
Angles outside the interval \([0, 2\pi)\) are represented by their values
modulo \(2\pi\). Up to this modulo operation, the closest angle[size]
representation of an exact mathematical value is different from the true value
by at most \(\epsilon\leq \pi/2^{\text{size}}\).
Complex numbers¶
Complex numbers may be declared as complex[float[size]] name
, where size
is the size of the IEEE-754 floating-point number used to store the real and
imaginary components. Each component behaves as a float[size]
type. The
designator [size]
can be omitted to use the default hardware float
, and
complex
with no arguments is a synonym for complex[float]
.
Imaginary literals are written as a decimal-integer or floating-point literal
followed by the letters im
. There may be zero or more spaces or tabs
between the numeric component and the im
component. The type of this token
is complex
(its value has zero real component), and the component type is as
normal given the floating-point literal, or the machine-size float
if the
numeric component is an integer.
The real and imaginary components of a complex number can be extracted using the
builtin functions real()
and imag()
respectively. The output types of
these functions is the component type specified in the type declaration. For
example, given a declaration complex[float[64]] c;
the output type of
imag(c)
would be float[64]
. The real()
and imag()
functions
can be used in compile-time constant expressions when called on compile-time
constant values.
complex[float[64]] c;
c = 2.5 + 3.5im;
complex[float] d = 2.0+sin(π/2) + (3.1 * 5.5 im);
float d_real = real(d); // equal to 3.0
Note
Real-world hardware may not support run-time manipulation of complex
values. Consult your hardware’s documentation to determine whether these
language features will be available at run time.
Warning
The OpenQASM 3.0 specification only directly permits complex numbers with
floating-point component types. Individually language implementations may
choose to make other component types available, but this version of the
specification prescribes no portable semantics in these cases. It is
possible that a later version of the OpenQASM specification will define
semantics for non-float
component types.
Boolean types¶
There is a Boolean type bool name;
that takes values true
or false
.
The Boolean type is byte aligned, and can be the base type of an array
.
When used in expressions, values of type bool
can always be implicitly cast to the equivalent scalar bit
, and vice versa.
The difference between bool
and bit
is primarily about the expectations of storage requirements; bit
is used when the storage is expected to be bit-packed, such as in the special register type bit[n]
.
bool
is a byte-aligned single-bit integer type.
bit my_bit = 0;
bool my_bool;
// Assign a cast bit to a boolean
my_bool = my_bit;
Compile-time constants¶
A typed declaration of a scalar type may be modified by the const
keyword,
such as const int a = 1;
. This defines a compile-time constant. Values of
type const T
may be used in all locations where a value of type T
is
valid. const
-typed values are required when specifying the widths of types
(e.g. in float[SIZE] f;
, SIZE
must have a const
unsigned integer
type). All scalar literals are const
types.
// Valid statements
const uint SIZE = 32; // Declares a compile-time unsigned integer.
qubit[SIZE] q1; // Declares a 32-qubit register called `q1`.
int[SIZE] i1; // Declares a signed integer called `i1` with 32 bits.
// Invalid statements
uint runtime_size = 32;
qubit[runtime_size] q2; // Invalid; runtime_size is not a `const` type.
int[runtime_size] i2; // Invalid for the same reason.
Identifiers whose type is const T
must be initialized, and may not be
assigned to in subsequent statements. The type of the result of the
initialization expression for a const
declaration must be const S
, where
S
is a type that is either T
or can be implicitly promoted to T
.
// Valid statements
const uint u1 = 4;
const int[8] i1 = 8;
float[64] runtime_f1 = 2.0;
const uint u2 = u1; // `u1` is of type `const uint`.
const float[32] f2 = u1; // `const uint` is implicitly promoted to `const float[32]`.
// Invalid statements
const int[64] i2 = f2; // `const float[32]` cannot be implicitly promoted to `const int[64]`.
const float[64] f3 = runtime_f1; // `runtime_f1` is not `const`.
Operator expressions, e.g. a + b
(addition), a[b]
(bit-level indexing)
and a == b
(equality), and certain built-in functions acting only on const
operands will be
evaluated at compile time. The resulting values are of type const T
, where
the type T
is the type of the result when acting on non-const
operands.
// Valid statements
const uint[8] SIZE = 5;
const uint[16] u1 = 2 * SIZE; // Compile-time value 10.
const float[64] f1 = 5.0 * SIZE; // Compile-time value 25.0.
const bit b1 = u1[1]; // Compile-time value `"1"`.
const bit[SIZE - 1] b2 = u1[0:3]; // Compile-time value `"1010"`.
The resultant type of a cast to type T
is const T
if the input value has
a type const S
, where values of type S
can be cast to type T
. If
S
cannot be cast to T
, the expression is invalid. The cast operator
does not contain the keyword const
.
// Valid statements
const float[64] f1 = 2.5;
uint[8] runtime_u = 7;
const int[8] i1 = int[8](f1); // `i1` has compile-time value 2.
const uint u1 = 2 * uint(f1); // `u1` has compile-time value 4.
// Invalid statements
const bit[2] b1 = bit[2](f1); // `float[64]` cannot be cast to `bit[2]`.
const int[16] i2 = int[16](runtime_u); // Casting runtime values is not `const`.
The resultant type of any expression involving a value that is not const
is
not const
. The output type of a call to a subroutine defined by a def
,
or a call to a subroutine linked by an extern
statement is not const
.
In these cases, values of type const T
are converted to type T
(which
has no runtime cost and no effect on the value), then evaluation continues as
usual.
// Valid statements
int[8] runtime_i1 = 4;
def f(int[8] a) -> int[8] {
return a;
}
// Invalid statements
const int[8] i2 = 2 * runtime_i1;
// Initialization expression has type `int[8]`, not `const int[8]`.
const int[8] i3 = f(runtime_i1);
// User-defined function calls do not propagate `const` values.
Built-in constants¶
Six identifiers are automatically defined in the global scope at the beginning of all OpenQASM 3 programs. There are two identifiers for each of the mathematical constants \(\pi\), \(\tau = 2\pi\) and Euler’s number \(e\). Each of these values has one ASCII-only identifier and one single-Unicode-character identifier.
Constant |
ASCII |
Unicode |
Approximate Base 10 |
---|---|---|---|
\(\pi\) |
pi |
π |
3.1415926535… |
\(\tau = 2\pi\) |
tau |
τ |
6.283185… |
Euler’s number \(e\) |
euler |
ℇ |
2.7182818284… |
Built-in constant expression functions¶
The following identifiers are compile-time functions that take const
inputs
and have a const
output. The normal implicit casting rules apply to the
inputs of these functions.
Note
These functions may not be available for use on runtime values; consult your compiler and hardware documentation for details.
Function |
Input Range/Type, […] |
Output Range/Type |
Notes |
---|---|---|---|
arccos |
|
|
Inverse cosine. |
arcsin |
|
|
Inverse sine. |
arctan |
|
|
Inverse tangent. |
ceiling |
|
|
Round to the nearest representable integer equal or greater in value. |
cos |
( |
|
Cosine. |
exp |
|
|
Exponential \(e^x\). |
floor |
|
|
Round to the nearest representable integer equal or lesser in value. |
log |
|
|
Logarithm base \(e\). |
mod |
|
|
Modulus. The remainder from the integer division of the first argument by the second argument. |
popcount |
|
|
Number of set (1) bits. |
pow |
|
|
\(\texttt{pow(a, b)} = a^b\). For floating-point and complex values, the principal value is returned. |
rotl |
|
|
Rotate the bits in the representation
of
|
rotr |
|
|
Rotate the bits in the representation
of |
sin |
( |
|
Sine. |
sqrt |
|
|
Square root. This always returns the principal root. |
tan |
( |
|
Tangent. |
For each built-in function, the chosen overload is the first one to appear in the list above where all given operands can be implicitly cast to the valid input types. The output type is not considered when choosing an overload. It is an error if there is no valid overload for a given sequence of operands.
// Valid statements.
const float[64] f1 = 2.5;
const int[8] i1 = 4;
const uint[4] u1 = 3;
const bit[8] b1 = "0010_1010";
const complex[float[64]] c1 = 1.0 + 2.0im;
const float[64] f2 = 2.0 * exp(f1);
const float[64] f3 = exp(i1);
// The ``float -> float`` overload of ``exp`` is chosen in both of these
// cases; in the first, there is an exact type match, in the second the
// ``int[8]`` input can be implicitly promoted to ``float``.
const int[8] i2 = pow(i1, u1);
// Value 64, expression has type `const int`. The first overload of `pow`
// is chosen, because `i1` can be implicitly promoted to `const int` and
// `u1` to `const uint`.
const float[64] f4 = pow(i1, -2);
// Value 0.0625, expression has type `const float`. The second,
// `(float, float) -> float`, overload is chosen, because `-2` (type
// `const int`) cannot be implicitly promoted to `const uint`, but both
// input types can be implicitly promoted to `float`. The `complex` overload
// is not attempted, because it has lower priority.
const bit[8] b2 = rotl(b1, 3);
// Value "0101_0001", expression has type `const bit[8]`.
// Invalid statements.
const complex[float[64]] c2 = mod(c1, 2);
// No valid overload is possible; the first given operand has type
// `const complex[float[64]]`, which cannot be implicitly promoted to
// `int` or `float`.
Literals¶
There are five types of literals in OpenQASM 3, integer, float, boolean,
bit string, and timing. These literals have const
types.
Integer literals can be written in decimal without a prefix, or as a hex, octal, or
binary number, as denoted by a leading 0x/0X
, 0o
, or 0b/0B
prefix.
Non-consecutive underscores _
may be inserted between the first and last
digit of the literal to improve readability for large values.
int i1 = 1; // decimal
int i2 = 0xff; // hex
int i3 = 0xffff_ffff // hex with _ for readability
int i4 = 0XBEEF; // uppercase HEX
int i5 = 0o73; // octal
int i6 = 0b1101; // binary
int i7 = 0B0110_1001; // uppercase B binary with _ for readability
int i8 = 1_000_000 // 1 million with _ for readability
- Float literals contain either
one or more digits followed by a
.
and zero or more digits,a
.
followed by one or more digits.
In addition, scientific notation can be used with a signed or unsigned integer exponent.
float f1 = 1.0;
float f2 = .1; // leading dot
float f3 = 0.; // trailing dot
float f4 = 2e10; // scientific
float f5 = 2e+1; // scientific with positive signed exponent
float f6 = 2.0E-1; // uppercase scientific with signed exponent
The two boolean literals are true
and false
.
Bit string literals are denoted by double quotes "
surrounding a number of
zero and one digits, and may include non-consecutive underscores to improve
readability for large strings.
bit[8] b1 = "00010001";
bit[8] b2 = "0001_0001"; // underscore for readability
Timing literals are float or integer literals with a unit of time.
ns, μs, us, ms, and s
are used for SI time units. dt
is a
backend-dependent unit equivalent to one waveform sample.
duration one_second = 1000ms;
duration thousand_cycles = 1000dt;
Arrays¶
Statically-sized arrays of values can be created and initialized, and individual elements can be accessed, using the following general syntax:
array[int[32], 5] myArray = {0, 1, 2, 3, 4};
array[float[32], 3, 2] multiDim = {{1.1, 1.2}, {2.1, 2.2}, {3.1, 3.2}};
int[32] firstElem = myArray[0]; // 0
int[32] lastElem = myArray[4]; // 4
int[32] alsoLastElem = myArray[-1]; // 4
float[32] firstLastElem = multiDim[0, 1]; // 1.2
float[32] lastLastElem = multiDim[2, 1]; // 3.2
float[32] alsoLastLastElem = multiDim[-1, -1]; // 3.2
myArray[4] = 10; // myArray == {0, 1, 2, 3, 10}
multiDim[0, 0] = 0.0; // multiDim == {{0.0, 1.2}, {2.1, 2.2}, {3.1, 3.2}}
multiDim[-1, 1] = 0.0; // multiDim == {{0.0, 1.2}, {2.1, 2.2}, {3.1, 0.0}}
The first argument to the array
declaration is the base type of the array.
The supported classical types include any sizes of int
, uint
, float
, complex
, and angle
, as well as bool
and duration
.
Note that bit
, bit[n]
and stretch
are not valid array base types, nor are any quantum types.
Arrays cannot be resized or reshaped. Arrays are statically typed, and cannot implicitly convert to or from any other type.
The size of an array is constant and immutable, and is recorded once, at declaration time.
Array declarations allocate memory of suitable size and alignment to accommodate the storage of its elements.
If the array declaration is direct-initialized via initializer list, its elements are initialized to the values provided by the initializer list. Otherwise, the memory allocated is not initialized, and that memory’s content is undefined.
Arrays may be passed as parameters or arguments to functions.
When an array, or slice of an array, is passed as argument to a function, the behavior accords to that described in Arrays in subroutines.
Arrays cannot be declared inside the body of a function or gate. All arrays
must be declared within the global scope of the program.
Indexing of arrays is n-based i.e., negative indices are allowed.
The index -1
means the last element of the array, -2
is the second to
last, and so on, with -n
being the first element of an n-element array.
Multi-dimensional arrays (as in the example above) are allowed, with a maximum
of 7 total dimensions. The subscript operator []
is used for element access,
and for multi-dimensional arrays subarray accesses can be specified using a
comma-delimited list of indices (e.g. myArr[1, 2, 3]
), with the outer
dimension specified first.
One or more dimension(s) of an array can be zero, in which case the array has size zero. An array of size zero cannot be indexed, e.g. given array[float[32], 0] myArray;
, it is an error to access either myArray[0]
or myArray[-1]
.
Assignment to elements of arrays, as in the examples above, acts as expected, with the left-hand side of the assignment operating as a reference, thereby updating the values inside the original array. For multi-dimensional arrays, the shape and type of the assigned value must match that of the reference.
array[int[8], 3] aa;
array[int[8], 4, 3] bb;
bb[0] = aa; // all of aa is copied to first element of bb
bb[0, 1] = aa[2] // last element of aa is copied to one element of bb
bb[0] = 1 // error - shape mismatch
Arrays may be passed to subroutines and externs. For more details, see Arrays in subroutines.
Aliasing¶
The let
keyword allows declared quantum bits and registers to be referred to by
another name as long as the alias is in scope.
qubit[5] q;
// myreg[0] refers to the qubit q[1]
let myreg = q[1:4];
Note that physical qubits are not declared and so cannot be aliased.
Index sets and slicing¶
Register concatenation and slicing¶
Two or more registers of the same type (i.e. classical or quantum) can
be concatenated to form a register of the same type whose size is the
sum of the sizes of the individual registers. The concatenated register
is a reference to the bits or qubits of the original registers. The
statement a ++ b
denotes the concatenation of registers a
and b
. A register cannot
be concatenated with any part of itself.
Classical and quantum registers can be indexed in a way that selects a subset of (qu)bits, i.e. by an index set. A register so indexed is interpreted as a register of the same type but with a different size. The register slice is a reference to the original register. A register cannot be indexed by an empty index set.
Similarly, classical arrays can be indexed using index sets. See Array concatenation and slicing.
An index set can be specified by a single integer (signed or unsigned), a
comma-separated list of integers contained in braces {a,b,c,…}
, or a range.
Ranges are written as a:b
or
a:c:b
where a
, b
, and c
are integers (signed or unsigned).
The range corresponds to the set \(\{a, a+c, a+2c, \dots, a+mc\}\)
where \(m\) is the largest integer such that \(a+mc\leq b\) if
\(c>0\) and \(a+mc\geq b\) if \(c<0\). If \(a=b\) then
the range corresponds to \(\{a\}\). Otherwise, the range is the
empty set. If \(c\) is not given, it is assumed to be one, and
\(c\) cannot be zero. Note the index sets can be defined by
variables whose values may only be known at run time.
qubit[2] one;
qubit[10] two;
// Aliased register of twelve qubits
let concatenated = one ++ two;
// First qubit in aliased qubit array
let first = concatenated[0];
// Last qubit in aliased qubit array
let last = concatenated[-1];
// Qubits zero, three and five
let qubit_selection = two[{0, 3, 5}];
// First seven qubits in aliased qubit array
let sliced = concatenated[0:6];
// Every second qubit
let every_second = concatenated[0:2:12];
// Using negative ranges to take the last 3 elements
let last_three = two[-4:-1];
// Concatenate two alias in another one
let both = sliced ++ last_three;
Classical value bit slicing¶
A subset of classical values (int, uint, and angle) may be accessed at the bit level using index sets similar to register slicing. The bit slicing operation always returns a bit array of size equal to the size of the index set.
int[32] myInt = 15; // 0xF or 0b1111
bit[1] lastBit = myInt[0]; // 1
bit[1] signBit = myInt[31]; // 0
bit[1] alsoSignBit = myInt[-1]; // 0
bit[16] evenBits = myInt[0:2:31]; // 3
bit[16] upperBits = myInt[-16:-1];
bit[16] upperReversed = myInt[-1:-16];
myInt[4:7] = "1010"; // myInt == 0xAF
Bit-level access is still possible with elements of arrays. It is suggested that
multi-dimensional access be done using the comma-delimited version of the
subscript operator to reduce confusion. With this convention nearly all
instances of multiple subscripts [][]
will be bit-level accesses of array
elements.
array[int[32], 5] intArr = {0, 1, 2, 3, 4};
// Access bit 0 of element 0 of intArr and set it to 1
intArr[0][0] = 1;
// lowest 5 bits of intArr[4] copied to b
bit[5] b = intArr[4][0:4];
Array concatenation and slicing¶
Two or more classical arrays of the same fundamental type can be concatenated to form an array of the same type whose size is the sum of the sizes of the individual arrays. Unlike with qubit registers, this operation copies the contents of the input arrays to form the new (larger) array. This means that arrays can be concatenated with themselves. However, the array concatenation operator is forbidden to be used directly in the argument list of a subroutine or extern call. If a concatenated array is to be passed to a subroutine then it should be explicitly declared and assigned the concatenation.
array[int[8], 2] first = {0, 1};
array[int[8], 3] second = {2, 3, 4};
array[int[8], 5] concat = first ++ second;
array[int[8], 4] selfConcat = first ++ first;
array[int[8], 2] secondSlice = second[1:2]; // {3, 4}
// slicing with assignment
second[1:2] = first[0:1]; // second == {2, 0, 1}
array[int[8], 4] third = {5, 6, 7, 8};
// combined slicing and concatenation
selfConcat[0:3] = first[0:1] ++ third[1:2];
// selfConcat == {0, 1, 6, 7}
subroutine_call(first ++ third) // forbidden
subroutine_call(selfConcat) // allowed
Arrays can be sliced just like quantum registers using a range a:b:c
and can be indexed using an integer but cannot be indexed by a a comma-separated
list of integers contained in braces {a,b,c,…}
. Slicing uses
the subscript operator []
, but produces an array (or reference in the case
of assignment) with the same number of dimensions as the given identifier.
Array slicing is syntactic sugar for concisely expressing for loops over
multi-dimensional arrays.
For sliced assignments, as with non-sliced assignments, the shapes and types of
the slices must match.
int[8] scalar;
array[int[8], 2] oneD;
array[int[8], 3, 2] twoD; // 3x2
array[int[8], 3, 2] anotherTwoD; // 3x2
array[int[8], 4, 3, 2] threeD; // 4x3x2
array[int[8], 2, 3, 4] anotherThreeD; // 2x3x4
threeD[0, 0, 0] = scalar; // allowed
threeD[0, 0] = oneD; // allowed
threeD[0] = twoD; // allowed
threeD[0] = oneD; // error - shape mismatch
threeD[0, 0] = scalar // error - shape mismatch
threeD = anotherThreeD // error - shape mismatch
twoD[1:2] = anotherTwoD[0:1]; // allowed
twoD[1:2, 0] = anotherTwoD[0:1, 1]; // allowed
Casting specifics¶
The classical types are divided into the ‘standard’ classical types (bool, int, uint, float, and complex) that exist in languages like C, and the ‘special’ classical types (bit, angle, duration, and stretch) that do not. The standard types follow rules that mimic those of C99 for promotion and conversion in mixed expressions and assignments.
If values with two different types are used as the operands of a binary
operation, the lesser of the two types is cast to the greater of the two. All
complex
are greater than all float
, and all complex
and all
float
are greater than all int
or uint
. Within each level of
complex
and float
, types with greater width are greater than types with
lower width. For more information, see the usual arithmetic conversions in C.
The rules for rank of integer conversions mimic those of C99. For more, see integer promotions, and integer conversions.
Standard and special classical types may only mix in expressions with operators defined for those mixed types, otherwise explicit casts must be provided, unless otherwise noted (such as for assigning float values or expressions to angles). Additionally, angle values will be implicitly promoted or converted in the same manner as unsigned integers when mixed with or assigned to angle values with differing precision.
In general, for any cast between standard types that results in loss of precision, if the source value is larger than can be represented in the target type, the exact behavior is implementation specific and must be documented by the vendor.
Allowed casts¶
Casting To |
||||||||
---|---|---|---|---|---|---|---|---|
Casting From |
bool |
int |
uint |
float |
angle |
bit |
duration |
qubit |
bool |
- |
Yes |
Yes |
Yes |
No |
Yes |
No |
No |
int |
Yes |
- |
Yes |
Yes |
No |
Yes |
No |
No |
uint |
Yes |
Yes |
- |
Yes |
No |
Yes |
No |
No |
float |
Yes |
Yes |
Yes |
- |
Yes |
No |
No |
No |
angle |
Yes |
No |
No |
No |
- |
Yes |
No |
No |
bit |
Yes |
Yes |
Yes |
No |
Yes |
- |
No |
No |
duration |
No |
No |
No |
No* |
No |
No |
- |
No |
qubit |
No |
No |
No |
No |
No |
No |
No |
- |
*Note: duration
values can be converted to float
using the division operator. See Converting duration to other types
Casting from bool¶
bool
values cast from false
to 0.0
and from true
to 1.0
or
an equivalent representation.
bool
values are interchangeable with scalar bit
values.
Because of this, a bool
can be assigned to a single index of a bit[]
register type, or an explicit cast can be used to convert a bool
into a bit[n]
value for any n
, where all bits are 0
except for the low bit, which has the same value as the Boolean.
Casting from int/uint¶
int[n]
and uint[n]
values cast to the standard types mimicking C99
behavior. Casting to bool
values follows the convention val != 0
.
As noted above, if the value is too large to be represented in the
target type the result is implementation-specific. However,
casting between int[n]
and uint[n]
is expected to preserve the bit
ordering, specifically it should be the case that x == int[n](uint[n](x))
and vice versa. Casting to bit[m]
is only allowed when m==n
. If the target
bit[]
has more or less precision, then explicit slicing syntax must be given.
As noted, the conversion is done assuming a little-endian 2’s complement
representation.
Casting from float¶
float[n]
values cast to the standard types mimicking C99 behavior (e.g.
discarding the fractional part for integer-type targets). As noted above,
if the value is too large to be represented in the
target type the result is implementation-specific.
Casting a float[n]
value to an angle[m]
involves finding the nearest
representable value modulo \(\text{float}_n(2\pi)\), where ties between two
possible representations are resolved by choosing to have zero in the
least-significant bit (i.e. round to nearest, ties to even). Casting the
floating-point values inf
, -inf
and all representations of NaN
to
angle[m]
is not defined.
For example, given the double-precision floating-point value:
// The closest double-precision representation of 2*pi.
const float[64] two_pi = 6.283185307179586
// For double precision, we have
// (two_pi * (127./512.)) / two_pi == (127./512.)
// exactly.
float[64] f = two_pi * (127. / 512.)
the result of the cast angle[8](f)
should have the bitwise representation
"01000000"
(which represents the exact angle
\(2\pi\cdot\frac{64}{256} = \frac\pi2\)), despite "00111111"
(\(2\pi\cdot\frac{63}{256}\)) being equally close, because of the
round-to-nearest ties-to-even behaviour.
Casting from angle¶
angle[n]
values cast to bool
using the convention val != 0
. Casting
to bit[m]
values is only allowed when n==m
, otherwise explicit slicing
syntax must be provided. When casting to bit[m]
, the value is a direct
copy of the bit pattern using the same little-endian ordering as described
above.
When casting between angles of differing precisions (n!=m
): if the target
has more significant bits, then the value is padded with m-n
least
significant bits of 0
; if the target has fewer significant bits, then
there are two acceptable behaviors that can be supported by compilers:
rounding and truncation. For rounding the value is rounded to the nearest
value, with ties going to the value with the even least significant bit.
Trunction is likely to have more hardware support. This behavior can be
controlled by the use of a #pragma
.
Casting from bit¶
bit[n]
values cast to bool
using the convention val != 0
. Casting to
int[m]
or uint[m]
is done assuming a little endian 2’s complement
representation, and is only allowed when n==m
, otherwise explicit slicing
syntax must be given. Likewise, bit[n]
can only be cast to angle[m]
when n==m
, in which case an exact per-bit copy is done using little-endian
bit order. Finally, casting between bits of differing precisions is not
allowed, explicit slicing syntax must be given.
The scalar bit
is implicitly interchangeable with bool
.
Converting duration to other types¶
Casting from or to duration values is not allowed, however, operations on
durations that produce values of different types is allowed. For example,
dividing a duration by a duration produces a machine-precision float
.
duration one_ns = 1ns;
duration a = 500ns;
float a_in_ns = a / one_ns; // 500.0
duration one_s = 1s;
float a_in_s = a / one_s; // 5e-7