Skip to content

✨ Translate for and while from OpenQASM to QC#1862

Open
denialhaag wants to merge 23 commits into
mainfrom
improve-openqasm-translation
Open

✨ Translate for and while from OpenQASM to QC#1862
denialhaag wants to merge 23 commits into
mainfrom
improve-openqasm-translation

Conversation

@denialhaag

@denialhaag denialhaag commented Jul 6, 2026

Copy link
Copy Markdown
Member

Description

This PR adds support for translating for and while from OpenQASM to QC. In doing so, this PR also completely refactors the translation. Instead of relying on qasm3::InstVisitor, the new version of the translation dispatches on qasm3::Token. While more manual, the new implementation is lighter and less tied to legacy decisions.

Fixes #1846

AI notice: I have used Opus 4.8 via Claude Code for a first version of the changes. I have then iterated on this version (largely manually, sometimes with further help of Opus 4.8).

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.

If PR contains AI-assisted content:

  • I have disclosed the use of AI tools in the PR description as per our AI Usage Guidelines.
  • AI-assisted commits include an Assisted-by: [Model Name] via [Tool Name] footer.
  • I confirm that I have personally reviewed and understood all AI-generated content, and accept full responsibility for it.

@denialhaag denialhaag added this to the MLIR Support milestone Jul 6, 2026
@denialhaag denialhaag self-assigned this Jul 6, 2026
@denialhaag denialhaag added enhancement Improvement of existing feature refactor Anything related to code refactoring MLIR Anything related to MLIR labels Jul 6, 2026
@codecov

codecov Bot commented Jul 6, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 65.09988% with 594 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...de/mlir/Dialect/QC/Translation/qasm3/QASM3Parser.h 64.6% 268 Missing ⚠️
.../lib/Dialect/QC/Translation/qasm3/QASM3Emitter.cpp 67.3% 223 Missing ⚠️
...ir/lib/Dialect/QC/Translation/qasm3/QASM3Lexer.cpp 59.4% 103 Missing ⚠️

📢 Thoughts on this report? Let us know!

@denialhaag denialhaag force-pushed the improve-openqasm-translation branch from 2f7d2d3 to f29d3b7 Compare July 6, 2026 16:37
@denialhaag denialhaag force-pushed the improve-openqasm-translation branch from f29d3b7 to 7e379f5 Compare July 6, 2026 17:34
@denialhaag

Copy link
Copy Markdown
Member Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Full review finished.

@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a standalone QASM3 parser for QC translation, moves DebugInfo into its own header, rewires translateQASM3ToQC to delegate parsing, and expands tests with new QASM control-flow fixtures and translation cases.

Changes

DebugInfo Header Extraction

Layer / File(s) Summary
Extract DebugInfo into its own header
include/mqt-core/qasm3/DebugInfo.hpp, include/mqt-core/qasm3/Statement.hpp, include/mqt-core/qasm3/Exception.hpp
DebugInfo moves into a new header, and the affected headers now include it instead of defining or indirectly relying on the inline type.

QASM3 Parser Rewrite with for/while Translation

Layer / File(s) Summary
Parser contracts, gate tables, and core lowering
mlir/lib/Dialect/QC/Translation/QASM3Parser.h, mlir/lib/Dialect/QC/Translation/QASM3Parser.cpp, mlir/lib/Dialect/QC/Translation/CMakeLists.txt
Adds parseQASM3, parser support code, gate dispatch/metadata, statement and control-flow lowering, and wires the new source into the library build.
Delegate TranslateQASM3ToQC to the new parser
mlir/lib/Dialect/QC/Translation/TranslateQASM3ToQC.cpp, mlir/include/mlir/Dialect/QC/Translation/TranslateQASM3ToQC.h
translateQASM3ToQC now reads the source buffer, calls detail::parseQASM3, updates exception handling, and revises the API docs.
QASM control-flow fixtures and cases
mlir/unittests/programs/qasm_programs.h, mlir/unittests/programs/qasm_programs.cpp, mlir/unittests/Dialect/QC/Translation/test_qasm3_translation.cpp
Adds new QASM strings, reference builders, and test instantiations covering RX theta plus nested if/for/while and loop-indexing scenarios.

