isQ Programming Language
This section mainly introduces isQ's grammar which is divided into the following parts:
- Package
- Import
- Types
- Classical Operation
- Quantum Operation
- Procedure
- Control Flow
- Modifier
- Oracle
- Parameter
Package
Each isQ file belongs to a package. The root of the package is specified with keyword package
at the begining of the file. For example, the first lines of /some/path/aaa/bbb/aaa/ccc.isq
are
/some/path/aaa/bbb/aaa
. The root is matched with file or folder name in the abosolute path of the isQ file, from lower to higher hierachies. In this example, there are two folders of the name aaa
. The root is the one closer to the file. If the specified package name cannot match any file or folder name, the compiler will report an error. If no package is specified, the default package name is the file name.
The global variables and procedures can be referred to with qualified names from another isQ file, after being imported. Continue with the previous example,
The global variableg
can be referred to as aaa.ccc.g
. This qulified name is constructed with the relative path from the package root (replacing /
with .
) plus the variable name.
Import
An isQ file can use the variables and procedures defined in another isQ file by importing it. The syntax is like this example:
// /some/path/aaa/bbb/ddd/eee.isq
package ddd;
import aaa.ccc; // /some/path/aaa/bbb/aaa/ccc.isq
import std; // ${ISQ_ROOT}/lib/std.isq
/some/path/aaa/bbb/ddd
which is placed in the same folder (i.e., /some/path/aaa/bbb/
) as the example in the Package part. It specifies the imported file with the relative path from the parent folder of the root, replacing /
with .
and omiting the .isq
suffix. Therefore, the above line imports /some/path/aaa/bbb/aaa/ccc.isq
. The user can also uses the relative path from the isQ library, which is specified by environmental variable ${ISQ_ROOT}/lib
. A standard library file, std.isq
, is placed in this folder. It contains the defination of basic gates such as H and X. Therefore, it should be imported by nearly all the isQ files.
Once a file is imported, its global variables and procedures (except main
) can be used directly. For example, the eee.isq
mentioned above can use g
direclty in its program. However, if multiple variables that are defined in different imported files share the same name, they have to be referred to using qualified names, e.g., aaa.ccc.g
and some.other.g
.
Types
primitive types
isQ mainly supports four primitive types:
- int: a 64-bit signed integer, e.g. -1, 1;
- bool: a boolean value that can be either true or false;
- double: a double precision floating point number, e.g. -0.1, 3.1415926;
- unit: the return type of a procedure that returns nothing;
- qbit: an opaque type that represents a quantum bit;
Users can use these type to define variables anywhere. All qubits are set to default value |0>. For example, we could define variables like:
// global variable
int a, b = 4; // int variables
double d = pi; // double variable with initial value 3.14159...
qbit q; // qbit
procedure main() {
// local variable with initial value 3
int c = 3;
...
}
array
All the four primitive type supports one-dimensional array. When the array is defined with intialization values, no length should be provided. Otherwise, a length must be provided. The length of a global array must be a positive integer, while for a local variable, any int expression can be used as a length. The length of an array can be obtained using the operator .length.
qbit q[3]; // the length of a global array must be a positive integer
unit main() {
// when an array is initilized, the length should not be specified
int a[] = [1, 2];
// a local variable may use expressions as length
int b[a.length];
...
}
A slice of an array can be obtained by appending a [start:end:step]
structure after the array. It is a subview of the original array, starting from the start
position, increasing with step
, and till the end
(excluding). For example, a[2:4:1]
refers to a[2]
and a[3]
. The three parts can be omitted, and the default values will be used. The default values of start
, end
, and step
are 0, the length of the array, and 1, respectively. When the step
is omitted, the second :
can be omitted as well. For example, a[:2]
represents the first two elements of a
. The step
can be negtive integers. In that case, any field cannot be omitted. For example, a[2:0:-1]
refers to a[2]
and a[1]
.
Classical Operation
isQ supports the following operators for classical types:
- Arithmetic operators: +, -, *, /, ** (power), %
- Comparison operators: ==, !=, >, <, >=, <=
- Logical operators: &&, and, ||, or, !, not
- Bitwise operators: &, |||, ^(XOR)
- Shift operators: >>, <<
Their semantics, operand types and precedence are consistent with common programming languages such as C and Python.
isQ has built-in automatic type conversion, which can convert bool to int and int to double. Specifically, true is converted to 1 and false to 0. Moreover, isQ provides a print command to print int and double values. Users can do arithmetic and print like this:
In addition, isQ provides an assert command, which operates with a bool value. If the value evaluates true during runtime, this command is omitted. Otherwise, the program aborts and reports this event. For example,
Note that putting paratheses around a bool value results in the same bool value.Quantum Operation
The quantum operation in isQ is simple, users can apply a gate or do measurement on qubits in a way similar to function call. All quantum operations are inside procedures and all qubits used are defined beforehand. When doing measurement, the result must be stored in an int variable.
basic operation
isQ supports some basic gate: X, Y, Z, H, S, T, Rx, Ry, Rz, CNOT, Toffoli, U3 (see this link for details), and two non-unitary operation: M(measure), |0>(set qubit to |0>). Users can directly use these gates like this:
qbit
array and slice can be used as the parameters of quantum gates and measurement. In this case, it represents applying the gates or measurement to all the qubits in the array or slice. For multiple-qubit gates, it means applying gates to each group of qubits. The number of gates depends on the shortest qbit
array or slice. For example:
qbit p[3], q[3];
H(p); // = H(p[0]); H(p[1]); H(p[2]);
CNOT(p, q[:2]); // = CNOT(p[0], q[0]); CNOT(p[1], q[1]);
int x = M(q);
The measurement result of a qbit
array or slice is an int
value where the measurement result of the first qbit
is used as the lowest-order bit. In the previous example, if the measurement result of q[0]
, q[1]
, and q[2]
are true
, true
, and false
, respectively, x
would be 3 (i.e., 011
).
defgate
isQ allows users to define gate by using keyword defgate and a matrix. An element in the matrix can be an int/double/complex value or an arithmetic expression. Complex value can be written in a way similar to Python. There are three points to note:
- The user-defined gate should be unitary and its size should be a power of 2.
- Users should define gate outside procedures and then use it inside.
- A user-defined gate's name can not conflict with the names of built-in gates
For example, we can define a 2-qubit gate and use it like this:
// define gate Rs
defgate Rs = [
0.5+0.8660254j,0,0,0;
0,1,0,0;
0,0,1,0;
0,0,0,1
];
qbit q[2];
unit main() {
// apply Rs on q;
Rs(q[0], q[1]);
}
If all the non-zero elements in the unitary is 1, i.e., the gate maps compuatational bases to a permutation of compuatational basis, you can use a simplified version with keyword perm. For example, the Toffoli gate can be defined as
The syntax means that the 3-qubit gate Tof
maps $|i\rangle$ to $|i\rangle$ for $0\leq i\leq5$, $|6\rangle$ to $|7\rangle$, and $|7\rangle$ to $|6\rangle$.
quantum state preparation
You can prepare a quantum state using an assignment. The left side of the assignment should be a qbit
array with a fixed length. The right side can be two forms:
-
A complex number array that represents the amplitude of the desired state. This array will be padded with 0 if its length is smaller than the Hilbert space dimension. After that, the amplitudes will be normalized.
-
An expression consisting of kets such as
(0.5+0.5j)|2>
. Only computational bases are supported currently.
import std;
procedure main()
{
qbit q[2];
// Initialize q in a Bell state using an array
q = [1, 0, 0, -1];
// or using ket expressions.
q = |0> - |3>;
}
Procedure
The main body of isQ program is composed of procedures, which is similar to functions in C. The entry procedure is named main, which has no paramters and output.
Users can define their own procedures, and procedures may have no output or return with classical type. When a procedure has no output, it uses keyword procedure or unit at the beginning of definition, otherwise, it uses classical type. User-procedure can has parameters with any type mentioned above, and can be called by others or by itself.
// proceduer with two qbit as paramters and has no return
// paramtesr can also be written like (a: qbit, b: qbit)
unit swap(qbit a, qbit b) {
...
}
// proceduer with two classical paramters and return with a double
double compute(int a, double b[]) {
double s = 0.0;
...
return s;
}
// entry procedure
unit main() {
qbit q[2];
// call swap
swap(q[0], q[1]);
double b[2];
// call compute
double c = compute(1, b);
}
In addition, procedure itself can also serve as a parameter to another procedure. Such a parameter is specified as returnType identifier(TypeList), in which returnType and TypeList are the return type and the argument types of the procedure. For example,
// proceduer with two qubits and a two-qubit-gate procedure as paramters and has no return.
// parameters can also be written like (a: qbit, b: qbit, two_bit_gate: (qbit, qbit)->unit)
unit circuit(qbit a, qbit b, unit two_bit_gate(qbit, qbit)) {
H(a);
H(b);
two_bit_gate(a, b);
}
// two qubit gate procedure1
unit two_bit_1(qbit a, qbit b){
...
}
// two qubit gate procedure2
unit two_bit_2(qbit a, qbit b){
...
}
// entry procedure
unit main() {
qbit q[2];
// call circuit with two_bit_1
circuit(q[0], q[1], two_bit_1);
// call circuit with two_bit_2
circuit(q[0], q[1], two_bit_2);
}
deriving
User-procedure can be purely quantum. Such kind of procedures work as gates, and we can do some operations on these procedures like on ordinary quantum gates, like modifiers.
isQ provides a keyword deriving gate, converting a procedure to quantum gate, so that user can add modifiers when calling procedure. For example, if we need a controlled-swap, we can write codes like:
// pure quantum procedure that swap two qbit
unit swap(qbit a, qbit b) {
CNOT(b, a);
CNOT(a, b);
CNOT(b, a);
} deriving gate
unit main() {
qbit q[3];
// set |q> -> |110>
X(q[0]);
X(q[1]);
// controlled-swap, set |q> -> |101>
ctrl swap(q[0], q[1], q[2])
}
When a qbit
procedure argument is of an arry type, its length must be fixed. For example, qbit a[3]
is legal but qbit a[]
is not.
template
A template represents a group of procedures. The syntax for defining and using a template is shown in the following example:
Note that expressions with operators must be wrapped with parenthesis ()
.
A normal procedure is generated based on the instantiation value before other compilation processes. Therefore, it may bypass some limitations of deriving gate, e.g.,
In addition, we can define some procedures for instantiating a template with particular values. This feature is helpful in defining the base case of recursive templates. For example, the factorial function can be implemented as follows.
When calling factorial::<0>()
, the second procedure will be used instead of the first.
Control Flow
isQ provides four kinds of classical control flow:
- if-statement
- for-loop
- while-loop
- switch-case
if-else
If-else statement in isQ is similar to that in C. However, the braces {...} cannot be omitted, even if there is single statement following the condition.
unit main() {
int a = 1;
int b;
if (a > 0) {
b = 1;
} else {
b = -1;
}
bool c = true;
if (c) {
b = 2;
}
}
for-loop
For-loop in isQ is similar to that in python. User can uses a non-defined variable as the iterating variable, and use two integer value a, b as loop range [a, b), where a must be less than b. The step size is optional and defaulted to 1. Like for-loop in other languages, keywords break and continue are supported.
unit main() {
int a = 0;
for i in 0:10 {
a = a + 1;
if (a > 5) {
break;
}
}
for j in 0:10:3 {
a = a - 1;
}
}
isQ also supports array iteration. For example,
Printed results should be "2", "3", and "4".while-loop
While-loop in isQ is similar to that in C. However, the braces {...} cannot be omitted, even if there is single statement following the condition.
switch-case
Based on the value of an int
-type variable, a switch-case selects a correspoding branch. The execution terminates before another case
or default
keyword. For example,
b
would be 3, not 5. The switch-case statement does not support break
as it is not needed.
Modifiers
Controlled gates are common in quantum circuit. As we introduced before, users can define gate, so theoretically, any controlled gate can be defined. But it requires users to calculate the whole matrix. To simplify this work, isQ provide gate modifiers like some other quantum programming languages.
There are three gate modifiers in isQ:
- ctrl: modify gate to controlled-gate, can has a paramter representing the number of control qubits(defualt is 1).
- nctrl: modify gate to neg-controlled-gate, can has a paramter representing the number of control qubits(defualt is 1).
- inv: modify gate to conjugate transpose.
Controlled gates can be easily defined by ctrl and nctrl. And of course, you can compound these modifiers. For example:
unit main() {
qbit p, q, r;
// p ---●---
// |
// q ---S---
ctrl S(p, q);
// p ---●---
// |
// q ---●---
// |
// r ---S---
ctrl<2> S(p, q, r);
// q ---S^{-1}---
inv S(q);
// p --X--●--X---
// |
// q ---S^{-1}---
nctrl inv S(p, q);
}
When a group of gates share the same control qubits, sometimes it is better to use a switch-case statement. For example
It is equivalent to the following statements nctrl nctrl H(q[1], q[0], p);
nctrl ctrl H(q[1], q[0], p);
ctrl nctrl H(q[1], q[0], p);
ctrl ctrl Rx(pi, q[1], q[0], p);
Oracle
value table
isQ supports simple quantum oracle definitions. For those simple oracles whose function like $f: \{0,1\}^n \rightarrow \{0,1\}^m$, keyword oracle can be used like this:
In the above example, g is the oracle name, and there are two integer parameters n, m, where n represents the number of work qubits and m represents the number of ancilla qubits. The value list is ${f(i)} {\ } for {\ } i {\ } in [0, 2^n) $, and every $f(i)$ must be in $[0, 2^m)$. Usually, $m$ is 1, and the list is a truth table like above.
oracle function
Alternatively, you can define an oracle using a Boolean function. This function accepts one or more parameters with bool array type, and returns a bool array. For example, we can rewrite the oracle g as follows:
The derived quantum gate applies to qbit arrays. For example:
Parameter
isQ supports compiling with parameters and passing values at runtime. If you want to do this, you can define two arrays in the parameter list of the procedure main
, and you can use them in the function body, like this:
When simulating, you can pass values by -i
or -d
. isQ will gather values and generate int and double arrays (can be empty). For the above example, you can pass values like this: