Using MQSSCI as a Library¶
The Passes page shows how to run passes through the mqss-opt/mqss-cc command-line
tools. This page is for the case where you don’t want a separate command-line step at all — you want
your own C++ program to compile a circuit directly.
Before You Start¶
Your project needs to link against the MQSSCI library first. That’s a one-time CMake setup step —
see
Integrating the MQSS Quantum Compilation Suite within your project
for how to fetch MQSSCI with FetchContent and link your target against mqss-ci::mqss-ci. The
rest of this page assumes that step is done and focuses purely on the C++ side.
A Minimal Example¶
mqss::mqssci::MQSSCompiler is the simplest way to compile a circuit: one header, one class, one
call. It hides dialect registration, MLIR context setup, and pipeline assembly behind a single
compile call.
#include "MQSSCIInterfaces/MQSSCompiler.h"
mqss::mqssci::MQSSCompiler compiler;
mqss::mqssci::CompilerOptions opts;
opts.optimization_level = mqss::mqssci::OptLevel::O1; // O1, O2, or O3 — selects the preset pipeline
opts.result_format = mqss::mqssci::ResultFormat::OPENQASM2; // or QIR, QIRBASE, QIRADAPTIVE, QIRFULL, IQMJSON
opts.verify = false; // optional; see Verifying the Compiled Output below
std::optional<std::string> qasm = compiler.compile("path/to/circuit.qke", "planqc", opts); // Use compileSource() to parse source string
if (!qasm) {
// Compilation failed. MQSSCompiler has already reported a diagnostic through
// MLIR's diagnostic engine — it never throws or aborts the process.
return 1;
}
llvm::outs() << *qasm;
Walking through it:
#include "MQSSCIInterfaces/MQSSCompiler.h"is the only header you need for this path.mqss::mqssci::CompilerOptionsconfigures the run:optimization_levelselects theO1/O2/O3preset pipeline (see Pass Pipelines for what each includes), andresult_formatselects the output format — one of themqss::mqssci::ResultFormatenumerators:OPENQASM2,QIR,QIRBASE,QIRADAPTIVE,QIRFULL, orIQMJSON.The second argument to
compile("planqc"above) is a known backend name that selects a built-in native-gate set for decomposition. See Choosing a Backend below for the alternatives.compilereturnsstd::optional<std::string>, not the compiled circuit directly. On success, it holds the compiled output; on failure it’sstd::nullopt, and the reason has already been reported viamlir::emitError— MQSSCI is built without C++ exceptions, so a failed compile never throws or crashes your program, it just returns an empty optional.
Verifying the Compiled Output¶
Set opts.verify = true to have MQSSCompiler check that native-gate decomposition didn’t change
what the circuit computes — the same circuit-equivalence checking described in
Verifying Circuit Correctness, applied automatically during compile/
compileSource rather than as a separate --mqssci-verify flag on mqss-opt.
mqss::mqssci::CompilerOptions opts;
opts.optimization_level = mqss::mqssci::OptLevel::O1;
opts.result_format = mqss::mqssci::ResultFormat::OPENQASM2;
opts.verify = true;
std::optional<std::string> qasm = compiler.compile("path/to/circuit.qke", "planqc", opts);
This is a hard gate, not just a warning: if verification finds that decomposition broke equivalence,
compile/compileSource return std::nullopt — same as any other failure — rather than handing
you output that silently computes the wrong thing. The default is opts.verify = false, so turning
it on is opt-in and costs nothing unless you ask for it.
Choosing a Backend¶
compile has overloads for three ways to select the native-gate set used for decomposition: by a
known backend name, by an explicit gate list, or not at all:
// By backend name: iqm, planqc, and wmi are recognized out of the box.
compiler.compile("path/to/circuit.qke", "iqm", opts);
// By an explicit native-gate set, when your target isn't one of the built-in backends.
compiler.compile("path/to/circuit.qke", {"rz", "rx", "cz"}, opts);
// Neither: skip native-gate decomposition entirely.
compiler.compile("path/to/circuit.qke", opts);
|
Native-gate set |
|---|---|
|
|
|
|
|
|
The gate mnemonics used here — both in these built-in sets and in an explicit native-gate set you
supply yourself — are the same ones the BasisConversionPass recognizes. See
Supported Gate Mnemonics for the full list of mnemonics, the
Quake operation each one maps to, and their constraints (parameter count, controls, adjointness).
The full signature (used in the examples above via its three shorthand overloads) also accepts a
qubit_connectivity map alongside the native-gate set, for targets with restricted qubit
connectivity. See MQSSCIInterfaces/MQSSCompiler.h for all four compile overloads.
Discovering Supported Input Formats¶
MQSSCompiler::getSupportedInputFormats() is a static method that returns the display name of every
input format MQSSCI’s mqss::mqssci::InputFormat enum defines:
for (std::string_view name : mqss::mqssci::MQSSCompiler::getSupportedInputFormats()) {
llvm::outs() << name << "\n";
}
It returns a std::vector<std::string_view> (currently {"cudaq-quake", "catalyst-quantum"}) —
useful for validating a user-supplied format name or listing the options in a CLI’s --help output.
Note that compile/compileSource currently only accept the cudaq-quake dialect regardless of
what this list reports; see the note at the top of MQSSCompiler.cpp.
Advanced: Building a Custom Pipeline Yourself¶
MQSSCompiler covers the common case: a preset optimization level, a fixed native-gate set, and one
of two output formats. If you need to run individual passes, pass pass-specific options (e.g.
CommonGateCancellationPassOptions), or assemble a pipeline MQSSCompiler doesn’t support, drop
down to the same mlir::OpPassManager-based API MQSSCompiler itself is built on:
#include "Passes/Transforms/Dialects.h"
#include "Passes/Transforms/Pipelines.h"
#include "Passes/CodeGen/CodeGenPasses.h"
// 1. Get an MLIRContext with every dialect MQSSCI's passes need already loaded.
std::unique_ptr<mlir::MLIRContext> context = mqss::mqssci::opt::createMQSSContext();
// 2. Parse your MLIR file into a module
auto module = mlir::parseSourceFile<mlir::ModuleOp>("path/to/file", context.get());
if (!module) {
llvm::errs() << "failed to parse MLIR file\n";
}
// 3. Build a pass manager and add passes to the pipeline
mlir::PassManager pm(context.get());
CommonGateCancellationPassOptions CancelOpts;
CommonCommutePassOptions CommuteOpts;
CancelOpts.mode = "CancelGate";
CommuteOpts.mode = "CX-RX";
pm.addPass(CommonGateCancellationPass(CancelOpts));
pm.addPass(CommonCommutePass(CommuteOpts));
// Run the Pass Pipeline
if (mlir::failed(pm.run(*module))) {
llvm::errs() << "Compiler: Pipeline failed\n";
}
// 4. Lower the optimized module to OpenQASM 2
pm.addPass(mqss::mqssci::codegen::QuakeToQASM2Pass());
if (mlir::failed(pm.run(*module))) {
llvm::errs() << "Compiler: Conversion of Quake to QASM2 failed\n";
}
mqss::mqssci::opt::createMQSSContext() is a convenience that registers every MQSSCI dialect and
returns a ready-to-use MLIRContext in one call — equivalent to building a DialectRegistry with
registerMQSSDialects, constructing an MLIRContext from it, and calling
loadAllAvailableDialects() yourself.
If you’d rather capture the OpenQASM output in a string instead of printing it, QuakeToQASM2Pass
also accepts an llvm::raw_ostream&:
std::string qasm;
llvm::raw_string_ostream os(qasm);
pm.addPass(mqss::mqssci::codegen::QuakeToQASM2Pass(os));
For the full list of available passes, their options, and valid option values, see Passes.
Testing the Interface¶
MQSSCompiler has its own testing framework at
tests/unittests/MQSSCIInterfaces/test_Interface.cpp. It builds automatically as part of the normal
build (via scripts/build.sh / make target) — run it with:
make test-interfaces
For what the interfaces testing framework covers and how to extend it, see Testing the Interface in the developer guide.