Skip to content

🚧 Heuristic Mapping Pass#1537

Open
MatthiasReumann wants to merge 17 commits intomainfrom
feat/heuristic-mapping-pass
Open

🚧 Heuristic Mapping Pass#1537
MatthiasReumann wants to merge 17 commits intomainfrom
feat/heuristic-mapping-pass

Conversation

@MatthiasReumann
Copy link
Collaborator

@MatthiasReumann MatthiasReumann commented Mar 3, 2026

Description

This pull request reimplements the heuristic mapping pass of the old mqtopt dialect. Similarly to #1510 the mapping pass ignores structured control flow (for now). I've copied the CMake setup of #1206.

A key difference between the implementations is that here I avoid building the "fat" Layout object (which keeps track of the SSA values) and use the WireIterators and the "lightweight" Layout instead. Moreover, just because I wanted to highlight how cool the WireIterator is, this pull request adds LightSABRE-like back and forth traversal to find a better initial layout.

Lastly, I think it should be possible to use the ideas implemented here for a unitized IR also.

Checklist:

  • The pull request only contains commits that are focused and relevant to this change.
  • I have added appropriate tests that cover the new/changed functionality.
  • I have updated the documentation to reflect these changes.
  • I have added entries to the changelog for any noteworthy additions, changes, fixes, or removals.
  • I have added migration instructions to the upgrade guide (if needed).
  • The changes follow the project's style guidelines and introduce no new warnings.
  • The changes are fully tested and pass the CI checks.
  • I have reviewed my own code changes.

@MatthiasReumann MatthiasReumann added c++ Anything related to C++ code MLIR Anything related to MLIR labels Mar 3, 2026
@MatthiasReumann MatthiasReumann changed the title ✨Heuristic Mapping Pass 🚧 Heuristic Mapping Pass Mar 3, 2026
@codecov
Copy link

codecov bot commented Mar 3, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@MatthiasReumann
Copy link
Collaborator Author

@coderabbitai full review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 4, 2026

✅ Actions performed

Full review triggered.

@MatthiasReumann MatthiasReumann marked this pull request as ready for review March 4, 2026 13:53
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 4, 2026

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added a new HeuristicMappingPass for quantum circuit-to-hardware mapping, enabling automated qubit placement and SWAP insertion for target architectures.
    • Introduced architecture modeling with configurable parameters (architecture name, lookahead depth, decay factors, iteration count).
    • Integrated pass into the compiler pipeline.
  • Tests

    • Added unit tests for the heuristic mapping pass with example quantum circuits.

Walkthrough

Introduces a new MLIR passes library for quantum circuit mapping, featuring an Architecture class that models quantum accelerator connectivity, a HeuristicMappingPass implementing A* routing for qubit layout optimization, and comprehensive build system integration with unit tests for validation.

Changes

Cohort / File(s) Summary
Build Configuration
mlir/include/mlir/CMakeLists.txt, mlir/lib/CMakeLists.txt, mlir/lib/Compiler/CMakeLists.txt
Reorders subdirectory declarations, adds new Passes subdirectory, and updates MQTCompilerPipeline to include QcoPasses as a public dependency.
Passes CMake & TableGen
mlir/include/mlir/Passes/CMakeLists.txt, mlir/include/mlir/Passes/Passes.h, mlir/include/mlir/Passes/Passes.td
Establishes pass build infrastructure with CMake configuration, generates pass declarations via TableGen, and defines HeuristicMappingPass with configurable parameters (archName, nlookahead, alpha, lambda, repeats).
Architecture Model
mlir/include/mlir/Passes/Mapping/Architecture.h, mlir/lib/Passes/Mapping/Architecture.cpp
Implements Architecture class that models quantum accelerator topology with distance computation via Floyd–Warshall, adjacency checks, and neighbour retrieval for routing algorithms.
Heuristic Mapping Pass
mlir/lib/Passes/Mapping/HeuristicMapping.cpp
Implements HeuristicMappingPass with Layout management, A* search-based qubit mapping, layer-based cold/hot routing, and swap insertion to satisfy target architecture constraints.
Pass Library Build
mlir/lib/Passes/CMakeLists.txt
Configures QcoPasses library target with source/build include directories and public header exposure.
Unit Tests
mlir/unittests/CMakeLists.txt, mlir/unittests/Passes/CMakeLists.txt, mlir/unittests/Passes/Mapping/CMakeLists.txt, mlir/unittests/Passes/Mapping/test_heuristic_mapping.cpp
Adds test infrastructure and comprehensive unit tests for HeuristicMappingPass validation on GHZ and Sabre quantum circuits against Rigetti Novera architecture.

Sequence Diagram

