Compilation
As was shown previously, the compilation of source programs can be achieved in pipelines by
using QAT.compile()
. This would retrieve a pipeline and
use its various modules to compile the source program. Loosely speaking, these modules can
be separated into three different stages: the frontend, middleend and backend.
The source code enters the front end, which compiles it to QAT’s intermediate representation
(IR). This enters the middle end, which analyses, validates and optimises it. Finally,
the modified QAT IR enters the backend, which generates native code for the target device.

Frontend
The frontend describes the front-facing part of the compiler. It’s responsible for dealing with the semantics of the source language, and compiling it into QAT’s intermediate representation (IR) which is independent of source language. A frontend is usually defined by two components:
A pipeline which can be used to validate and modify the provided source program (before compiling to QAT IR), see
qat.frontend.passes
for a list of passes available.A parser that generates an abstract syntax tree (AST) from the source program, and interprets the tree to produce QAT IR.
The general rule of thumb is that there is a single frontend per type of source program. Currently, QAT supports source programs in the following formats through their appropriate frontends:
QASM2 example
Each frontend can be provided with a compilation pipeline, with a suitable default already chosen for each. As an example, let’s try to compile a QASM2 program
1from qat.frontend.qasm import Qasm2Frontend
2from qat.model.loaders.legacy import EchoModelLoader
3from qat.frontend.analysis_passes import InputAnalysis
4from qat.frontend.transform_passes import InputOptimisation
5from qat.passes.pass_base import PassManager
6
7qasm_str = """
8OPENQASM 2.0;
9include "qelib1.inc";
10qreg q[2];
11creg c[2];
12h q[0];
13cx q[0], q[1];
14measure q -> c;
15"""
16
17model = EchoModelLoader(8).load()
18pipeline = PassManager() | InputAnalysis() | InputOptimisation(model)
19frontend = Qasm2Frontend(model, pipeline=pipeline)
20ir = frontend.emit(qasm_str)
The ir
emitted is an instruction builder that contains QAT IR, and can now be used
within the middleend of the compilation.
Automatically chosen frontends
Often enough, we are not explicitly told what is the type of source program. We need to infer the type and then decide on the correct frontend to use. We would also like to avoid defining individual pipelines for each type of source program: it is sometimes expected that the middleend and backend will be the same regardless of the type of source program. For these reasons, it makes sense to have an automatic frontend that inspects the source program to determine and deploy the matching frontend.
This is achieved in QAT using the AutoFrontend
.
This frontend is provided with a list of frontends and attempts to match the source
program with one of the given frontends. If a frontend is found to be compatible with the
source program, it is used. Defining an automatic frontend might look something like:
frontend = AutoFrontend(
model,
Qasm2Frontend(model),
Qasm3Frontend(model),
QIRFrontend(model)
)
The natural question that follows is “how does a frontend know if it is compatible with
the source program?” Each source-language specific frontend must be equipped with a
check_and_return_source
method that inspects the contents of the source program, and returns it if it is found to be
compatible.
Let’s consider a simple example to demonstrate how a custom frontend can be used within an
AutoFrontend
.
1from qat.frontend import BaseFrontend, AutoFrontend, Qasm2Frontend
2from qat.purr.backends.echo import get_default_echo_hardware
3
4class MyCustomFrontend(BaseFrontend):
5
6 def check_and_return_source(self, src):
7 if not isinstance(src, str):
8 return False
9 if "this is a fancy new source language" in src:
10 return src
11 return False
12
13 def emit(self, src, *args):
14 return src
15
16
17qasm_program = """
18OPENQASM 2.0;
19include "qelib1.inc";
20qreg q[2];
21creg c[2];
22h q[0];
23cx q[0], q[1];
24measure q -> c;
25"""
26custom_program = "this is a fancy new source language"
27invalid_program = "this will not work"
28
29model = get_default_echo_hardware(8)
30frontend = AutoFrontend(model, Qasm2Frontend(model), MyCustomFrontend(model))
31qasm_frontend = frontend.assign_frontend(qasm_program)
32custom_frontend = frontend.assign_frontend(custom_program)
33no_frontend = frontend.assign_frontend(invalid_program)
The types of the returned objects will be
Qasm2Frontend
, MyCustomFrontend`and
:class:`NoneType
respectively.
Alternative frontends
QAT also has a few other frontends available:
FallthroughFrontend
: Simply passes through the source program. Used in situations where no frontend is required.CustomFrontend
: Allows a custom pipeline-oriented frontend to be defined.
Middleend
The middleend module handles compilation responsibilities at the level of QAT IR. It is passed a quantum program at the level of QAT IR, and applies a sequence of hardware-agnostic passes that
Perform analysis on the IR,
Validate the IR has appropriate specified properties,
Transforms the IR (e.g. optimization and santisation of the IR).
See qat.middleend.passes
for a full list of available passes.
Default Middleend
The standard middleend to use is the
DefaultMiddleend
, which has a
pre-defined pipeline.
from qat.middleend import DefaultMiddleend
middleend = DefaultMiddleend(hardware_model)
ir = middleend.emit(ir)
Despite being hardware-agnostic, the middleend needs to be instantiated with the hardware
model.
Note that here hardware-agnostic means that it does not depend on the code-generation
related details of the target. However, the calibration file is still required to produce
the correction QAT IR instructions. Calling middleend.emit(ir)
will instruct the
middleend to pass the IR through the compilation pipeline.
Custom Middleend
We can specify a middleend with a custom compilation pipeline using the
CustomMiddleend
class. For example, the
following middleend would optimise over phase shifts and remove any unnecessary
post-processing instructions to reduce the overall instruction count.
from qat.middleend import CustomMiddleend
from qat.passes.pass_base import PassManager
from qat.compiler.transform_passes import PhaseOptimisation, PostProcessingSanitisation
pipeline = (
PassManager()
| PhaseOptimisation()
| PostProcessingSanitisation()
)
middleend = CustomMiddleend(model, pipeline)
Backend
After we have compiled the source program to QAT IR, and performed any validation and transformation we wish to do at the level of QAT IR, the program will enter the backend. The objective of the backend is to compile the QAT IR into a language understandable by the target, a process referred to as “code generation” (codegen). Like the frontend, the backend has two components:
A compilation pipeline which performs analysis on the IR which is used during codegen, validation passes to verify the code is compatible with the native code, and transformation passes to make the intermediate code more appropriate for the codegen. See
qat.backend.passes
for a full list of passes (which might be target specific).An emitter that walks through the QAT IR and generates native code.
WaveformV1Backend example
As an example, lets consider the
WaveformV1Backend
, a backend
that generates code for earlier (and now legacy) iterations of OQC hardware, but is still
maintained for some of our simulators and demonstration purposes. Like the frontend and
middleend, the emitter can be used to generate native code by calling the emit
method,
from qat.backend.waveform_v1 import WaveformV1Backend
backend = WaveformV1Backend(model)
pkg = backend.emit(ir)
The package returned from a backend is referred to as an
Executable
. They are Pydantic data classes
that contain all the information needed to execute a program, including the instructions
needed by the control hardware (or simulator), and the classical post-processing
instructions required by the runtime needed to interpret and process the results (see the
execution section for more details).
Making ends meet
Now that we have covered each type of “end”, we can bring it together to define a complete compilation pipeline. Let’s write one that compiles to “WaveformV1”.
from qat.model.loaders.legacy import EchoModelLoader
from qat.frontend import AutoFrontend
from qat.middleend import DefaultMiddleend
from qat.backend.waveform_v1 import WaveformV1Backend
from compiler_config.config import CompilerConfig
model = EchoModelLoader(8).load()
frontend = AutoFrontend(model)
middleend = DefaultMiddleend(model)
backend = WaveformV1Backend(model)
qasm_str = """
OPENQASM 2.0;
include "qelib1.inc";
qreg q[2];
creg c[2];
h q[0];
cx q[0], q[1];
measure q -> c;
"""
config = CompilerConfig(repeats=1000)
ir = frontend.emit(qasm_str, compiler_config=config)
ir = middleend.emit(ir, compiler_config=config)
pkg = backend.emit(ir, compiler_config=config)
The result will be a freshly prepared package ready for execution! Notice that this just
achieves what QAT().compile()
would, but not as neatly wrapped up.