# Using MQSSCI as a Library The [Passes](passes.md) 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](../develop-guide/integrate.md) 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. ```cpp #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 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](passes.md#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](#choosing-a-backend) below for the alternatives. 4. `compile` returns `std::optional`, 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](verification.md), applied automatically during `compile`/ `compileSource` rather than as a separate `--mqssci-verify` flag on `mqss-opt`. ```cpp mqss::mqssci::CompilerOptions opts; opts.optimization_level = mqss::mqssci::OptLevel::O1; opts.result_format = mqss::mqssci::ResultFormat::OPENQASM2; opts.verify = true; std::optional 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: ```cpp // 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"` | `prx`, `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](passes.md#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: ```cpp for (std::string_view name : mqss::mqssci::MQSSCompiler::getSupportedInputFormats()) { llvm::outs() << name << "\n"; } ``` It returns a `std::vector` (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: ```cpp #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 context = mqss::mqssci::opt::createMQSSContext(); // 2. Parse your MLIR file into a module auto module = mlir::parseSourceFile("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&`: ```cpp 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](passes.md). ## 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: ```sh make test-interfaces ``` For what the interfaces testing framework covers and how to extend it, see [Testing the Interface](../develop-guide/integrate.md#testing-the-interface) in the developer guide.