sequenceDiagram
    participant Client as QC Program
    participant Pass as HeuristicMappingPass
    participant Arch as Architecture
    participant Search as A* Search
    participant Layout as Layout Manager
    participant IR as MLIR IR

    Client->>Pass: runOnOperation()
    Pass->>Pass: collectDynamicQubits()
    Pass->>Pass: computeBidirectionalLayers()
    Pass->>Pass: collectLayers()
    
    loop For each repeat iteration
        Pass->>Search: search(currentLayout, layers)
        Search->>Arch: areAdjacent(u, v)
        Arch-->>Search: adjacency result
        Search->>Arch: distanceBetween(u, v)
        Arch-->>Search: distance via Floyd-Warshall
        Search->>Layout: evaluate swap candidates
        Layout-->>Search: goal check result
        Search-->>Pass: optimal swap sequence
        Pass->>Layout: apply swaps
    end
    
    Pass->>Pass: place() - map to static qubits
    Pass->>Pass: routeHot() - insert SWAPs in IR
    Pass->>IR: emit SWAPOp instructions
    IR-->>Client: mapped QCO program
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • 🗺️ MLIR Dialect Redesign #1264: Establishes the foundational QCO passes infrastructure, CMake build configuration, and pass declaration mechanisms that this PR extends with concrete pass implementations.
  • ✨ MLIR | Implement QMAP Algorithm #1237: Introduces overlapping A* routing algorithms and Architecture distance/adjacency abstractions that are refined and integrated into the HeuristicMappingPass implementation.

Suggested labels

feature

Suggested reviewers

  • ystade
  • burgholzer

Poem

