Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions .agent/plans/qir-output-contract.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# QIR output and resource contracts

Status: complete.

## Goal and scope

Close the standalone allocation-verifier gap and repair Adaptive release and
result ownership and shared measurement/store fusion in PR #2446. Keep QC/QCO
allocation ownership at the MQT program boundary. QIR-specific restrictions
belong to conversion, with native diagnostic and semantic regression coverage.

## Decisions

- Standalone QIR and mapping passes invoke the shared MQT allocation check; pass
dependency loading alone does not verify input before execution.
- QIR output preparation supports a single entry-function return. Reject
multiple exits before mutation; use the actual return block rather than
block-list order.
- Preserve quantum releases at their source control-flow positions.
- Adaptive scalar results use dynamic allocation, matching returned result
arrays. Base scalar results retain static IDs. The public QIR builder must
enforce consistent result ownership independently of qubit ownership.
- Fuse same-block measurement/store pairs only with an available index and no
intervening classical interference. Known quantum effects and stores to
distinct constant indices are safe to cross. Reject uncertain cases before
mutation.
- QIR builder finalization releases owned qubits at its current insertion point;
output recording and result releases remain in the output epilogue.

## Validation

Native QC IR, QC-to-QCO, mapping, Base and Adaptive QIR conversion, QIR
IR/builder, compiler, and JIT suites pass: 1,253 tests. Runtime probes also
execute mixed scalar/register results and both conditional-release paths without
ownership errors. Negative tests preserve valid source IR when rejecting
multiple exits or unsafe output stores. Existing target compilation covers
stores to distinct bits across quantum modifiers; `qc.yield` supplies the
effect-free terminator contract needed by recursive effect analysis.

`uvx nox -s lint` and `uvx nox -s cpp-lint` pass. Hosted CI is separate; full
Python and documentation suites were not run locally.

## Outcome

Allocation checks have one implementation, including standalone pass boundaries.
Quantum release control flow and result ownership are explicit. Output
preparation checks its supported subset before mutating returns or stores.
General multiple return normalization and arbitrary classical output-store
lowering remain outside this subset; callers receive diagnostics rather than
incorrect QIR.
55 changes: 55 additions & 0 deletions .agent/plans/quantum-allocation-scope.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Quantum allocation scope

Status: complete; validated locally.

## Goal and scope

Dynamic quantum allocations in QC/QCO programs belong in the entry block of the
function marked `mqt.entry_point`. Helper functions receive quantum resources as
arguments. This covers `qc.alloc`, `qco.alloc`, qubit `memref.alloc`, and
`qtensor.alloc`. Classical allocations and static qubit references are
unchanged.

## Decisions

The MQT entry-point attribute verifier owns the whole-program rule. It already
checks module-level entry-point uniqueness and can inspect all four allocation
forms without extending an upstream operation. QC/QCO program construction loads
the MQT verifier even for caller-supplied contexts and checks modules without an
entry marker. Raw unmarked MLIR fragments can be verified independently;
operation verification alone does not establish this program-wide invariant.

Builders reject invalid allocation placement before creating an operation.
OpenQASM semantic analysis already rejects non-global qubit declarations, and
loop emission restores the entry-block insertion point for later declarations.
The Adaptive conversion no longer scans allocation placement; Mapping discovers
allocations directly in the entry block.

Tests cover all four allocation forms, allowed and forbidden placement, missing
entry markers, caller-supplied contexts, builders, and frontend loop emission.
Pass tests use valid quantum-resource arguments or static references where the
behavior under test does not require allocation.

## Validation

With LLVM/MLIR 23.1.0, the full lint-preset build and all 2,358 configured MLIR
CTest entries passed, including the verifier, compiler, builder, and frontend
regressions. Commands from the repository root:

- `uvx nox -s lint`
- `uvx nox -s cpp-lint -- ec799daa09f855bd0edcbc5592a5fedd90836516`
- `ctest --test-dir build/cpp-lint -L mqt-mlir-unittests --output-on-failure -j8`

Full changed-file C++ lint passed with local clang-tidy 23.0.0git and the macOS
SDK headers configured.

With the built package and test environment active,
`python -m pytest -n4 test/python` passed all 1,131 tests on Python 3.14 with
Qiskit 2.5.2. The revised fixtures preserve whitespace-prefixed input handling
and check that loop-local allocations fail during program construction, before
export.

