MLIR development policy¶
This page defines how MQT Core uses MLIR. It condenses the parts of the MLIR and LLVM guidance that most often affect design, review, tests, and debugging. MQT Core is an MLIR consumer, so this policy is normative for this repository even where existing code differs.
Reviewed against LLVM and MLIR 23.1.0. Revisit this page and the MLIR clang-tidy configuration on every major LLVM/MLIR upgrade.
C++ const and IR handles¶
MLIR’s intermediate representation (IR) is a mutable graph. Value and its
TypedValue, BlockArgument, and OpResult forms, Operation, Block,
Region, ModuleOp, and typed operation wrappers are small handles into that
graph. A const handle does not make the referenced IR immutable and creates a
false model of const-correctness. Follow MLIR’s
rationale for the usage of const:
void inspect(Value value, Operation* operation);
for (Value operand : operation->getOperands()) {
/// Use operand without implying that the IR graph is immutable.
}
Do not write:
void inspect(const Value value, const Operation* operation);
This rule also applies to local variables, lambda parameters, range variables,
structured bindings, typed wrappers such as func::FuncOp, and const auto
that deduces one of these types. ValueRange, OperandRange, and ResultRange
are cheap non-owning views over the same handles. Copy these handles and views
instead of binding them as const values or references. Do not add top-level
const to any by-value parameter. Continue to use normal const-correctness for
ordinary C++ objects, references, pointers, containers, and strings; do not
distort a generic interface or access through a const container merely because
one contained value is an MLIR handle. MLIR Type and Attribute objects are
immutable values and are not mutable IR graph handles.
The dependency-free source gate checks only explicitly named core handles and
views. A text check cannot infer the type behind auto or distinguish an MLIR
operation wrapper from an unrelated C++ type whose name ends in Op. This
policy still applies in both cases.
Passes, verifiers, and rewrites¶
Follow the MLIR Developer Guide and these repository rules:
A pass may assume that its declared input operation is verified. It must not crash or assert on valid IR, and its successful output must verify.
A verifier checks only invariants owned by its operation. Do not make an operation verifier depend on enclosing pipelines or unrelated operations.
Declare every dialect that a pass can create or load as a dependent dialect.
Use bounded recursion. Treat unbounded recursive IR walks or pattern application as correctness risks, not only performance risks.
Make rewrite-pattern return values truthful. Return failure without changing IR; report success only after performing the promised rewrite.
Use established matchers such as
m_Constantinstead of manually recognizing one producer shape.Use traits for static properties and interfaces when behavior varies by operation implementation.
Treat a memref as a shaped memory abstraction, not as a C++ pointer.
Search upstream MLIR for an operation, interface, trait, conversion, or helper before adding an MQT-specific equivalent.
Use diagnostics for invalid input or unsupported behavior. Reserve assertions for internal invariants that valid input cannot violate. Diagnostics must state what failed and, when useful, which form is supported.
Linear quantum values¶
Every !qco.qubit and one-dimensional qubit tensor or vector SSA value in valid
QCO IR has exactly one use, including block arguments. qco::verifyLinearity
owns this whole-IR check; ordinary MLIR operation verification alone does not
establish it. Builders and transformations must preserve the invariant, and
public QCO pipeline boundaries must validate it.
Rewrites on valid QCO IR can use *value.user_begin() to obtain the sole
consumer, or *value.use_begin() for its OpOperand and operand number. Do not
repeat hasOneUse() guards in these rewrites. Keep linearity checks in the
verifier and optional debug assertions at internal boundaries. This rule does
not apply to QC references or classical SSA values, and does not permit assuming
that results preserve wire order: linearity and wire correspondence are separate
contracts.
When forwarding a linear value requires deleting its producer, use a rewrite that removes both operations. A fold can only change its root; do not rely on later dead-code elimination to restore linearity before other rewrites run.
Data structures and performance¶
Use LLVM views and abstract range types at MLIR-facing boundaries. Prefer an
LLVM data structure such as SmallVector, DenseMap, or MapVector when its
storage, lookup, ordering, or API behavior provides a concrete benefit. Keep a
standard-library type when it already expresses the required contract.
When code in the mlir namespace or one of its nested namespaces uses an LLVM
name that mlir/Support/LLVM.h imports, include that header and use the
unqualified name, such as SmallVector, StringRef, or function_ref. Do not
rely on mlir/Support/LLVM.h for type definitions. Include each LLVM header
that the source file needs. Keep the llvm:: qualifier for names that
mlir/Support/LLVM.h does not import.
Do not convert containers in bulk for style. Require a profile, benchmark, or a specific allocation or complexity argument for a performance rewrite. Keep user-visible output deterministic: never use pointer identity or unspecified iteration order as an observable ordering rule.
Tests¶
MQT Core uses GoogleTest and CTest for MLIR code. Do not add lit or FileCheck
infrastructure. Adapt the useful principles from the [MLIR Testing Guide]
mlir-testing as follows:
Parse and transform IR in-process. Use a subprocess only for irreducible command-line behavior.
Use the smallest input that isolates the contract.
Give the test a name that states the behavior.
Check semantic operations, types, attributes, and diagnostics instead of a large textual snapshot.
Test valid and invalid cases when both form part of the contract.
Verify input and successful output around pass-pipeline tests.
Add a regression test for every behavioral bug fix.
Debugging¶
Start from the MLIR debugging workflow:
Reduce the input to a small
.mlirfile and identify the first failing pass.Run only the relevant pass pipeline.
Print generic IR when custom syntax may hide malformed state.
Print IR before the relevant pass or after a failure.
Disable multithreading when output order obscures the failure.
Enable dialect-conversion tracing for a conversion failure.
Save a pass-pipeline crash reproducer for crashes that are not immediately local.
Turn the reduced case into the smallest direct regression test.
mqt-cc registers MLIR’s standard pass-manager options. Useful options include
--mlir-print-op-generic, --mlir-print-ir-before-all,
--mlir-print-ir-after-failure, --mlir-disable-threading,
--mlir-print-stacktrace-on-diagnostic, and
--mlir-pass-pipeline-crash-reproducer=<path>. --debug-only traces require a
build of LLVM/MLIR and MQT Core with debug logging enabled; do not assume that a
release build provides them.