🐰 A heuristic hop through quantum gates, where swaps align with architecture's fates—
Layout dances with A* search so bright, mapping qubits to hardware just right.
Floyd–Warshall whispers distances true, guiding our routing to what qubits can do. 🌟

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 51.22% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title uses an emoji and vague phrasing ('Heuristic Mapping Pass') that doesn't clearly convey the scope or primary change despite being related to the changeset. Consider using a more descriptive title without emojis, such as 'Add heuristic mapping pass for quantum circuit placement' to improve clarity for team members scanning history.
✅ Passed checks (1 passed)
Check name Status Explanation
Description check ✅ Passed The PR description covers the main changes (heuristic mapping pass reimplementation, architecture choices, layout improvements), provides context (references to prior PRs), and includes the required checklist template with items identified as needing completion.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/heuristic-mapping-pass

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@mlir/lib/Compiler/CMakeLists.txt`:
- Line 23: The target currently links QcoPasses as a PUBLIC dependency, exposing
it transitively; change the linkage to PRIVATE so QcoPasses is not exported in
the public interface. Locate the target_link_libraries line that references
QcoPasses in the mlir/lib/Compiler/CMakeLists.txt and replace PUBLIC with
PRIVATE (keeping the same target name and ordering), since public headers under
mlir/include/mlir/Compiler/* do not expose QcoPasses symbols and only
implementation uses passes like QCToQCO, QCOToQC, and QCToQIR.

In `@mlir/lib/Passes/Mapping/Architecture.cpp`:
- Around line 44-47: The loop in Architecture.cpp uses u and v from couplingSet_
as direct indices into dist_, prev_ and neighbours_ without bounds checks;
validate that u and v are within the valid ranges before indexing (e.g., ensure
u < dist_.size() and v < dist_[u].size() and v < prev_[u].size() and u/v <
neighbours_.size() as appropriate), and if an index is out-of-range either skip
the pair (continue) or emit a clear error/diagnostic; apply the same validation
to the other occurrences where couplingSet_ pairs are indexed (the references
around the lines corresponding to the second occurrence at 70-71) and use the
unique symbols couplingSet_, dist_, prev_, neighbours_ to locate and fix all
spots.

In `@mlir/lib/Passes/Mapping/HeuristicMapping.cpp`:
- Line 331: Remove the unconditional call to layout.dump() from the normal pass
path and only print when debugging is enabled: locate the call to layout.dump()
in HeuristicMapping (the pass implementation in HeuristicMapping.cpp) and wrap
it with the pass/module debug check (e.g., a conditional using LLVM_DEBUG,
DEBUG_TYPE, or a isDebugEnabled-style flag used in this codebase) so that
layout.dump() executes only under debug logging; ensure you reference the same
debug macro or helper used elsewhere in this file to keep behavior consistent.
- Around line 313-321: The code assumes the number of dynamic qubits (dyn from
collectDynamicQubits) fits the hardware; if dyn.size() > arch.nqubits()
subsequent Layout lookups (Layout::identity, etc.) will index out of range; add
a guard after computing dyn (and before Layout::identity and the SABRE loop)
that checks if dyn.size() > arch.nqubits() and handle it deterministically
(e.g., return a failed LogicalResult/Optional, emit an error/message via the
existing logging/diagnostic mechanism, or early-clamp/resize mappings) so later
uses of layout and any indexing into arch.nqubits()-sized structures are safe;
reference symbols: collectDynamicQubits, dyn, arch.nqubits(), Layout::identity,
and the repeats loop.
- Around line 303-309: The code currently hardcodes the architecture when
constructing Architecture arch(...) which ignores the user-provided -arch
option; update the constructor to use the pass option (e.g. this->archName)
instead of the literal "RigettiNovera" and avoid hardcoding the qubit
count/edges: replace the call Architecture arch("RigettiNovera", 9, {...}) with
a construction that uses the selected architecture name and its associated
topology (for example Architecture arch(this->archName, /*numQubits*/
this->nqubits or query a helper that returns the node count and edge list for
archName) or call a factory like Architecture::fromName(this->archName)),
ensuring the code references the pass field (this->archName) so the -arch option
is honored; keep Parameters weights(this->alpha, this->lambda, this->nlookahead)
as-is.
- Around line 609-620: The SWAPOp is being created with a stale insertion point
because rewriter.create<SWAPOp>(unknown, ...) is called without first moving the
insertion point; before calling rewriter.create in the loop that iterates over
swaps (the block using wires, swaps, in0, in1 and then calling
rewriter.replaceAllUsesExcept), set the rewriter insertion point to the current
wire/front location (e.g., use the appropriate rewriter.setInsertionPoint or
setInsertionPointAfter API on the operation representing the wire front) so the
SWAPOp is inserted at the correct place relative to in0/in1, then create the
SWAPOp and perform replaceAllUsesExcept as shown.
- Line 478: The variable "step" is declared with the unsigned type std::size_t
but needs to hold negative values for Direction::Backward; change its type to a
signed pointer-difference type (e.g., std::ptrdiff_t) so it matches
WireIterator::difference_type and avoids underflow when set to -1; update the
declaration near the constexpr std::size_t step = d == Direction::Forward ? 1 :
-1; (used with std::ranges::advance and wire iteration) to use std::ptrdiff_t
(or WireIterator::difference_type) instead.

In `@mlir/unittests/Passes/Mapping/test_heuristic_mapping.cpp`:
- Line 135: Remove the unconditional IR dumps from the test by deleting or
gating the module->dump() calls so they do not print on every successful run;
locate the invocations of module->dump() in test_heuristic_mapping.cpp (the
calls at and near the two occurrences noted) and either remove them or wrap them
behind a debug/verbose guard (e.g., LLVM_DEBUG/if (verbose)) so CI output is not
noisy while preserving the ability to enable dumps for debugging.
- Around line 66-73: The current code uses mappings[op.getQubit(...)] which can
insert default entries and only checks op.getNumQubits() > 1; change this to
defensively lookup qubit indices (e.g., use mappings.find or contains) for
op.getQubit(0) and op.getQubit(1), verify both exist before using them, and
explicitly require op.getNumQubits() == 2 (reject ops with 0, 1, or >2 qubits) —
if a lookup fails or the arity is not 2, set executable = false and return
WalkResult::interrupt() (retain checks using arch.areAdjacent(i0, i1) only after
successful lookups).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: dccba142-3321-49bd-bcfa-8639de6bb40c

📥 Commits

Reviewing files that changed from the base of the PR and between cb7c22c and 4586450.

📒 Files selected for processing (14)
  • mlir/include/mlir/CMakeLists.txt
  • mlir/include/mlir/Passes/CMakeLists.txt
  • mlir/include/mlir/Passes/Mapping/Architecture.h
  • mlir/include/mlir/Passes/Passes.h
  • mlir/include/mlir/Passes/Passes.td
  • mlir/lib/CMakeLists.txt
  • mlir/lib/Compiler/CMakeLists.txt
  • mlir/lib/Passes/CMakeLists.txt
  • mlir/lib/Passes/Mapping/Architecture.cpp
  • mlir/lib/Passes/Mapping/HeuristicMapping.cpp
  • mlir/unittests/CMakeLists.txt
  • mlir/unittests/Passes/CMakeLists.txt
  • mlir/unittests/Passes/Mapping/CMakeLists.txt
  • mlir/unittests/Passes/Mapping/test_heuristic_mapping.cpp

QCToQCO
QCOToQC
QCToQIR
QcoPasses
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

🧩 Analysis chain

🏁 Script executed:

# First, check the actual CMakeLists.txt file to see current state
cat -n mlir/lib/Compiler/CMakeLists.txt | head -40

Repository: munich-quantum-toolkit/core

Length of output: 1195


🏁 Script executed:

# Check for QcoPasses references in public headers
fd -e h -e hpp . mlir/include/mlir/Compiler -type f -exec grep -l "QcoPasses\|qco::create\|HeuristicMapping" {} \;

Repository: munich-quantum-toolkit/core

Length of output: 241


🏁 Script executed:

# Check what's actually in the public headers
find mlir/include/mlir/Compiler -type f \( -name "*.h" -o -name "*.hpp" \) -exec basename {} \;

Repository: munich-quantum-toolkit/core

Length of output: 89


🏁 Script executed:

# Search for QcoPasses usage in CompilerPipeline implementation
rg -n "QcoPasses|qco::.*Pass|HeuristicMapping|Passes\.h" mlir/lib/Compiler/CompilerPipeline.cpp

Repository: munich-quantum-toolkit/core

Length of output: 109


🏁 Script executed:

# Check if CompilerPipeline.h (public header) references QcoPasses
cat mlir/include/mlir/Compiler/CompilerPipeline.h

Repository: munich-quantum-toolkit/core

Length of output: 4359


🏁 Script executed:

# Check the actual CompilerPipeline.cpp file to see QcoPasses usage
cat mlir/lib/Compiler/CompilerPipeline.cpp | head -100

Repository: munich-quantum-toolkit/core

Length of output: 3215


🏁 Script executed:

# Search more broadly for any QCO-related pass references in the implementation
rg -n "qco::|QcoPasses|HeuristicMapping|Dialects/QCO" mlir/lib/Compiler/CompilerPipeline.cpp

Repository: munich-quantum-toolkit/core

Length of output: 53


🏁 Script executed:

# Check what headers are included in CompilerPipeline.cpp
rg -n "^#include" mlir/lib/Compiler/CompilerPipeline.cpp

Repository: munich-quantum-toolkit/core

Length of output: 557


🏁 Script executed:

# Check the full CompilerPipeline.cpp implementation to see if QcoPasses is referenced
wc -l mlir/lib/Compiler/CompilerPipeline.cpp

Repository: munich-quantum-toolkit/core

Length of output: 113


🏁 Script executed:

# Look at the entire CompilerPipeline.cpp implementation to find all pass additions
sed -n '50,250p' mlir/lib/Compiler/CompilerPipeline.cpp

Repository: munich-quantum-toolkit/core

Length of output: 5918


🏁 Script executed:

# Search for any reference to QcoPasses or qco-specific passes in the file
grep -n "addPass\|createHeuristicMapping\|QcoPasses" mlir/lib/Compiler/CompilerPipeline.cpp

Repository: munich-quantum-toolkit/core

Length of output: 268


🏁 Script executed:

# Check what QcoPasses library actually provides (CMakeLists for QcoPasses)
find mlir -name CMakeLists.txt -exec grep -l "QcoPasses" {} \;

Repository: munich-quantum-toolkit/core

Length of output: 219


🏁 Script executed:

# Check the QcoPasses CMakeLists to see what it provides and depends on
cat mlir/lib/Passes/CMakeLists.txt

Repository: munich-quantum-toolkit/core

Length of output: 1070


🏁 Script executed:

# Check if there's a QcoPasses header being exposed
find mlir/include -name "*QcoPasses*" -o -name "*Passes.h" | grep -i qco

Repository: munich-quantum-toolkit/core

Length of output: 53


🏁 Script executed:

# Check mlir/include/mlir/Passes/CMakeLists.txt
cat mlir/include/mlir/Passes/CMakeLists.txt

Repository: munich-quantum-toolkit/core

Length of output: 478


🏁 Script executed:

# Double-check: confirm CompilerPipeline.cpp has no include directives for QcoPasses headers
rg "mlir/Passes|QcoPasses" mlir/lib/Compiler/CompilerPipeline.cpp

Repository: munich-quantum-toolkit/core

Length of output: 53


🏁 Script executed:

# Check if any file in mlir/include/mlir/Compiler/ references QcoPasses
rg "QcoPasses|mlir/Passes/Passes.h" mlir/include/mlir/Compiler/

Repository: munich-quantum-toolkit/core

Length of output: 53


🏁 Script executed:

# Verify what's actually in the Passes include directory
ls -la mlir/include/mlir/Passes/

Repository: munich-quantum-toolkit/core

Length of output: 420


🏁 Script executed:

# Final verification: Check what createQCToQCO, createQCOToQC, createQCToQIR actually come from
rg -l "createQCToQCO|createQCOToQC|createQCToQIR" mlir/include/mlir/Conversion/

Repository: munich-quantum-toolkit/core

Length of output: 53


🏁 Script executed:

# Confirm that the pass creation functions are from conversion headers, not QcoPasses
rg "createQCToQCO|createQCOToQC|createQCToQIR" mlir/include/mlir/Conversion/ -A2

Repository: munich-quantum-toolkit/core

Length of output: 53


🏁 Script executed:

# Check what headers are in mlir/include/mlir/Passes/
cat mlir/include/mlir/Passes/Passes.h

Repository: munich-quantum-toolkit/core

Length of output: 912


🏁 Script executed:

# Verify the Conversion headers don't depend on QcoPasses
rg "QcoPasses|mlir/Passes/Passes.h" mlir/include/mlir/Conversion/QCToQCO/ mlir/include/mlir/Conversion/QCOToQC/ mlir/include/mlir/Conversion/QCToQIR/

Repository: munich-quantum-toolkit/core

Length of output: 53


Consider making QcoPasses a PRIVATE dependency.

Line 23 currently exports QcoPasses transitively via PUBLIC. The public headers (mlir/include/mlir/Compiler/*) do not expose any QcoPasses symbols, and the implementation only uses conversion passes (QCToQCO, QCOToQC, QCToQIR) and standard MLIR transformation passes. Moving QcoPasses to PRIVATE keeps the public link surface minimal.

♻️ Possible CMake adjustment
 add_mlir_library(
   MQTCompilerPipeline
   CompilerPipeline.cpp
   ADDITIONAL_HEADER_DIRS
   ${MQT_MLIR_SOURCE_INCLUDE_DIR}/mlir/Compiler
   LINK_LIBS
   PUBLIC
   MLIRPass
   MLIRTransforms
   MLIRTransformUtils
   QCToQCO
   QCOToQC
   QCToQIR
-  QcoPasses
   MQT::MLIRSupport
+  PRIVATE
+  QcoPasses
   DISABLE_INSTALL)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mlir/lib/Compiler/CMakeLists.txt` at line 23, The target currently links
