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:

  1. #include "MQSSCIInterfaces/MQSSCompiler.h" is the only header you need for this path.

  2. mqss::mqssci::CompilerOptions configures the run: optimization_level selects the O1/O2/O3 preset pipeline (see Pass Pipelines for what each includes), and result_format selects the output format — one of the mqss::mqssci::ResultFormat enumerators: OPENQASM2, QIR, QIRBASE, QIRADAPTIVE, QIRFULL, or IQMJSON.

  3. 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.

  4. compile returns std::optional<std::string>, not the compiled circuit directly. On success, it holds the compiled output; on failure it’s std::nullopt, and the reason has already been reported via mlir::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);

backend_name

Native-gate set

"iqm"

phased_rx, cz

"planqc"

rx, cz, rz

"wmi"

cz, x, y, rz

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.