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", {"rx", "cz", "rz"}, 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 ({"rx", "cz", "rz"} above) is the target’s native-gate set, which the circuit is decomposed into. See Choosing a Native-Gate Set and 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", {"rx", "cz", "rz"}, 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 Native-Gate Set and Backend

MQSSCompiler has no built-in gate sets: you always pass the target’s native-gate set yourself, or none at all to skip decomposition:

// Decompose into an explicit native-gate set.
compiler.compile("path/to/circuit.qke", {"rx", "cz", "rz"}, opts);

// Skip native-gate decomposition entirely.
compiler.compile("path/to/circuit.qke", opts);

For reference, these are the native-gate sets of some common targets:

Target

Native-gate set

IQM

prx, cz

planqc

rx, cz, rz

WMI

cz, x, y, rz, sx

The gate mnemonics in a native-gate set are the 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). A few device-specific names are accepted as aliases for a mnemonic, e.g. r for prx.

The backend_name argument

The full signature also takes a backend_name and a qubit_connectivity map:

compiler.compile("path/to/circuit.qke", "LRZ", {"r", "cz"}, qubit_connectivity, opts);

backend_name only changes anything when it is "LRZ" (case-sensitive); any other value, including "", compiles exactly the same way.

With "LRZ", OpenQASM2 output uses your own spelling for any aliased gate in the native-gate set. For example, passing r produces r(...) calls in the output, not phased_rx(...), which is the name prx is otherwise emitted under. With any other backend name, the output keeps phased_rx. This renaming only applies to OPENQASM2 output.

qubit_connectivity lists the coupled qubit pairs of a target with restricted connectivity; the circuit is mapped onto it before decomposition. See MQSSCIInterfaces/MQSSCompiler.h for all four compile overloads and their compileSource counterparts.

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 its supported 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.