QcoPasses as a PUBLIC dependency, exposing it transitively; change the linkage
to PRIVATE so QcoPasses is not exported in the public interface. Locate the
target_link_libraries line that references QcoPasses in the
mlir/lib/Compiler/CMakeLists.txt and replace PUBLIC with PRIVATE (keeping the
same target name and ordering), since public headers under
mlir/include/mlir/Compiler/* do not expose QcoPasses symbols and only
implementation uses passes like QCToQCO, QCOToQC, and QCToQIR.

Comment on lines +44 to +47
for (const auto& [u, v] : couplingSet_) {
dist_[u][v] = 1;
prev_[u][v] = u;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Validate coupling-map indices before indexing matrices/vectors.

u/v from couplingSet_ are used as direct indices without range checks. Invalid input can trigger OOB access in dist_, prev_, and neighbours_.

🛡️ Proposed fix
 void Architecture::floydWarshallWithPathReconstruction() {
   for (const auto& [u, v] : couplingSet_) {
+    if (u >= nqubits_ || v >= nqubits_) {
+      report_fatal_error("coupling edge out of range: (" + Twine(u) + ", " +
+                         Twine(v) + ") for architecture size " +
+                         Twine(nqubits_));
+    }
     dist_[u][v] = 1;
     prev_[u][v] = u;
   }

Also applies to: 70-71

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mlir/lib/Passes/Mapping/Architecture.cpp` around lines 44 - 47, The loop in
Architecture.cpp uses u and v from couplingSet_ as direct indices into dist_,
prev_ and neighbours_ without bounds checks; validate that u and v are within
the valid ranges before indexing (e.g., ensure u < dist_.size() and v <
dist_[u].size() and v < prev_[u].size() and u/v < neighbours_.size() as
appropriate), and if an index is out-of-range either skip the pair (continue) or
emit a clear error/diagnostic; apply the same validation to the other
occurrences where couplingSet_ pairs are indexed (the references around the
lines corresponding to the second occurrence at 70-71) and use the unique
symbols couplingSet_, dist_, prev_, neighbours_ to locate and fix all spots.

Comment on lines +303 to +309
Parameters weights(this->alpha, this->lambda, this->nlookahead);
// TODO: Hardcoded architecture.
Architecture arch("RigettiNovera", 9,
{{0, 3}, {3, 0}, {0, 1}, {1, 0}, {1, 4}, {4, 1},
{1, 2}, {2, 1}, {2, 5}, {5, 2}, {3, 6}, {6, 3},
{3, 4}, {4, 3}, {4, 7}, {7, 4}, {4, 5}, {5, 4},
{5, 8}, {8, 5}, {6, 7}, {7, 6}, {7, 8}, {8, 7}});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

The -arch pass option is currently ignored.

Line 305 hardcodes RigettiNovera regardless of archName, so user-supplied architecture selection has no effect.

🔧 Proposed fix
-    // TODO: Hardcoded architecture.
+    if (!archName.empty() && archName != "RigettiNovera") {
+      getOperation()->emitError()
+          << "unsupported architecture '" << archName
+          << "' (currently only 'RigettiNovera' is implemented)";
+      signalPassFailure();
+      return;
+    }
     Architecture arch("RigettiNovera", 9,
                       {{0, 3}, {3, 0}, {0, 1}, {1, 0}, {1, 4}, {4, 1},
                        {1, 2}, {2, 1}, {2, 5}, {5, 2}, {3, 6}, {6, 3},
                        {3, 4}, {4, 3}, {4, 7}, {7, 4}, {4, 5}, {5, 4},
                        {5, 8}, {8, 5}, {6, 7}, {7, 6}, {7, 8}, {8, 7}});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mlir/lib/Passes/Mapping/HeuristicMapping.cpp` around lines 303 - 309, The
code currently hardcodes the architecture when constructing Architecture
arch(...) which ignores the user-provided -arch option; update the constructor
to use the pass option (e.g. this->archName) instead of the literal
"RigettiNovera" and avoid hardcoding the qubit count/edges: replace the call
Architecture arch("RigettiNovera", 9, {...}) with a construction that uses the
selected architecture name and its associated topology (for example Architecture
arch(this->archName, /*numQubits*/ this->nqubits or query a helper that returns
the node count and edge list for archName) or call a factory like
Architecture::fromName(this->archName)), ensuring the code references the pass
field (this->archName) so the -arch option is honored; keep Parameters
weights(this->alpha, this->lambda, this->nlookahead) as-is.

