Quale Programming Language
Quale is a programming language designed for quantum computation.
Installation
Installing quale is simple if you have a working rust tool chain. The compiler is written in Rust. You can fetch the source and directly build it yourself.
git clone https://github.com/quale-lang/quale.git
cd quale
cargo build --release
cargo install --path .
The last command cargo install will install qcc binary in your $HOME/.cargo/bin/.
Using the compiler
The help page can be viewed by using --help or -h option.
$ qcc --help
usage: qcc [options] <quale-file>
--help show this page
--print-ast print AST
--print-ast-only print AST without translating to assemmbly
--print-qasm print OpenQASM IR
--analyze run static analyzer
-O0 disable optimizations (NA)
-O1 enable first-level optimizations (NA)
-Og enable all optimizations (NA)
-d,--debug run compiler in debug-mode
-o compiled output
The options are pretty self-explanatory but the --analyze and all of optimization options aren’t available yet. They are placeholders for now. --print-* options print the requested IR on the standard output.
A Fair Coin Toss Example
We will begin our quick introduction with quale by simulating a fair coin toss. In classical computing, if we have to write a coin toss program, we will call a random number generator (RNG) over the range 0 to 1 (both inclusive). And we will assume 0 to represent heads and 1 to represent tails. But the fairness of this coin toss is dependent upon the RNG we will use. We simulate randomness in classical computing relying on entropy of the system. The more noise we can detect, the better RNG works. But in quantum computing, randomness comes in its purest form. I won’t go into the details of how it occurs. But remember that putting a qubit, which is a datatype we will use often, in superposition and subsequently measuring it can give us a purely random result. In Quale, qubit is written as qbit when defined as type, and in the following guide, when a qubit is mentioned it refers to the logical qubits, and qbit refers to the type in quale.
We will work on this problem stepwise. Firstly, the main function which usually is the entry point in many languages is back. We also start with the main function to write our code. As it happens generally, exactly one main function should be reachable by the compiler, so that it can set the entry point for execution in our resulting code.
fn main () {
return 42; // only a classical value can be returned
}
In quale, main belongs to one of the two categories of functions. It is a deterministic function, which means that it can only return a classical bit (or value). There can be functions of non-deterministic type which may return a quantum value (qubit). A non-deterministic function can also return classical value, but at least one of its parameters should be a quantum value. This category is important to consider and its necessity stems from the no-cloning theorem in quantum computing. This theorem states that it is impossible to create an independent and identical copy of a quantum state. So you can imagine, there are certain things we aren’t allowed to do in a non-deterministic function, like consistently mutating a variable state in a loop.
To keep the syntax simple, there is no language-defined keyword or attribute to distinguish between these two categories of functions. Quale’s type system, which is Peter Selinger’s type system presented in his paper [1], handles this discretion for us. This means that you can write correct code without manually ensuring no-cloning stands for quantum values as long as the compiler is happy.
Coming back to our fair coin toss example, we established that putting a qubit in superposition and measuring it will give us a fairly picked choice between 0 and 1. And similarly to classical example, we will represent 0 with heads and 1 with tails. We can write this into our main function.
fn main() {
let choice: bit = toss();
if choice == 0 {
print("Heads");
} else {
print("Tails");
}
}
toss is a non-deterministic function which will return a fair choice to us. We store the returned value in choice variable. choice is a classical type value, so we can write it as bit (or bool).
Now let us introduce a non-deterministic function in our program.
fn toss() : qbit {...}
toss is returning a qbit value (the details of the function will be expanded below). So, toss is a non-deterministic function. Also it is worth mentioning that this qubit is returned but our variable choice collapses this qubit into a bit and then stores it. This is the measurement part which we talked about earlier. The measurement is simply a collapsing of the quantum state into a classical state. Here, the measurement happens implicitly and is kept hidden away from programmer’s view point.
Note: The compiler upon seeing a mismatch where a qbit is used but a bit was expected, puts a measurement operator which takes in a qbit and returns a classical bit. This mechanism provides simplicity for programmer that they don’t have to worry about measuring explicitly. The compiler will do this job for them.
So a qubit in superposition when returned from toss, upon measurement will either give us 0 or 1 with an equal probability. But how can we put a qubit in superposition in the first place? For this we rely on the standard library for quale which provides certain standard gates which can be applied to qubits in function-like manner.
This toss function requires a two-step procedure to achieve what we aimed. First we need to create a qubit in state 0. And then we put it in superposition. Again as a matter of fact, we can put a qubit in superposition by applying Hadamard gate to it.
fn toss() : qbit {
let zero_state: qbit = 0; // represent a qubit in zero state simply as 0
let superpositioned = Hadamard(zero_state);
superpositioned
}
fn main() {
let choice: bit = toss();
if choice == 0 {
print("Heads");
} else {
print("Tails");
}
}
Hadamard is another function (technically called a gate) that takes in a qbit and returns a qbit. So its type would be Hadamard: qbit -> qbit. This will be defined in standard library of quale. We will assume it as a black box for now. The first let statement creates a qubit in state zero. We can represent 0 as either a classical bit 0 or a qubit in state zero (represented as |0〉). The type determines whether it will be a bit or a qbit. There is also a verbose way to write qubit states similar to how binaries and octals values are written. The syntax for that is 0q(α, β), where α and β are probability amplitudes for basis vectors |0〉 and |1〉. Say for a zero state qubit which has amplitude 1 for |0〉 and 0 for |1〉, we write 0q(1, 0).
The second let calls the Hadamard function and receives a qubit in superposition. Notice that, superpositioned is inferred to be of qbit type (toss’s return type is qbit and superpositioned is returned), so there wouldn’t be a measurement operator here.
This pretty much covers the program for a fair coin toss. We create a qubit in state 0 and then put that qubit in superposition, followed by a measurement of this superpositioned qubit which resulted in a 0 or 1 with exactly 0.5 probability.
Put the above code into a file toss.ql and you can dump its AST as qcc --print-ast toss.ql. This command will print the AST on stdout and also create a .s file in the same place. This .s file is the readable format of OpenQASM IR.