Development policy¶
This page defines MQT Core’s repository-owned development policy. It supplements the contribution guide, which describes the contribution process, and the AI usage policy, which defines accountability for AI-assisted work.
C++ choices¶
Use C++20 standard-library facilities before adding a project abstraction or dependency.
Keep variables local, initialize them when declared, and give each name one clear meaning.
Use
autowhen the initializer makes the type clear or when spelling the type would hide the important part of an expression. Spell the type when it communicates a contract or prevents a surprising conversion.Preserve deterministic user-visible output. Do not rely on pointer values or unspecified container iteration order.
Keep cleanup separate from behavioral changes unless the cleanup is required to make the behavior correct.
Do not add flexibility, configuration, or abstraction without a current use.
The MLIR section explains the deliberate differences for code built on LLVM and MLIR.
C++ includes¶
Order include groups as follows, sorting each group alphabetically:
The matching header for the source file.
MQT Core headers, including MQT’s MLIR headers.
Other private project headers.
Third-party library headers.
Upstream MLIR headers.
LLVM headers.
System and standard-library headers.
Use quotes for project and third-party headers. Use angle brackets for system
and standard-library headers, or when a library requires them. Keep includes
that depend on macros or declarations at the point where they are needed. For
example, <qiskit.h> needs angle brackets to avoid the local Qiskit.h on
case-insensitive file systems. Keep its extension function table,
<qiskit/funcs_py.h>, in angle brackets so that it sorts after the umbrella
header.
The root .clang-format enforces this order. MQT’s MLIR headers use the mqt/
prefix, for example "mqt/Dialect/QCO/IR/QCOOps.h". Upstream MLIR headers use
mlir/, for example "mlir/IR/MLIRContext.h". When adding a dependency,
include its header prefix in the third-party formatter category. Other quoted
includes belong to the private project header group.
C++ documentation comments¶
Use // for ordinary code comments. Never use /// for them.
Use /// for Doxygen documentation. Keep public API documentation at the
declaration; do not duplicate it in the implementation.
Start descriptions with a short sentence stating the entity’s purpose or
observable behavior, on one source line where practical. Separate any further
content, including \param and \returns, with a blank /// line. Add details
only when they explain something the summary and signature leave unclear.
Separate further paragraphs with blank /// lines. Do not use \brief or
\details outside the macro exception below.
Structural commands such as @defgroup and @name remain valid; marker-only
blocks such as @{ and @} need no summary. Do not add a summary solely to
precede an @see cross-reference.
Preserve existing documentation when changing comment style. Inline /* ... */
comments remain valid, including unused parameter names
(OpAdaptor /*adaptor*/) and argument labels (/*isSigned=*/false).
Prefer Unicode for simple mathematical notation in comments, such as π,
R(θ, φ), U†, and |0⟩. Put spaces around arrows in prose and rewrite
diagrams, as in QCO → jeff and QC ↔ QCO. Preserve the spelling of code
identifiers, language syntax, and literal output in examples. Use Doxygen math
(\f$...\f$ or \f[...\f]) for formulas that need typeset fractions,
subscripts, or other mathematical layout.
Keep //!< or ///< for trailing member documentation. Keep /** ... */
documentation inside backslash-continued macros: line comments there can consume
the following declarations after line splicing. Preserve explicit @brief
commands there when Doxygen needs them to retain summaries after macro
expansion.
The cpp-documentation-style lint hook checks project-owned C++ files. It
rejects block documentation and explicit summary or detail commands, except on
continued macro lines. It also rejects lint directives written as documentation
comments.
Keep top-level @file documentation and put its summary on the next line,
without @brief:
/// @file Circuit.h
/// Defines the circuit representation.
/// Returns the number of qubits in the circuit.
[[nodiscard]] size_t getNqubits() const;
/// Applies an operation to the selected qubits.
///
/// Rejects duplicate indices in \p qubits.
///
/// \param qubits Qubit indices in application order.
/// \returns The created operation.
Operation apply(llvm::ArrayRef<Qubit> qubits);
Reproduce C++ lint locally¶
Before pushing a C++ change, run:
uvx nox -s cpp-lint
The session configures the lint preset and builds mqt-core-lint-headers to
prepare generated headers without compiling or linking the project. It then runs
the same cpp-linter release and options as CI against every line of each
changed C++ file. It compares against origin/main by default. Pass a different
Git diff base after -- when needed:
uvx nox -s cpp-lint -- upstream/main
Use --all to check every eligible project C++ file instead:
uvx nox -s cpp-lint -- --all
Changed-line clang-tidy commands remain useful for quick iteration, but they
do not reproduce CI’s whole-changed-file scope. Update this session when the
reusable C++ lint workflow changes its action version or inputs.
Python docstrings¶
Use Google-style docstrings, consistent with
PEP 257, including in nanobind binding strings and generated stubs.
Start with a short sentence on one line stating the Python API’s purpose or
observable behavior. Separate any further content, including Args:,
Returns:, and Raises:, with a blank line. Add input, output, side-effect,
and failure details only when the summary and signature leave them unclear.
Separate further paragraphs and sections with blank lines.
"""Applies an operation to the selected qubits.
Qubit indices follow application order. Duplicate indices raise ValueError.
"""
Change binding docstrings in the C++ source and regenerate stubs with
uvx nox -s stubs; never edit generated .pyi files manually.
Commit messages¶
MQT Core adapts Chris Beams’s commit-message guidance to its gitmoji convention:
Start with the established gitmoji prefix and an imperative subject.
Target 50 characters and never exceed 72 characters, including the prefix.
Do not end the subject with a period.
Add a blank line before the body.
Use the body to explain why the change is needed, its constraints, and any non-obvious tradeoffs. Do not restate the diff.
Wrap prose at 72 characters where practical.
Preserve legitimate human
Co-authored-bytrailers. Record AI assistance withAssisted-by, never by representing an AI system as an author.
Prose and terminology¶
Keep documentation proportional to the change. For small additions, update the existing explanation or example first. Include behavior, usage, and limits that help the reader. Keep internal representation and validation details in the owning reference and link to it instead of repeating them in user guides.
Apply Orwell’s six rules for writing to documentation, comments, diagnostics, tests, commit messages, and review communication:
Avoid familiar metaphors, similes, and figures of speech.
Use a short word when it has the same meaning as a long word.
Remove every word that does not add meaning.
Use active voice when possible.
Use everyday English instead of jargon when precision permits.
Break a rule before making the text unclear or incorrect.
Apply the relevant ASD-STE100 Simplified Technical English principles: keep sentences short and direct, give each sentence one main idea, use one term per concept, and prefer explicit nouns to vague pronouns. These are required style rules, not a claim of formal ASD-STE100 compliance. Use established terms from quantum computing, LLVM/MLIR, HPC, and computer science. Explain differences between communities’ terms once; do not invent synonyms for variety.
Use one established term for one concept. The MQT Core glossary records preferred names, accepted aliases, and distinctions that matter to public APIs or compiler design. Update the glossary in the same pull request when introducing or changing a public or potentially ambiguous term. Do not add entries for ordinary language or private implementation details.
Maintenance¶
Review subsystem policy, agent instructions, formatting, lint rules, and exceptions when a major dependency changes. Update the recorded upstream version and remove obsolete exceptions. Existing code does not override a new decision merely because migration is incomplete.
Agent guidance¶
Treat AGENTS.md as a concise routing and guardrail layer, not a second copy of
the development policy. Keep rationale, examples, and detailed procedures in the
canonical documentation and link to the applicable sections. Repeat only short,
non-obvious rules that agents must keep in immediate context to avoid a
recurring mistake. Enforce mechanical rules in repository tooling instead of
relying on prose.
Scope instructions to relevant work. Keep skill descriptions short and load workflow details on demand. Define outcomes and constraints rather than prescribing each step. Complete authorized work through relevant validation; repeat checks only for new changes, failures, or unresolved concerns.
Plans and audit files should preserve useful decisions, not log routine tasks. Benchmark only when requested or needed to answer a concrete performance question. Keep ad hoc experiments outside the repository unless explicitly requested.
Release documentation¶
Do not change CHANGELOG.md or UPGRADING.md except in release-preparation PRs
or when explicitly asked. Feature and fix PR descriptions must explain
user-facing changes, limitations, and required migrations, with relevant labels
for Release Drafter.
During release preparation, record notable user-facing changes with PR references and all contributing human authors. Group entries by user workflow and subsystem. Fold refinements to functionality that has never shipped into its feature entry. After release, describe subsequent changes relative to that published behavior.
Lead major architectural releases with the new program model, enabled workflows, and consequences for existing users before listing individual changes. Keep the release overview and migration paths visible from the README and documentation home page. Preserve PR and contributor references when regrouping entries.
Document breaking changes to released interfaces and migration instructions in
the PR that introduces them. Give the replacement, changed semantics, or lack of
a replacement. During release preparation, collect these instructions in
UPGRADING.md and summarize the changes in CHANGELOG.md. Do not add
migrations between intermediate unreleased APIs. Preserve published release
sections except when correcting a verified error.
Before a release, prepare the changelog and upgrade guide from the Git history, merged PR descriptions, the Release Drafter draft, and any existing Unreleased notes. Account for maintenance branches, backports, and frontports; merge dates alone do not define what ships for the first time. Include relevant feature and fix PRs, verify contributor attribution, and record why maintenance-only changes do not need public entries. AI tools may help draft entries, but a maintainer must verify their claims, PR links, and attribution against the release diff and merged PRs before accepting the draft. Check migration examples against the released and proposed APIs, build the executable documentation, and check its links.
Between releases, CHANGELOG.md and UPGRADING.md contain only published
releases. Create their next sections during release preparation and finalize the
version and release date before publishing. Version tags drive package metadata;
do not edit generated version files to stage a release. Release preparation does
not itself publish a release.
Documentation validation¶
Install Python 3.14, LLVM/MLIR 23.1, and Doxygen before building the complete
documentation. The Doxygen configuration is validated with Ubuntu 24.04’s
version 1.9.8 and version 1.17. Use uvx nox --non-interactive -s docs to build
generated references and execute the MyST notebooks. This command fails on
Sphinx and Doxygen diagnostics and checks local links in the generated HTML,
including the native C++ reference. Use
uvx nox --non-interactive -s docs -- -b linkcheck to check external links
separately.
Notebook execution is forced on each build. The docs session isolates the QDMI registry from system, user, project, and inline environment definitions while retaining packaged devices. Executable examples use local DDSIM and require no credentials. Configuration recipes for external providers do not execute.
Use {code-cell} blocks in pages with MyST-NB front matter. Keep required setup
visible or collapsible with hide-input, display useful computed output, and
assert the demonstrated semantics. Use checked subprocess calls for CLI
examples. Ordinary code fences document configuration or interfaces without
executing them. Give figures descriptive alternative text and preserve full
contracts in linked references when they would interrupt a tutorial.
MLIR¶
These rules apply to MLIR dialects, transforms, tools, and bindings. MQT Core is an MLIR consumer; this policy applies 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 and positional quantum state correspondence through
scf.for, both scf.while regions, qco.if, and qco.index_switch. Functions
return their quantum arguments as trailing results in argument order. Classical
state can change shape across scf.while regions. Ordinary MLIR operation
verification alone does not establish these QCO invariants. 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 permits relying on the
positional correspondence checked by qco::verifyLinearity. Exactly-one-use and
wire correspondence remain separate checks.
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.
Follow LLVM’s SmallVector guidance: use SmallVector<T>
without an explicit inline capacity by default. Retain SmallVector<T, N> when
a known bound or measured storage need justifies N, such as synthesis results
containing one or two qubits. N sets inline storage, not a size limit. Keep
explicit capacities required by LLVM for large element types. Comment only on
reasons that are not clear from the type, contract, or nearby code. Keep the
rationale near the owning type or declaration; do not repeat it at each use or
restate how inline capacity works. Consider std::array for exactly fixed-size
data; do not change unrelated APIs merely to use it.
For parameters that borrow a sequence, use ArrayRef<T> or a suitable MLIR
range when only reading elements, MutableArrayRef<T> when changing elements in
place, and SmallVectorImpl<T>& when resizing or appending. These interfaces do
not depend on inline capacity. A function that transfers ownership may use a
concrete container type. Audit unexplained capacity choices individually; do not
replace explicit capacities in bulk or claim performance gains without evidence.
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.
Build the driver with the configured LLVM/MLIR installation:
cmake --preset release
cmake --build --preset release --target mqt-cc
mqt_cc=build/release/mlir/tools/mqt-cc/mqt-cc
Save this reduced example as reduced.mlir:
module {
func.func @f(%q: !qco.qubit) -> !qco.qubit {
%h0 = qco.h %q : !qco.qubit
%h1 = qco.h %h0 : !qco.qubit
return %h1 : !qco.qubit
}
}
Run exactly the selected pipeline with --run-pipeline:
"$mqt_cc" reduced.mlir --run-pipeline \
--pass-pipeline='builtin.module(canonicalize)'
"$mqt_cc" reduced.mlir --run-pipeline \
--pass-pipeline='builtin.module(hadamard-lifting)' \
--mlir-print-op-generic --mlir-print-ir-before-all \
--mlir-print-ir-after-failure --mlir-disable-threading
The first command removes the two Hadamards. The second prints the input to
HadamardLifting on stderr and its result on stdout. An empty builtin.module()
pipeline preserves the input. Use --mlir-print-ir-before=hadamard-lifting to
select one pass’s dump in a longer pipeline. Non-stdout MLIR output selected
with -o is bytecode; redirect stdout to save textual IR.
Isolated execution requires MLIR input and a builtin.module(...) pipeline. It
skips frontend conversion, compiler preparation, default optimizations, and
output lowering. Parsing verifies the input, and QCO linearity is checked before
and after the pipeline. Pass-manager verification remains enabled. Do not
combine this mode with --emit, target compilation, or the decomposition
convenience flag. Supply already reduced input for the pass’s supported subset.
Ordinary compilation with --pass-pipeline retains its required preparation and
cleanup stages around the supplied QCO pipeline. --passes is an alias for the
same textual syntax; individual pass flags are not supported. The CLI and
library share QCO pass and upstream transform registration. The driver also
registers its conversion passes so they can be selected for debugging.
Diagnostics and reproducers¶
Use --mlir-print-stacktrace-on-diagnostic to attach a trace when the LLVM
build supports stack traces. Source locations, excerpts, and attached operation
notes remain available throughout compilation, including for stdin. Initial jeff
conversion also uses the requested pass-manager instrumentation.
For a runnable failure example, save this valid but unsupported QC input as
unsupported.mlir:
module {
func.func @f() {
cf.br ^next
^next:
return
}
}
Generate and replay its failing conversion pipeline:
"$mqt_cc" unsupported.mlir --run-pipeline \
--pass-pipeline='builtin.module(qc-to-qco)' \
--mlir-print-ir-before-all --mlir-print-ir-after-failure \
--mlir-disable-threading \
--mlir-pass-pipeline-crash-reproducer=failure.mlir
"$mqt_cc" failure.mlir --run-reproducer --mlir-print-ir-before-all
Both commands fail with the unsupported unstructured-control-flow diagnostic.
--run-reproducer applies the recorded pipeline, threading, and verification
settings. It requires a non-empty recorded module pipeline and cannot be
combined with a custom pipeline or ordinary compiler output modes. It follows
the recorded verification policy, including disabled verification, and does not
add QCO linearity checks. Ordinary file loading does not activate replay.
Disable threading when generating a local reproducer with
--mlir-pass-pipeline-local-reproducer.
For conversion tracing, add --debug-only=dialect-conversion. This requires
LLVM/MLIR and MQT Core code built with debug logging enabled; build-type names
alone do not establish that support. Keep threading disabled when tracing to
make the output easier to read. Stack addresses and frame counts vary by
platform and are unsuitable regression assertions.