Comment on lines +313 to +321
const auto dyn = collectDynamicQubits(func.getFunctionBody());
const auto [ltr, rtl] = computeBidirectionalLayers(dyn);

// Use the SABRE Approach to improve the initial layout choice (here:
// identity): Traverse the layers from left-to-right-to-left and
// cold-route along the way. Repeat this procedure "repeats" times.

Layout layout = Layout::identity(arch.nqubits());
for (std::size_t r = 0; r < this->repeats; ++r) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Guard against circuits that exceed hardware capacity.

If dyn.size() > arch.nqubits(), later layout lookups index out of range and can hit UB in release builds.

🛡️ Proposed fix
       const auto dyn = collectDynamicQubits(func.getFunctionBody());
+      if (dyn.size() > arch.nqubits()) {
+        func.emitError() << "heuristic mapping requires at most "
+                         << arch.nqubits() << " qubits, got " << dyn.size();
+        signalPassFailure();
+        return;
+      }
       const auto [ltr, rtl] = computeBidirectionalLayers(dyn);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mlir/lib/Passes/Mapping/HeuristicMapping.cpp` around lines 313 - 321, The
code assumes the number of dynamic qubits (dyn from collectDynamicQubits) fits
the hardware; if dyn.size() > arch.nqubits() subsequent Layout lookups
(Layout::identity, etc.) will index out of range; add a guard after computing
dyn (and before Layout::identity and the SABRE loop) that checks if dyn.size() >
arch.nqubits() and handle it deterministically (e.g., return a failed
LogicalResult/Optional, emit an error/message via the existing
logging/diagnostic mechanism, or early-clamp/resize mappings) so later uses of
layout and any indexing into arch.nqubits()-sized structures are safe; reference
symbols: collectDynamicQubits, dyn, arch.nqubits(), Layout::identity, and the
repeats loop.

return;
}
}
layout.dump();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Remove unconditional debug output from the normal pass path.

Line 331 prints layout data for every function execution; this should be debug-gated.

🧹 Proposed fix
-      layout.dump();
+      LLVM_DEBUG(layout.dump());
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
layout.dump();
LLVM_DEBUG(layout.dump());
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mlir/lib/Passes/Mapping/HeuristicMapping.cpp` at line 331, Remove the
unconditional call to layout.dump() from the normal pass path and only print
when debugging is enabled: locate the call to layout.dump() in HeuristicMapping
(the pass implementation in HeuristicMapping.cpp) and wrap it with the
pass/module debug check (e.g., a conditional using LLVM_DEBUG, DEBUG_TYPE, or a
isDebugEnabled-style flag used in this codebase) so that layout.dump() executes
only under debug logging; ensure you reference the same debug macro or helper
used elsewhere in this file to keep behavior consistent.