`uvx nox --non-interactive -s docs` passed with strict reference checking and
all seven executable notebooks. `uvx nox -s lint` passed after these fixture and
documentation fixes; C++ sources are unchanged. These results are local; hosted
CI has not run for this update.
6 changes: 5 additions & 1 deletion mlir/include/mlir/Compiler/Programs.h
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,9 @@ class QCProgram final : public Program {
///
/// The context must own every dialect referenced by the module and must
/// remain the module's context. The factory verifies the module and rejects
/// QCO and QTensor operations. QC operations are not required.
/// QCO and QTensor operations. QC operations are not required. Dynamic
/// quantum allocations require an `mqt.entry_point` function and must appear
/// directly in its entry block.
[[nodiscard]] static std::optional<QCProgram>
fromModule(std::shared_ptr<MLIRContext> context,
OwningOpRef<ModuleOp> moduleOp);
Expand Down Expand Up @@ -237,6 +239,8 @@ class QCOProgram final : public Program {
/// The context must own every dialect referenced by the module and must
/// remain the module's context. The factory verifies the module and QCO
/// linearity and rejects QC operations. QCO operations are not required.
/// Dynamic quantum allocations require an `mqt.entry_point` function and
/// must appear directly in its entry block.
[[nodiscard]] static std::optional<QCOProgram>
fromModule(std::shared_ptr<MLIRContext> context,
OwningOpRef<ModuleOp> moduleOp);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,11 @@ def QCToQIRAdaptive : Pass<"qc-to-qir-adaptive", "mlir::ModuleOp"> {
Requirements:

- Input is a valid module in the QC dialect.
- The entry function must be marked with `mqt.entry_point`.
- The entry function must be marked with `mqt.entry_point` and have a single return.
- Stores to returned CBit registers must share a block with their measurement,
use an index available at measurement (or a constant), and have no
intervening classical memory effects except stores to provably distinct
constant indices of the same register.

Behavior:

Expand All @@ -30,9 +34,13 @@ def QCToQIRAdaptive : Pass<"qc-to-qir-adaptive", "mlir::ModuleOp"> {
0. Initialization block: Sets up the execution environment and performs required runtime initialization.
1. Epilogue block: Records measurement results and returns from the entry function.
Any blocks in-between have no restrictions regarding their operations as long as they are supported.
- Quantum releases retain their original control-flow positions.
- Scalar and register results are dynamically allocated and released after output recording.
- Measurement results may be used as classical values to drive conditional branches.
- Non-quantum dialects are lowered via MLIR's built-in conversions.
}];

let dependentDialects = ["mlir::LLVM::LLVMDialect"];
let dependentDialects = ["mlir::LLVM::LLVMDialect",
"mlir::cf::ControlFlowDialect",
"mlir::arith::ArithDialect"];
}
4 changes: 4 additions & 0 deletions mlir/include/mlir/Conversion/QCToQIR/QIRBase/QCToQIRBase.td
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ def QCToQIRBase : Pass<"qc-to-qir-base", "mlir::ModuleOp"> {

Requirements:

- Stores to returned CBit registers must share a block with their measurement,
use an index available at measurement (or a constant), and have no
intervening classical memory effects except stores to provably distinct
constant indices of the same register.
- Input is a valid module in the QC dialect.
- The entry function must be marked with `mqt.entry_point`.
- The input entry function must consist of a single block.
Expand Down
29 changes: 15 additions & 14 deletions mlir/include/mlir/Conversion/QCToQIR/QIRCommon/QIRCommon.h
Original file line number Diff line number Diff line change
Expand Up @@ -63,12 +63,13 @@ struct LoweringState {
/// Destination register index and bit index of each stored measurement.
DenseMap<Operation*, std::pair<size_t, Value>> cregMeasurements;

/// Map from index to `StaticResult`
DenseMap<int64_t, qir::StaticResult> staticResults;
/// Indexed scalar results, dynamically allocated in Adaptive and static in
/// Base.
DenseMap<int64_t, qir::StaticResult> scalarResults;

/// Metadata for returned static measurement results. Each entry is a defining
/// Metadata for returned scalar measurement results. Each entry is a defining
/// `qc::MeasureOp`
DenseSet<Operation*> returnedStaticResults;
DenseSet<Operation*> returnedScalarResults;

/// Converted controls associated with their specific body unitary.
DenseMap<Operation*, SmallVector<Value>> controlledGates;
Expand Down Expand Up @@ -172,15 +173,15 @@ void addOutputRecording(LLVM::LLVMFuncOp& main, MLIRContext* ctx,
* @brief Prepares classical result registers for QIR conversion
*
* @details
* Inventories classical result registers, records the destination of each
* stored measurement, consumes supported classical-register stores, and strips
* classical results from `func::ReturnOp` operations so QIR output recording
* can replace them.
*
* A direct measurement-result store is consumed because the QIR measurement
* call writes to the corresponding result slot. Other classical-register
* stores are rejected. Register initialization comes from `cbit.alloc` and
* needs no operation-order recognition.
* Requires a single entry-function return. Inventories classical result
* registers and validates output stores before rewriting returns or stores.
* A returned-register store must share a block with its measurement and use
* an index available there (or a constant). Intervening operations must be
* effect-free, affect only quantum resources, or store to a provably distinct
* constant index of the same register. The QIR measurement can then write
* directly to the destination without changing observable order or control
* flow. Other stores to returned registers are rejected; local CBit stores
* retain their ordinary semantics.
*
* This must be called **before** func-to-LLVM conversion, while
* `func::ReturnOp`, `qc::MeasureOp`, and `cbit::StoreOp` are still in the IR.
Expand All @@ -196,6 +197,6 @@ void addOutputRecording(LLVM::LLVMFuncOp& main, MLIRContext* ctx,
* returned classical bit register
*/
Value getResultPtr(LoweringState& state, Operation* op,
ConversionPatternRewriter& rewriter);
ConversionPatternRewriter& rewriter, bool dynamic);

} // namespace mlir
6 changes: 6 additions & 0 deletions mlir/include/mlir/Dialect/MQT/IR/MQTDialect.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
#include <mlir/IR/BuiltinOps.h>
#include <mlir/IR/Dialect.h>
#include <mlir/IR/Operation.h>
#include <mlir/Support/LogicalResult.h>

//===----------------------------------------------------------------------===//
// Dialect
Expand Down Expand Up @@ -53,4 +54,9 @@ void setUnitaryFunction(Operation* operation);
}
return nullptr;
}

/// Check that dynamic quantum allocations belong to the program entry block.
/// Modules without an entry point must not contain dynamic quantum allocations.
/// Nested modules have separate program scopes.
[[nodiscard]] LogicalResult verifyQuantumAllocations(ModuleOp moduleOp);
} // namespace mlir::mqt
6 changes: 5 additions & 1 deletion mlir/include/mlir/Dialect/MQT/IR/MQTDialect.td
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,11 @@ def MQTDialect : Dialect {
register allocation. Input and register names share one function-wide
namespace.
`mqt.entry_point` marks the single public, defined `func.func` program entry
in a module.
in a module. Dynamic quantum allocations (`qc.alloc`, `qco.alloc`,
qubit `memref.alloc`, and `qtensor.alloc`) must appear directly in its
entry block. Other functions receive quantum resources as arguments;
they cannot allocate them. Classical allocations and static qubit references
are not subject to this restriction.
`mqt.unitary` marks a private function that defines a unitary operation.
Its body admits unitary operations and memory-effect-free, region-free
classical computation, matching the quantum modifier-body contract. Parameter
Expand Down
7 changes: 7 additions & 0 deletions mlir/include/mlir/Dialect/QC/Builder/QCProgramBuilder.h
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ namespace qc {
/// allocation
/// (`allocQubit` / `allocQubitRegister`), never both. The builder terminates
/// with a usage error if the modes are mixed.
/// Dynamic allocation is only allowed directly in the entry block of the
/// `mqt.entry_point` function. Helpers receive allocated qubits as arguments.
///
/// @par Example Usage:
/// ```c++
Expand Down Expand Up @@ -120,6 +122,7 @@ class QCProgramBuilder final : public ImplicitLocOpBuilder {
/// Create a complete private function and infer its result types.
///
/// Borrowed qubit arguments are updated in place and must not be returned.
/// The body must not dynamically allocate qubits or qubit registers.
func::FuncOp
createFunction(StringRef name, TypeRange argumentTypes,
function_ref<SmallVector<Value>(ValueRange)> body);
Expand Down Expand Up @@ -169,6 +172,7 @@ class QCProgramBuilder final : public ImplicitLocOpBuilder {
};

/// Allocate a single qubit initialized to |0⟩
/// Requires an insertion point in the entry block of `mqt.entry_point`.
/// @return A qubit reference
///
/// @par Example:
Expand Down Expand Up @@ -196,6 +200,7 @@ class QCProgramBuilder final : public ImplicitLocOpBuilder {
/// Allocate a qubit register and eagerly load every element.
///
/// Every allocated qubit is initialized to |0⟩.
/// Requires an insertion point in the entry block of `mqt.entry_point`.
///
/// \param size Number of qubits; must be positive.
/// \param name Optional source-level register name.
Expand All @@ -217,6 +222,7 @@ class QCProgramBuilder final : public ImplicitLocOpBuilder {
/// Every allocated qubit is initialized to |0⟩. The builder tracks the
/// register for automatic deallocation. Use `loadQubit` to obtain references
/// at their points of use.
/// Requires an insertion point in the entry block of `mqt.entry_point`.
///
/// \param size Number of qubits; must be positive.
/// \param name Optional source-level register name.
Expand Down Expand Up @@ -1375,6 +1381,7 @@ class QCProgramBuilder final : public ImplicitLocOpBuilder {
AllocationMode allocationMode = AllocationMode::Unset;

/// Ensure static and dynamic qubit allocation modes are not mixed.
/// Dynamic allocation also requires the entry-point entry block.
void ensureAllocationMode(AllocationMode requestedMode);
};
} // namespace qc
Expand Down
5 changes: 5 additions & 0 deletions mlir/include/mlir/Dialect/QC/IR/QCDialect.td
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ def QCDialect : Dialect {

The name "QC" stands for "Quantum Circuit."

In a program, dynamic qubit and qubit-register allocations must be in
the entry block of the `mqt.entry_point` function. Helper functions
receive quantum resources as arguments. The MQT entry-point verifier
checks this rule across the program.

Example:
```mlir
qc.h %q // Applies Hadamard to qubit %q in place
Expand Down
2 changes: 1 addition & 1 deletion mlir/include/mlir/Dialect/QC/IR/QCOps.td
Original file line number Diff line number Diff line change
Expand Up @@ -1021,7 +1021,7 @@ def CallOp
// Modifiers
//===----------------------------------------------------------------------===//

def YieldOp : QCOp<"yield", traits = [Terminator]> {
def YieldOp : QCOp<"yield", traits = [Pure, Terminator]> {
let summary = "Yield from a modifier region";
let description = [{
Terminates a modifier region, yielding control back to the enclosing operation.
Expand Down
7 changes: 7 additions & 0 deletions mlir/include/mlir/Dialect/QCO/Builder/QCOProgramBuilder.h
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ namespace qco {
/// allocation
/// (`allocQubit`, `allocQubitRegister`, or `qtensorAlloc`), never both. The
/// builder terminates with a usage error if the modes are mixed.
/// Dynamic allocation is only allowed directly in the entry block of the
/// `mqt.entry_point` function. Helpers receive allocated qubits as arguments.
///
/// @par Example Usage:
/// ```c++
Expand Down Expand Up @@ -106,6 +108,7 @@ class QCOProgramBuilder final : public ImplicitLocOpBuilder {
///
/// The callback must return one trailing qubit for every qubit argument, in
/// qubit-argument order.
/// The body must not dynamically allocate qubits or qubit tensors.
func::FuncOp
createFunction(StringRef name, TypeRange argumentTypes,
function_ref<SmallVector<Value>(ValueRange)> body);
Expand Down Expand Up @@ -260,6 +263,7 @@ class QCOProgramBuilder final : public ImplicitLocOpBuilder {
};

/// Allocate a single qubit initialized to |0⟩
/// Requires an insertion point in the entry block of `mqt.entry_point`.
/// @return A tracked qubit handle (convertible to `Value`)
///
/// @par Example:
Expand All @@ -285,6 +289,7 @@ class QCOProgramBuilder final : public ImplicitLocOpBuilder {
Qubit staticQubit(uint64_t index);

/// Allocate a qubit tensor and eagerly extract every element
/// Requires an insertion point in the entry block of `mqt.entry_point`.
/// @param size Number of qubits (must be positive)
/// @param name Optional source-level register name
/// @return A `QubitRegister` containing the residual tensor and one
Expand Down Expand Up @@ -342,6 +347,7 @@ class QCOProgramBuilder final : public ImplicitLocOpBuilder {
/// `!qco.qubit` values. No elements are extracted. If the size is a constant,
/// the tensor has static size; otherwise it has dynamic size. Its qubits are
/// initialized in the |0> state, and the tensor is tracked automatically.
/// Requires an insertion point in the entry block of `mqt.entry_point`.
///
/// @param size Number of qubits (must be positive)
/// @return The allocated tensor
Expand Down Expand Up @@ -1946,6 +1952,7 @@ class QCOProgramBuilder final : public ImplicitLocOpBuilder {
AllocationMode allocationMode = AllocationMode::Unset;

/// Ensure static and dynamic qubit allocation modes are not mixed.
/// Dynamic allocation also requires the entry-point entry block.
void ensureAllocationMode(AllocationMode requestedMode);
};
} // namespace qco
Expand Down
5 changes: 5 additions & 0 deletions mlir/include/mlir/Dialect/QCO/IR/QCODialect.td
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ def QCODialect : Dialect {

The name "QCO" stands for "Quantum Circuit Optimization."

In a program, dynamic qubit and qubit-tensor allocations must be in
the entry block of the `mqt.entry_point` function. Helper functions
receive quantum resources as arguments. The MQT entry-point verifier
checks this rule across the program.

Example:
```mlir
%q_out = qco.h %q_in // Consumes %q_in, produces %q_out
Expand Down
Loading
Loading