Estimated code review effort: 4 (Complex) | ~75 minutes

Suggested labels: c++

Suggested reviewers: burgholzer

Poem

A rabbit hops through loops anew,
for and while now translate true,
Debug info tucked in its own little nest,
And tests hop onward to verify the rest. 🐇

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The PR implements #1846 by translating for and while into SCF ops and adds tests covering the new control-flow support.
Out of Scope Changes check ✅ Passed The refactor, new parser split, helper headers, docs, and tests all support the translation change; no unrelated scope stands out.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Title check ✅ Passed The title clearly states the main change: translating OpenQASM for and while constructs to QC.
Description check ✅ Passed The description includes a summary, linked issue, motivation, AI disclosure, and a mostly complete checklist.
✨ Finishing Touches
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch improve-openqasm-translation

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@include/mqt-core/qasm3/DebugInfo.hpp`:
- Around line 20-36: Add Doxygen-style comments for the new DebugInfo struct,
its constructor, and toString() in DebugInfo.hpp so the purpose of DebugInfo,
each constructor parameter (including parentDebugInfo), and the return value of
toString() are documented; place the comments directly above the DebugInfo
declaration, the DebugInfo constructor, and the toString() method.
- Around line 26-29: The DebugInfo constructor is using a redundant double
std::move on the filename parameter, which should be cleaned up to satisfy
clang-tidy. Update the DebugInfo initializer list so the filename member is
initialized with a single move from the constructor argument, and leave the
parentDebugInfo handling unchanged.

In `@mlir/lib/Dialect/QC/Translation/QASM3Parser.cpp`:
- Around line 593-630: The qubit and classical register declaration parsers
duplicate the same size-parsing, declaration registration, and allocation logic
across parseQubitDeclaration()/parseQregDeclaration() and
parseBitDeclaration()/parseCregDeclaration(). Extract shared helpers such as
declareQubitRegister and declareClassicalRegister, then have these parser
methods delegate to them while preserving the existing keyword/operand ordering
differences so the behavior stays consistent and easier to maintain.
- Around line 853-903: parseFor() currently forwards any parsed step into scfFor
and always bumps the upper bound with stopVal + 1, but scf.for only accepts a
positive step. Add validation in parseFor() after computing stepVal (or before
lowering) to reject zero and negative steps with an error, and keep the existing
inclusive-stop adjustment only for valid ascending ranges. Use parseFor,
emitIntegerExpression, and builder.scfFor to locate the lowering path.
- Around line 400-407: Both QASM3Parser::error overloads always throw
CompilerError, so mark each error(...) helper as [[noreturn]] to make the
control flow explicit at bare call sites and improve static analysis. Update
both the Token-based and DebugInfo-based overloads in QASM3Parser.cpp, keeping
their behavior unchanged while adding the attribute to the function
declarations/definitions.

In `@mlir/unittests/Dialect/QC/Translation/test_qasm3_translation.cpp`:
- Around line 465-467: The test case name in QASM3TranslationTestCase is
misspelled as "NestedForLooWhilefOp"; update the string to
"NestedForLoopWhileOp" so the case is searchable and consistent with
qasm::nestedForLoopWhileOp and MQT_NAMED_BUILDER(nestedForLoopWhileOp).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: cd38d502-c3a9-475e-b5c0-57cb0847777d

📥 Commits

Reviewing files that changed from the base of the PR and between d453dd7 and 7e379f5.

📒 Files selected for processing (11)
  • include/mqt-core/qasm3/DebugInfo.hpp
  • include/mqt-core/qasm3/Exception.hpp
  • include/mqt-core/qasm3/Statement.hpp
  • mlir/include/mlir/Dialect/QC/Translation/TranslateQASM3ToQC.h
  • mlir/lib/Dialect/QC/Translation/CMakeLists.txt
  • mlir/lib/Dialect/QC/Translation/QASM3Parser.cpp
  • mlir/lib/Dialect/QC/Translation/QASM3Parser.h
  • mlir/lib/Dialect/QC/Translation/TranslateQASM3ToQC.cpp
  • mlir/unittests/Dialect/QC/Translation/test_qasm3_translation.cpp
  • mlir/unittests/programs/qasm_programs.cpp
  • mlir/unittests/programs/qasm_programs.h

Comment thread include/mqt-core/qasm3/DebugInfo.hpp
Comment thread include/mqt-core/qasm3/DebugInfo.hpp
Comment thread mlir/lib/Dialect/QC/Translation/QASM3Parser.cpp Outdated
Comment thread mlir/lib/Dialect/QC/Translation/QASM3Parser.cpp Outdated
Comment thread mlir/lib/Dialect/QC/Translation/QASM3Parser.cpp Outdated
Comment thread mlir/unittests/Dialect/QC/Translation/test_qasm3_translation.cpp Outdated
@denialhaag

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@denialhaag denialhaag requested a review from burgholzer July 8, 2026 15:50

@burgholzer burgholzer left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks @denialhaag for working on this! 🙏🏼

I pushed one small commit with some cosmetic fixed I stumbled over while reviewing.
As you will probably see through the reviews, I am not yet quite happy where this is at. Overall, the current solution almost feels more constrained than the previous solution and still does not quite naturally fit to MLIR. I believe we should be taking much more advantage of MLIR built-in functionality for this.

It's also in one of the comments, but I'd like to highlight this here again: I am not 100% certain the approach of directly emitting the program while parsing really creates a nice design.
It mixes a couple too many things for my taste.
Maybe you can take all of the thoughts from the comments in this review back to the drawing board and re-evaluate what a good approach for all of this looks like.

Comment on lines +289 to +295
t["mcx"] = mcxInfo;
t["mcx_gray"] = mcxInfo;
t["mcx_vchain"] = mcxInfo;
t["mcx_recursive"] = mcxInfo;

t["mcphase"] = GateInfo{
.nControls = 0, .nTargets = 0, .nParameters = 1, .type = ::qc::OpType::P};

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I am not the biggest fan of this kind of special handling. Can we get rid of this somehow and find a "proper" solution?

Comment thread mlir/lib/Dialect/QC/Translation/QASM3Parser.cpp Outdated
Comment thread mlir/lib/Dialect/QC/Translation/QASM3Parser.cpp Outdated
Comment thread mlir/lib/Dialect/QC/Translation/QASM3Parser.cpp Outdated
Comment thread mlir/lib/Dialect/QC/Translation/QASM3Parser.cpp Outdated
Comment on lines +996 to +1001
const auto index = evaluateNonNegativeConstant(*bit.index, debugInfo);
if (index >= registerBits.size() || !registerBits[index]) {
error(*debugInfo, "Bit " + std::to_string(index) + " of register '" +
registerName + "' has been not measured yet.");
}
return registerBits[index];

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Hm.. I am beginning to more and more think that this is something that would be perfectly suitable for an MLIR verifier on an OpenQASM3 dialect.. or at least some kind of semantic analysis.

I am also not 100% sure whether it is really so clever to mix semantic analysis with the actual program emission like it is currently being done here.
From past conversations with LLMs, they mostly suggested to parse to an AST, then perform semantic analysis, then emit code. The current implementation kind of combines all three.

Feels like it may be worth to revisit this design decision.

Comment on lines +1197 to +1202
// Resolve parameters
SmallVector<Value> params;
params.reserve(call.parameters.size());
for (const auto& arg : call.parameters) {
params.push_back(emitFloatExpression(arg, debugInfo));
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Parameters do not necessarily need to be floating point expressions, right? One could declare a custom gate that uses other kind or arguments for parameters, I suppose.

Comment on lines +1240 to +1245
// Inline compound gate
if (const auto* compound = std::get_if<ParsedCompoundGate>(&it->second)) {
emitCompoundGate(*compound, params, split.targets, split.posControls,
split.negControls, split.invert, debugInfo);
return;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I believe this is another opportunity to improve the parser. There is no need to inline custom gate definitions.
We can simply materialize the definitions as proper func::func functions in the MLIR code and call the respective methods.
Inlining is something that can then happen in MLIR.

@denialhaag denialhaag Jul 10, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I'd prefer to do this as part of #1659.

  1. This would definitely break equivalence checking because the QCProgramBuilder cannot build the respective reference programs.

  2. I'm also pretty sure we'll need to implement additional canonicalization patterns. Consider, for example, this program:

    OPENQASM 3.0;
    include "stdgates.inc";
    qubit[3] q;
    gate compound q0, q1 {
      x q0;
      x q0;
    }
    ctrl(2) @ compound q[0], q[1], q[2];

    If translated into a new function, it would likely be canonicalized to an empty function. Subsequently, the control modifier would need to check whether the called function is empty and then remove the call, unless this is already handled by built-in patterns.

  3. I'm not 100 percent sure how our modifiers would handle function calls right now. They might be removed if their body does not contain a UnitaryOpInterface.

  4. I'm all but certain that we could not convert a program with multiple functions to QCO (let alone QIR).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Yeah, that makes sense. Maybe one can add a couple of notes there or point the issue to this discussion

Comment thread mlir/lib/Dialect/QC/Translation/QASM3Parser.cpp Outdated
Comment on lines +1818 to +1823
/// Statically evaluate `ParsedExpr` to an `size_t`.
[[nodiscard]] size_t evaluateNonNegativeConstant(
const ParsedExpr& expr,
const std::shared_ptr<DebugInfo>& debugInfo) const {
return static_cast<size_t>(evaluateIntegerConstant(expr, debugInfo));
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This kind of feels like something that should already happen in MLIR. I would have assumed we can have the translation to rather plainly emit the respective constructs and MLIR constant propagation and folding do the job for us.

@denialhaag denialhaag Jul 9, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This function is only used in four cases:

  1. Translating q[<index>]: I guess this could always be translated to memref.load operations. There are some limitations in QCProgramBuilder that prevent repeated loads. Plus, we currently do not allow memref.load operations within modifiers. All of these points have a solution, but it could get nasty.
  2. Translating c[<index>]: Cannot be changed before #1726 is implemented.
  3. Translating ctrl(<number of controls>): I don't really think this can ever be a dynamic value.
  4. Translating pow<exponent>: This can be computed dynamically. I will change it.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This function is only used in four cases:

  1. Translating q[<index>]: I guess this could always be translated to memref.load operations. There are some limitations in QCProgramBuilder that prevent repeated loads. Plus, we currently do not allow memref.load operations within modifiers. All of these points have a solution, but it could get nasty.

I see the nastiness. But this is exactly one of the examples for the point I was trying to raise in the other comment: indexed accessors are a standard thing in OpenQASM; so people will use them and tools will produce them in all kinds of places. We need to be able to handle those cases and find ways how these constructs can be represented well in our dialect.
Given how gate statements are parsed "as a whole" including the modifiers, it should be moderately straight forward to first compute the required qubit values and then do all the modifier handling and wrapping.

  1. Translating c[<index>]: Cannot be changed before #1726 is implemented.

Another good reason for bumping the priority on that one 😌

  1. Translating ctrl(<number of controls>): I don't really think this can ever be a dynamic value.

Yeah, probably not, otherwise the operand list would have to be dynamic as well.

  1. Translating pow<exponent>: This can be computed dynamically. I will change it.

👍🏼

@denialhaag denialhaag force-pushed the improve-openqasm-translation branch from e11e3a7 to 3b1e39b Compare July 10, 2026 15:41
@denialhaag

Copy link
Copy Markdown
Member Author

I pushed one small commit with some cosmetic fixed I stumbled over while reviewing.

Sorry, I forgot to pull, and git pull --rebase was no longer feasible... I manually cherry-picked your improvements in 3b1e39b.

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

Labels

enhancement Improvement of existing feature MLIR Anything related to MLIR refactor Anything related to code refactoring

Projects

None yet

Development

Successfully merging this pull request may close these issues.

✨ Translate for and while from OpenQASM to QC

2 participants