# 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", {"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](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` (`{"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](#choosing-a-native-gate-set-and-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", {"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: ```cpp // 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](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). 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: ```cpp 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: ```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 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: ```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.