*/
template <Direction d>
static SmallVector<Layer> collectLayers(MutableArrayRef<WireIterator> wires) {
constexpr std::size_t step = d == Direction::Forward ? 1 : -1;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Verify the unsigned-backstep declaration is present
rg -nP 'constexpr\s+std::size_t\s+step\s*=\s*d\s*==\s*Direction::Forward\s*\?\s*1\s*:\s*-1' mlir/lib/Passes/Mapping/HeuristicMapping.cpp

# Also check the file structure around this line
echo "---"
echo "Context around line 478:"
sed -n '470,490p' mlir/lib/Passes/Mapping/HeuristicMapping.cpp

Repository: munich-quantum-toolkit/core

Length of output: 974


🏁 Script executed:

# Check how the 'step' variable is used
rg -A 10 -B 5 'constexpr.*step.*Direction::Forward' mlir/lib/Passes/Mapping/HeuristicMapping.cpp

Repository: munich-quantum-toolkit/core

Length of output: 622


🏁 Script executed:

# Search for LLVM equivalents or patterns in MLIR codebase
echo "Searching for LLVM equivalents to std::size_t/std::ptrdiff_t..."
rg -t cpp 'llvm::ssize_t|llvm::ptrdiff_t|llvm::ptrdiff_t' mlir/lib/Passes/Mapping/HeuristicMapping.cpp | head -20

echo "---"
echo "Checking LLVM types used in this file:"
rg -t cpp 'llvm::' mlir/lib/Passes/Mapping/HeuristicMapping.cpp | head -20

Repository: munich-quantum-toolkit/core

Length of output: 359


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find how 'step' is used in the function after line 478
rg -A 50 'constexpr.*step.*Direction::Forward' mlir/lib/Passes/Mapping/HeuristicMapping.cpp | grep -E 'step|advance|std::ranges'

Repository: munich-quantum-toolkit/core

Length of output: 478


🏁 Script executed:

# Get more context - the full loop that uses step
sed -n '478,530p' mlir/lib/Passes/Mapping/HeuristicMapping.cpp

Repository: munich-quantum-toolkit/core

Length of output: 2098


🏁 Script executed:

# Check if std::ranges::advance is used and how it handles the step
rg -B 5 -A 15 'std::ranges::advance' mlir/lib/Passes/Mapping/HeuristicMapping.cpp

Repository: munich-quantum-toolkit/core

Length of output: 2842


🏁 Script executed:

# Check the MLIR includes and how this file uses LLVM types
head -50 mlir/lib/Passes/Mapping/HeuristicMapping.cpp

Repository: munich-quantum-toolkit/core

Length of output: 1414


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check LLVM/MLIR convention for signed difference type
rg -t cpp 'std::ptrdiff_t|llvm::ssize_t|ptrdiff_t' mlir/ --max-count=5

echo "---"
# Check how other MLIR code handles ranges::advance with negative steps
rg -B 5 -A 2 'std::ranges::advance' mlir/ | head -40

Repository: munich-quantum-toolkit/core

Length of output: 3214


🏁 Script executed:

# Check if there's a typed Direction enum
rg -B 5 -A 10 'enum.*Direction|class Direction' mlir/

Repository: munich-quantum-toolkit/core

Length of output: 1309


Change std::size_t to std::ptrdiff_t to handle backward traversal correctly.

The step variable must support negative values when traversing backward (Direction::Backward passes -1). Storing -1 in std::size_t causes unsigned underflow and wraps to SIZE_MAX, which is then passed to std::ranges::advance(). Since WireIterator::difference_type is std::ptrdiff_t, the step must use the same signed type.

Proposed fix
-    constexpr std::size_t step = d == Direction::Forward ? 1 : -1;
+    constexpr std::ptrdiff_t step = d == Direction::Forward ? 1 : -1;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
constexpr std::size_t step = d == Direction::Forward ? 1 : -1;
constexpr std::ptrdiff_t step = d == Direction::Forward ? 1 : -1;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mlir/lib/Passes/Mapping/HeuristicMapping.cpp` at line 478, The variable
"step" is declared with the unsigned type std::size_t but needs to hold negative
values for Direction::Backward; change its type to a signed pointer-difference
type (e.g., std::ptrdiff_t) so it matches WireIterator::difference_type and
avoids underflow when set to -1; update the declaration near the constexpr
std::size_t step = d == Direction::Forward ? 1 : -1; (used with
std::ranges::advance and wire iteration) to use std::ptrdiff_t (or
WireIterator::difference_type) instead.

Comment on lines +609 to +620
const auto unknown = rewriter.getUnknownLoc();
for (const auto [hw0, hw1] : *swaps) {
const auto in0 = wires[hw0].qubit();
const auto in1 = wires[hw1].qubit();

auto op = rewriter.create<SWAPOp>(unknown, in0, in1);
const auto out0 = op.getQubit0Out();
const auto out1 = op.getQubit1Out();

rewriter.replaceAllUsesExcept(in0, out1, op);
rewriter.replaceAllUsesExcept(in1, out0, op);

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

SWAP ops are created with a stale insertion point.

rewriter.create<SWAPOp> on Line 614 has no local setInsertionPoint, so insertion can occur far from the wire front (leftover insertion point from previous rewrites), which risks invalid ordering/dominance after replaceAllUsesExcept.

🔧 Proposed fix
       const auto unknown = rewriter.getUnknownLoc();
       for (const auto [hw0, hw1] : *swaps) {
         const auto in0 = wires[hw0].qubit();
         const auto in1 = wires[hw1].qubit();
+        assert(in0.getNumUses() == 1 && in1.getNumUses() == 1 &&
+               "expected linear qubit wires");
+        auto* use0 = *in0.getUsers().begin();
+        auto* use1 = *in1.getUsers().begin();
+        auto* insertBefore = use0->isBeforeInBlock(use1) ? use0 : use1;
+        rewriter.setInsertionPoint(insertBefore);

         auto op = rewriter.create<SWAPOp>(unknown, in0, in1);
         const auto out0 = op.getQubit0Out();
         const auto out1 = op.getQubit1Out();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const auto unknown = rewriter.getUnknownLoc();
for (const auto [hw0, hw1] : *swaps) {
const auto in0 = wires[hw0].qubit();
const auto in1 = wires[hw1].qubit();
auto op = rewriter.create<SWAPOp>(unknown, in0, in1);
const auto out0 = op.getQubit0Out();
const auto out1 = op.getQubit1Out();
rewriter.replaceAllUsesExcept(in0, out1, op);
rewriter.replaceAllUsesExcept(in1, out0, op);
const auto unknown = rewriter.getUnknownLoc();
for (const auto [hw0, hw1] : *swaps) {
const auto in0 = wires[hw0].qubit();
const auto in1 = wires[hw1].qubit();
assert(in0.getNumUses() == 1 && in1.getNumUses() == 1 &&
"expected linear qubit wires");
auto* use0 = *in0.getUsers().begin();
auto* use1 = *in1.getUsers().begin();
auto* insertBefore = use0->isBeforeInBlock(use1) ? use0 : use1;
rewriter.setInsertionPoint(insertBefore);
auto op = rewriter.create<SWAPOp>(unknown, in0, in1);
const auto out0 = op.getQubit0Out();
const auto out1 = op.getQubit1Out();
rewriter.replaceAllUsesExcept(in0, out1, op);
rewriter.replaceAllUsesExcept(in1, out0, op);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mlir/lib/Passes/Mapping/HeuristicMapping.cpp` around lines 609 - 620, The
SWAPOp is being created with a stale insertion point because
rewriter.create<SWAPOp>(unknown, ...) is called without first moving the
insertion point; before calling rewriter.create in the loop that iterates over
swaps (the block using wires, swaps, in0, in1 and then calling
rewriter.replaceAllUsesExcept), set the rewriter insertion point to the current
wire/front location (e.g., use the appropriate rewriter.setInsertionPoint or
setInsertionPointAfter API on the operation representing the wire front) so the
SWAPOp is inserted at the correct place relative to in0/in1, then create the
SWAPOp and perform replaceAllUsesExcept as shown.

Comment on lines +66 to +73
if (op.getNumQubits() > 1) {
const auto i0 = mappings[op.getQubit(0)];
const auto i1 = mappings[op.getQubit(1)];
if (!arch.areAdjacent(i0, i1)) {
executable = false;
return WalkResult::interrupt();
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Read the test file around the flagged lines to understand context
cat -n mlir/unittests/Passes/Mapping/test_heuristic_mapping.cpp | sed -n '40,85p'

Repository: munich-quantum-toolkit/core

Length of output: 1978


🌐 Web query:

llvm::DenseMap operator[] behavior when key is missing insert default value

💡 Result:

llvm::DenseMap::operator[] inserts when the key is missing.

  • If Key is not present, operator[] calls lookupOrInsertIntoBucket(Key) and returns a reference to the newly created mapped value. The insertion constructs the value with ValueT(...); for operator[] there are no constructor args, so this becomes ValueT() (value-/default-construction). This means ValueT must be default-constructible. [1][2]
  • If Key is present, it returns a reference to the existing value (no insertion). [2]

If you want “get with default” without inserting, use lookup() (returns ValueT() if missing) or lookup_or(key, default) (returns your provided default). [3]

Sources:
[1] LLVM DenseMap.h source (definition of operator[] and lookupOrInsertIntoBucket) (llvm.org)
[2] LLVM DenseMapBase doxygen (shows operator[] definitions and where they live) (llvm.org)
[3] LLVM DenseMapBase doxygen / DenseMap.h (lookup / lookup_or behavior) (llvm.org)

Citations:


Replace operator[] with defensive lookups and explicitly check for 2-qubit gates.

Lines 67–68 use mappings[op.getQubit(...)], which inserts a default value (0) for missing keys instead of failing. This silently masks incomplete mappings. Additionally, the condition op.getNumQubits() > 1 checks only the first two qubits of multi-qubit operations; ops with 3+ qubits should be explicitly rejected.

Proposed fix
     std::ignore = module->walk([&](qc::UnitaryOpInterface op) {
-      if (op.getNumQubits() > 1) {
-        const auto i0 = mappings[op.getQubit(0)];
-        const auto i1 = mappings[op.getQubit(1)];
-        if (!arch.areAdjacent(i0, i1)) {
+      if (op.getNumQubits() == 2) {
+        const auto it0 = mappings.find(op.getQubit(0));
+        const auto it1 = mappings.find(op.getQubit(1));
+        if (it0 == mappings.end() || it1 == mappings.end() ||
+            !arch.areAdjacent(it0->second, it1->second)) {
+          executable = false;
+          return WalkResult::interrupt();
+        }
+      } else if (op.getNumQubits() > 2) {
           executable = false;
           return WalkResult::interrupt();
-        }
       }
       return WalkResult::advance();
     });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mlir/unittests/Passes/Mapping/test_heuristic_mapping.cpp` around lines 66 -
73, The current code uses mappings[op.getQubit(...)] which can insert default
entries and only checks op.getNumQubits() > 1; change this to defensively lookup
qubit indices (e.g., use mappings.find or contains) for op.getQubit(0) and
op.getQubit(1), verify both exist before using them, and explicitly require
op.getNumQubits() == 2 (reject ops with 0, 1, or >2 qubits) — if a lookup fails
or the arity is not 2, set executable = false and return WalkResult::interrupt()
(retain checks using arch.areAdjacent(i0, i1) only after successful lookups).

runHeuristicMapping(module);
EXPECT_TRUE(isExecutable(module, arch));

module->dump();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Remove unconditional IR dumps from the test path.

Line 135 and Line 185 print the full module on every successful run, which adds avoidable CI noise.

🧹 Proposed cleanup
-  module->dump();

Also applies to: 185-185

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mlir/unittests/Passes/Mapping/test_heuristic_mapping.cpp` at line 135, Remove
the unconditional IR dumps from the test by deleting or gating the
module->dump() calls so they do not print on every successful run; locate the
invocations of module->dump() in test_heuristic_mapping.cpp (the calls at and
near the two occurrences noted) and either remove them or wrap them behind a
debug/verbose guard (e.g., LLVM_DEBUG/if (verbose)) so CI output is not noisy
while preserving the ability to enable dumps for debugging.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

c++ Anything related to C++ code MLIR Anything related to MLIR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant