Skip to content

✨ Translate for and while from OpenQASM to QC - #1862

Draft
denialhaag wants to merge 46 commits into
mainfrom
improve-openqasm-translation
Draft

✨ Translate for and while from OpenQASM to QC#1862
denialhaag wants to merge 46 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 71.56250% with 546 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...de/mlir/Dialect/QC/Translation/qasm3/QASM3Parser.h 73.2% 231 Missing ⚠️
.../lib/Dialect/QC/Translation/qasm3/QASM3Emitter.cpp 73.1% 210 Missing ⚠️
...ir/lib/Dialect/QC/Translation/qasm3/QASM3Lexer.cpp 59.7% 105 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

Summary by CodeRabbit

  • New Features
    • Added OpenQASM 3 → QC translation support, including gate definitions/calls, modifiers, dynamic qubit indexing, broadcasting, control flow (if/for/while), and measurement/condition handling.
    • Added new constant builders for floating-point and index values when constructing QC programs.
  • Bug Fixes
    • Improved translation error reporting with source-location diagnostics and clearer failure behavior.
  • Documentation
    • Refined API documentation for the QASM3 translation entry points.
  • Tests
    • Expanded the QASM translation test suite with new control-flow, expression, broadcasting, and condition programs (and updated reference mappings).

Walkthrough

Adds standalone OpenQASM 3 lexer, parser, and QC MLIR emitter components. Translation now delegates through the new pipeline, with support and tests for loops, conditions, expressions, broadcasting, measurements, and source diagnostics.

Changes

QASM3 translation pipeline

Layer / File(s) Summary
Translation contracts and parser implementation
include/mqt-core/qasm3/*, mlir/include/mlir/Dialect/QC/Translation/qasm3/*
Adds DebugInfo, lexer/parser contracts, sink callbacks, and parsing for declarations, gates, expressions, conditions, and control flow.
QASM3 lexical analysis
mlir/lib/Dialect/QC/Translation/qasm3/QASM3Lexer.cpp
Implements tokenization for keywords, literals, comments, hardware qubits, punctuation, operators, and diagnostics.
QC MLIR emission and translation wiring
mlir/include/mlir/Dialect/QC/Builder/QCProgramBuilder.h, mlir/lib/Dialect/QC/{Builder,Translation}/*, mlir/tools/mqt-cc/mqt-cc.cpp
Adds constant helpers and lowers parsed QASM3 into QC MLIR, including gates, expressions, conditions, measurements, broadcasting, and structured loops.
Translation fixtures and equivalence coverage
mlir/unittests/Dialect/QC/Translation/*, mlir/unittests/programs/qasm_programs.*
Adds reference builders and QASM fixtures covering measurements, control flow, broadcasting, expressions, and conditions.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant SourceMgr
  participant TranslateQASM3ToQC
  participant Lexer
  participant Parser
  participant QCEmitter
  participant QCProgramBuilder
  SourceMgr->>TranslateQASM3ToQC: provide QASM3 source
  TranslateQASM3ToQC->>Lexer: tokenize source
  Lexer-->>Parser: return tokens
  Parser->>QCEmitter: dispatch parsed statements
  QCEmitter->>QCProgramBuilder: emit QC MLIR operations
  QCProgramBuilder-->>QCEmitter: return constructed values
  QCEmitter-->>TranslateQASM3ToQC: return finalized module
Loading

Possibly related PRs

Suggested labels: feature, c++

Poem

I’m a rabbit with tokens tucked under my ear,
Parsing loops and gates with a hop and a cheer.
Expressions bloom, conditions take flight,
QC modules emerge clean and bright.
for, while, and qubits follow the beat—
A carrot-powered compiler, wonderfully neat!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 37.76% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: OpenQASM for/while translation to QC.
Description check ✅ Passed The description includes the summary, motivation, linked issue, and AI disclosure checklist details.
Linked Issues check ✅ Passed The changes implement the requested OpenQASM for/while support and add the needed SCF lowering coverage for #1846.
Out of Scope Changes check ✅ Passed The additional parser, lexer, tests, and build updates appear supportive of the translation refactor and loop support, not unrelated.
✨ 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 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 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 thread mlir/lib/Dialect/QC/Translation/QASM3Parser.cpp Outdated
Comment thread mlir/lib/Dialect/QC/Translation/QASM3Parser.cpp Outdated
@denialhaag
denialhaag force-pushed the improve-openqasm-translation branch from b9c73a0 to 4981a09 Compare July 13, 2026 08:41
@denialhaag
denialhaag force-pushed the improve-openqasm-translation branch from 4981a09 to 99fa5db Compare July 13, 2026 08:53
@denialhaag

Copy link
Copy Markdown
Member Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Full review finished.

@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: 17

🤖 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 `@mlir/.clang-tidy`:
- Line 11: Remove the broad cppcoreguidelines-slicing suppression from the
mlir/.clang-tidy configuration and add a targeted NOLINT annotation at the
specific code triggering the known false positive. Keep the slicing check
enabled for the rest of the mlir/ subtree and follow the project’s existing
clang-tidy annotation style.

In `@mlir/include/mlir/Dialect/QC/Translation/qasm3/QASM3Parser.h`:
- Around line 844-854: Update the parameter parsing block to call
parseIdentifierList only when the token after the opening parenthesis is not
RParen, allowing empty gate parameter lists while preserving the existing
expect(TokenKind::RParen) validation.
- Around line 272-278: Update the parser’s symbol management around declare(),
parseBlock(), and parseFor() to use a lexical scope stack instead of one global
symbols map. Push a new scope when entering each block and pop it on exit so
declarations do not escape and inner declarations can shadow outer names; ensure
declare() checks/inserts only in the current scope. Register each parseFor()
iteration variable in the body’s scope before parsing the loop body so
assignments resolve correctly.

In `@mlir/include/mlir/Dialect/QC/Translation/TranslateQASM3ToQC.h`:
- Around line 28-40: Update the Doxygen `@return` documentation for both
translateQASM3ToQC overloads to state that translation failure may return
nullptr, while successful translation returns the QC module.

In `@mlir/lib/Dialect/QC/Translation/qasm3/QASM3Emitter.cpp`:
- Around line 395-425: Validate folded register sizes in qubitRegister and
classicalRegister before calling builder.allocQubitRegister or
builder.allocClassicalBitRegister, rejecting non-positive values with a source
diagnostic that uses loc. Ensure invalid sizes return failure and valid sizes
continue through the existing allocation paths.
- Around line 1367-1392: Update the Expr::Kind::Pow branch to use checked
exponentiation by squaring instead of a loop proportional to **rhs. Bound the
exponent so excessively large user-controlled values are rejected, and detect
overflow in every intermediate multiplication, returning error(expr.loc, ...)
rather than invoking signed-overflow undefined behavior.
- Around line 557-572: Update ifBegin in QCProgramBuilder so entering the
scf::IfOp then region also synchronizes the builder’s private regionStack. Use
an existing builder region-scope API if available, or add a safe API that pushes
the region while preserving the IfScope lifetime and restores it when the scope
exits; do not only call setInsertionPointToStart. Ensure dynamic register
indexing inside the branch observes the correct region depth in memrefLoad.
- Around line 336-392: Prevent assignBool, assignInt, and assignFloat from
inserting branch-local SSA values directly into the global booleanValues,
intValues, or floatValues tables when called inside if, while, or for control
flow. Lower such assignments through structured-control-flow results or loop
iter_args so values are merged and carried correctly, or reject assignments to
variables defined outside the active control-flow region; apply the same
handling to the control-flow emission logic covering the referenced later range.
- Around line 257-284: Update finalize() and the bare-register lookup path to
validate every accessed or returned slot in info.bits, not merely the vector
length. Reject any register containing a missing/null Value, including gaps such
as an unmeasured lower-index bit, before appending or returning values. Preserve
the existing output-register error handling and valid measured-bit behavior.
- Around line 341-382: Update the dynamic-load cache key construction and lookup
in the relevant QASM3 emitter logic to use the resolved SSA index value or
version, rather than the source expression spelling. Ensure reassignment through
assignInt and related integer-binding updates cannot reuse a cached load created
for an earlier value, while preserving reuse when the resolved index remains
unchanged.
- Around line 1466-1471: Update the parse/finalization flow around
parser.parseProgram() and emitter.finalize() to return nullptr immediately when
status indicates failure, before calling finalize(). Only finalize the emitter
after successful parsing, while preserving the existing mod failure check and
null return.
- Around line 598-652: Update the loop-lowering logic around
tryEvaluateIntConstant and builder.scfFor so dynamically resolved steps are
guaranteed strictly positive before constructing scf.for. Preserve the existing
static-negative countdown lowering, and for unknown or runtime non-positive
steps either emit a runtime validation/error guard or normalize them through an
equivalent dynamic countdown path rather than passing them directly as the
scf.for step.
- Around line 897-908: Update resolveStandardGate to validate gate.nTargets
against the supplied target operands before returning the dispatch entry,
rejecting both missing and extra targets with the existing gate error pattern.
Keep the parameter-count validation and successful GATE_DISPATCH lookup behavior
unchanged.

In `@mlir/lib/Dialect/QC/Translation/qasm3/QASM3Lexer.cpp`:
- Around line 114-141: The skipTrivia() block-comment path must detect when
scanning reaches end without finding */ instead of silently allowing next() to
return Eof. Preserve an unterminated-comment error state or make next() return
an Error token, while retaining normal trivia skipping for properly terminated
block comments.
- Around line 157-194: Update next() so a period followed by a digit is routed
to lexNumber(), alongside digit-prefixed tokens, allowing leading-dot literals
such as .5 to be recognized as FloatLiteral while preserving existing handling
for standalone periods.
- Around line 196-213: Update Lexer::lexString to accept the opening quote
delimiter and terminate on that same delimiter, preserving unterminated-string
error handling. In the include-string lexing dispatch, recognize and pass both
single-quote and double-quote delimiters so inputs such as single-quoted
includes produce StringLiteral tokens.

In `@mlir/unittests/Dialect/QC/Translation/test_qasm3_translation.cpp`:
- Around line 721-727: Add the missing forLoopOffsetIndex fixture to the // ---
ForOp --- // test suite in QASM3TranslationTestCase, pairing
qasm::forLoopOffsetIndex with its matching MQT_NAMED_BUILDER reference builder.
🪄 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: 6afd7c09-7460-41d8-9733-eb8874f34d2e

📥 Commits

Reviewing files that changed from the base of the PR and between def69f1 and a7a0c60.

📒 Files selected for processing (18)
  • include/mqt-core/qasm3/DebugInfo.hpp
  • include/mqt-core/qasm3/Exception.hpp
  • include/mqt-core/qasm3/Statement.hpp
  • mlir/.clang-tidy
  • mlir/include/mlir/Dialect/QC/Builder/QCProgramBuilder.h
  • mlir/include/mlir/Dialect/QC/Translation/TranslateQASM3ToQC.h
  • mlir/include/mlir/Dialect/QC/Translation/qasm3/QASM3Emitter.h
  • mlir/include/mlir/Dialect/QC/Translation/qasm3/QASM3Lexer.h
  • mlir/include/mlir/Dialect/QC/Translation/qasm3/QASM3Parser.h
  • mlir/lib/Dialect/QC/Builder/QCProgramBuilder.cpp
  • mlir/lib/Dialect/QC/Translation/CMakeLists.txt
  • mlir/lib/Dialect/QC/Translation/TranslateQASM3ToQC.cpp
  • mlir/lib/Dialect/QC/Translation/qasm3/QASM3Emitter.cpp
  • mlir/lib/Dialect/QC/Translation/qasm3/QASM3Lexer.cpp
  • mlir/tools/mqt-cc/mqt-cc.cpp
  • mlir/unittests/Dialect/QC/Translation/test_qasm3_translation.cpp
  • mlir/unittests/programs/qasm_programs.cpp
  • mlir/unittests/programs/qasm_programs.h

Comment thread mlir/.clang-tidy
Comment thread mlir/include/mlir/Dialect/QC/Translation/qasm3/QASM3Parser.h Outdated
Comment thread mlir/include/mlir/Dialect/QC/Translation/qasm3/QASM3Parser.h
Comment thread mlir/include/mlir/Dialect/QC/Translation/TranslateQASM3ToQC.h Outdated
Comment thread mlir/lib/Dialect/QC/Translation/qasm3/QASM3Emitter.cpp
Comment thread mlir/lib/Dialect/QC/Translation/qasm3/QASM3Emitter.cpp
Comment thread mlir/lib/Dialect/QC/Translation/qasm3/QASM3Lexer.cpp Outdated
Comment thread mlir/lib/Dialect/QC/Translation/qasm3/QASM3Lexer.cpp
Comment thread mlir/lib/Dialect/QC/Translation/qasm3/QASM3Lexer.cpp Outdated
Comment thread mlir/unittests/Dialect/QC/Translation/test_qasm3_translation.cpp
@denialhaag

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 14, 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.

@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: 2

🤖 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 `@mlir/include/mlir/Dialect/QC/Translation/qasm3/QASM3Parser.h`:
- Around line 761-764: The assignment validation around
scopes.back().contains(id) incorrectly rejects writes to variables declared in
enclosing scopes. Remove this rejection and propagate enclosing-scope values
through the sink/SCF region arguments so assignments in while, if, and for
bodies update the original variable correctly.

In `@mlir/lib/Dialect/QC/Translation/qasm3/QASM3Emitter.cpp`:
- Around line 573-581: Extend DeclarationScope and the QCEmitter control-flow
handling so qubitRegister() and classicalRegister() state is saved and restored
alongside bindings and loadedQubits. Ensure declarations made inside scf.if,
scf.for, or scf.while remain scoped to that region and cannot affect later
lookups or finalize(); alternatively reject block-local register declarations
consistently.
🪄 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: 2e5f9002-046b-4e93-9b61-05aad7f5fe61

📥 Commits

Reviewing files that changed from the base of the PR and between a7a0c60 and 4d5581e.

📒 Files selected for processing (10)
  • include/mqt-core/qasm3/Exception.hpp
  • include/mqt-core/qasm3/Statement.hpp
  • mlir/include/mlir/Dialect/QC/Translation/TranslateQASM3ToQC.h
  • mlir/include/mlir/Dialect/QC/Translation/qasm3/QASM3Emitter.h
  • mlir/include/mlir/Dialect/QC/Translation/qasm3/QASM3Lexer.h
  • mlir/include/mlir/Dialect/QC/Translation/qasm3/QASM3Parser.h
  • mlir/lib/Dialect/QC/Translation/qasm3/QASM3Emitter.cpp
  • mlir/lib/Dialect/QC/Translation/qasm3/QASM3Lexer.cpp
  • mlir/unittests/Dialect/QC/Translation/test_qasm3_translation.cpp
  • mlir/unittests/programs/qasm_programs.h
💤 Files with no reviewable changes (1)
  • mlir/unittests/programs/qasm_programs.h

Comment on lines +761 to +764
if (!scopes.back().contains(id)) {
return sink.error(loc, "cannot assign to '" + id +
"' from inside a block; it is declared in an "
"enclosing scope");

@coderabbitai coderabbitai Bot Jul 14, 2026

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.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Allow assignments to enclosing-scope variables inside nested control-flow bodies. This rejects valid updates like while (...) { i = i + 1; } and assignments in if/for blocks. Thread these values through the sink/SCF region arguments instead of treating every outer-scope write as illegal.

🤖 Prompt for 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.

In `@mlir/include/mlir/Dialect/QC/Translation/qasm3/QASM3Parser.h` around lines
761 - 764, The assignment validation around scopes.back().contains(id)
incorrectly rejects writes to variables declared in enclosing scopes. Remove
this rejection and propagate enclosing-scope values through the sink/SCF region
arguments so assignments in while, if, and for bodies update the original
variable correctly.

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.

@burgholzer, I would move the point @coderabbitai is raising here to a follow-up. I'm not sure how our conversions and the builders would deal with this at the moment. 🤔

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.

@denialhaag, that sounds reasonable — deferring this to a follow-up avoids scope creep given the uncertainty around how the SCF region/builder plumbing would need to change to support enclosing-scope mutations. Let me know if you'd like me to open a follow-up issue to track this (referencing this PR and comment), and I'll take care of it.

@burgholzer burgholzer Jul 14, 2026

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.

@coderabbitai hm. I'll look into this as part of my review. at a quick glance, this feels a bit like an arbitrary limitation to be honest.

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.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

@denialhaag denialhaag Jul 14, 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.

It is a limitation, but I don't think it's a limitation of the translation. The QCProgramBuilder currently cannot create a while loop that modifies a classical variable. For this to be necessary, we would need to add block arguments and respective yields. In addition, all conversions that involve scf operations would not respect a classical block argument at the moment. This is all fixable, but I probably wouldn't do it as part of this PR.

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.

Would we? SCF while regions are not isolated from above, aren't they?
I am probably just missing something here. I'll look at this with fresh eyes in the coming days.

@denialhaag denialhaag Jul 14, 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.

No, the regions are not isolated from above. That said, I think you need to thread any values you are updating through the region arguments anyway. For example, see this example from the MLIR docs:

func.func @reduce(%buffer: memref<1024xf32>, %lb: index,
                  %ub: index, %step: index) -> (f32) {
  // Initial sum set to 0.
  %sum_0 = arith.constant 0.0 : f32
  // iter_args binds initial values to the loop's region arguments.
  %sum = scf.for %iv = %lb to %ub step %step
      iter_args(%sum_iter = %sum_0) -> (f32) {
    %t = load %buffer[%iv] : memref<1024xf32>
    %sum_next = arith.addf %sum_iter, %t : f32
    // Yield current iteration sum to next iteration %sum_iter or to %sum
    // if final iteration.
    scf.yield %sum_next : f32
  }
  return %sum : f32
}

@denialhaag denialhaag Jul 14, 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.

Okay, I'm definitely getting too tired. Reassignment obviously isn't a thing in MLIR. A reassignment in OpenQASM corresponds to the definition of a new variable in MLIR. If this new variable is defined within a region, it would definitely need to be yielded to be accessible outside.

Comment thread mlir/lib/Dialect/QC/Translation/qasm3/QASM3Emitter.cpp
@denialhaag
denialhaag requested a review from burgholzer July 14, 2026 19:43
@burgholzer

burgholzer commented Jul 15, 2026

Copy link
Copy Markdown
Member

Hey @denialhaag 👋🏼 I let GPT-5.6-Codex review the PR and compare it against the OpenQASM 3 live specification. Below is the improvement plan it came up with. To me, this makes a lot of sense. If this direction is okay with you, I’d task Codex with implementing the stabilizations for this PR and then continue the broader work in follow-up PRs.

Improve PR #1862 into a specification-driven OpenQASM 3 frontend

Summary

PR #1862 has a strong foundation: zero-copy lexing, SourceMgr/SMLoc diagnostics, LLVM containers, LogicalResult, and direct use of arith, math, memref, scf, func, and QC operations. Broadcasting and structured-control lowering are also conceptually sound.

However, it is not yet suitable for the stated “arbitrary OpenQASM 3” goal. The custom parser implements only a narrow subset of the official grammar, while syntax, semantic analysis, and MLIR emission remain interleaved despite the sink abstraction. This causes rejected valid programs, accepted invalid syntax, target-specific parser restrictions, and some paths that can produce invalid MLIR.

Use a staged delivery:

  1. Make ✨ Translate for and while from OpenQASM to QC #1862 safe and honest to merge by fixing correctness blockers and documenting its supported subset.
  2. Replace the streaming frontend with a specification-driven, versioned OpenQASM-facing MLIR stage.
  3. Lower incrementally to builtin MLIR and QC dialects, using custom operations only where no suitable builtin representation exists.

Target stable OpenQASM 3.0 and 3.1 with version-gated behavior. Track later live-spec additions explicitly.

Review Conclusions

Merge-blocking correctness issues

  • Classical state is not represented correctly across regions. Scalar assignment to an enclosing scope is deliberately rejected, but measurement assignment to an enclosing classical register is allowed. The emitter then records an SSA value defined inside an scf.if/scf.for/scf.while region and may use or return it outside that region, violating dominance. Until classical storage is implemented, reject these assignments and verify every produced module before returning it.
  • while cannot implement ordinary mutable loop state. OpenQASM block scopes inherit mutable outer variables, while the current parser rejects their assignment. Proper lowering requires scf.while arguments/results or mutable storage, as required by OpenQASM scoping semantics.
  • Standard-gate handling is inconsistent. The legacy gate metadata table and the emitter dispatch table contain different names: several recognized gates such as ccx, c3x, c4x, cu1, and iswapdg have no emitter, while several dispatch aliases are unreachable. Moreover, standard gates are available even without include "stdgates.inc", contrary to the versioned standard-library contract.
  • Gate definitions can recurse during eager expansion and exhaust the stack. Diagnose self-recursion/cycles and enforce declaration-before-use.
  • Dynamic zero loop steps reach signed division by zero; statically positive loops can overflow when converting an inclusive maximum bound with stop + 1. Add checked lowering or diagnostics.
  • The lexer/parser accepts and rejects the wrong language in several places: it lacks integer bases and separators, **, %, shifts, bitwise operators, type designators such as int[32], trailing commas, and most official statements; it incorrectly accepts pow(a,b) as a function despite the specification requiring **. It also permits version declarations in arbitrary positions and ignores the parsed version.
  • Input/output handling is not OpenQASM-compatible. Only output bit[...] is accepted, every classical register is returned when no output exists, and outputs are required to have been measured. Implement input/output as function arguments/results and preserve OpenQASM 2 compatibility only behind an explicit mode.
  • measure q;, anonymous scopes, set/expression-based for iteration, and valid gate-body loops are missing despite appearing in the official grammar.
  • Remove the PR-wide .clang-tidy suppression and address any slicing locally.

Important coverage gaps

The current frontend is missing substantial language families documented by the live language specification:

  • Width-aware bit, int, uint, float, angle, complex, duration, and stretch types, casts, promotions, and complete expression precedence.
  • Arrays, aliases, concatenation, slicing, index sets, bit strings, timing literals, and dynamic classical indexing.
  • Comparisons, compound assignment, shifts, bitwise operations, complete builtins, and general i1 conditions.
  • def, extern, calls, returns, break, continue, switch, end, nop, pragmas, and annotations.
  • pow modifiers, timing instructions, boxes, calibration grammar, cal, and defcal.
  • Real include resolution, nested-source diagnostics, include cycles, and version-dependent stdgates.inc.

Architecture assessment

Keep these strengths:

  • LLVM source locations and diagnostics.
  • Bump allocation and zero-copy source views where useful.
  • Direct lowering to standard MLIR dialects after semantic analysis.
  • Existing equivalence tests and MLIR verification.
  • QC operations for quantum semantics and modifiers already modeled by QC.

Replace these weaknesses:

  • The template parser/sink callbacks do not create a true phase boundary; parser symbol checks and emitter semantic checks are mixed.
  • The 1,500-line template header increases compile cost for negligible runtime benefit.
  • Eager custom-gate inlining can cause exponential IR growth and prevents reuse of func.func/func.call.
  • General integer computation uses index, losing width and signedness. Reserve index for indexing and loop bounds.
  • Classical registers are external metadata rather than MLIR state.
  • Dynamic-qubit caching serializes expression trees into heap-allocated strings and uses opaque SSA pointer values.
  • Gate metadata is duplicated across legacy maps, std::function dispatch, and QC builders.

Implementation Changes

1. Stabilize #1862 before merge

  • Add an explicit supported-feature document and stop describing the implementation as accepting arbitrary OpenQASM.
  • Reject any enclosing-scope classical update that cannot be represented with valid dominance, including measurement assignments in control-flow regions.
  • Run mlir::verify before returning a translated module; translation failure must produce a source-anchored diagnostic, never malformed IR.
  • Make version handling stateful: allow at most one leading version statement, accept only supported 3.0/3.1 versions, and select the corresponding standard library.
  • Populate standard gates only after stdgates.inc; generate metadata and emission from one registry and test every registered gate. Keep qelib1.inc behind compatibility mode.
  • Diagnose recursive gates, invalid control counts, unsupported power modifiers, zero dynamic loop steps, and inclusive-bound overflow.
  • Correct the easiest grammar issues immediately: numeric literals, **, %, valid strings, type designators, trailing commas, anonymous scopes, and targetless measurement.
  • Remove the global lint suppression and add changelog coverage.

2. Introduce a specification-driven source stage

  • Pin the official 3.0 and 3.1 ANTLR grammars as the syntax source of truth. Check generated C++ sources into a dedicated generated target so normal builds require only the pinned ANTLR C++ runtime, not Java. Use SLL parsing with LL fallback.
  • Parse all syntactically valid constructs into an internal oq3 MLIR dialect. Unsupported constructs must survive parsing and fail later with a precise “not lowerable yet” diagnostic rather than a misleading syntax error.
  • Model source-level declarations, assignments, gates, calls, modifiers, IO, control flow, timing, calibration blocks, and source types. Attach exact file/range locations from SourceMgr, including included files.
  • Add a semantic-analysis pass for scope, declaration-before-use, type inference, promotions, casts, constness, version rules, recursion, operand cardinality, and gate broadcasting.
  • Keep the source dialect internal initially; preserve the existing translateQASM3ToQC API.

3. Lower typed OpenQASM IR using MLIR infrastructure

OpenQASM concept Lowering
Boolean and classical arithmetic arith, math, builtin integer/float/complex types
Mutable bit[n] registers memref<nxi1> with loads/stores; integrate with issue #1726
Structured mutable scalars scf.if results and scf.for/scf.while iterated arguments
Inputs, outputs, subroutines, gates func.func, function arguments/results, func.call; integrate with issue #1659
Quantum storage and operations QC qubit types, memrefs, gates, measurement, reset, and modifiers
Switch and unstructured exits scf.index_switch where possible; otherwise cf blocks and branches
Angle, duration, stretch, power Small OpenQASM/QC-specific types or ops until target-independent lowering exists
  • Represent signedness and declared widths explicitly during semantic analysis; select signed/unsigned arith operations accordingly.
  • Use OperationFolder, dialect folding, canonicalization, and CSE for ordinary constants. Retain a small APInt/APFloat evaluator only where OpenQASM requires a compile-time value before IR shape can be formed.
  • Represent custom gates and subroutines as symbols and calls. Leave inlining to an explicit MLIR pass.
  • Implement pow as a QC modifier operation capable of dynamic/fractional exponents; statically integral cases may later canonicalize to repetition/inversion.
  • Resolve includes textually through llvm::vfs::FileSystem, include paths, and SourceMgr; detect cycles and preserve parent locations.
  • Parse calibration bodies losslessly as opaque source operations until a pulse-level dialect is available.

4. Public interface additions

Keep both existing overloads and add an options overload without breaking callers:

  • OpenQASMVersion: V3_0 and V3_1.
  • OpenQASMTranslationOptions:
    • optional version override used only when the source omits OPENQASM;
    • include search paths and virtual filesystem;
    • opt-in qelib1.inc/OpenQASM 2 compatibility;
    • diagnostic limit.
  • Default omitted-version behavior to OpenQASM 3.0.
  • Do not expose the internal oq3 dialect as a stable public API until its operation set has completed one compatibility cycle.

5. Performance work

  • Replace the templated sink parser and global std::function maps with generated parser code and a single TableGen/generated gate registry.
  • Stop eagerly extracting every qubit in a register; materialize constant or dynamic loads on demand.
  • Replace expression-string cache keys with dominance-aware MLIR values and standard CSE/canonicalization after issue 🎨 Allow repeated memref.loads of the same qubit #1893 removes repeated-load restrictions.
  • Avoid rebuilding and dynamically casting the complete legacy standard-gate map for every translation.
  • Deduplicate constants through folding and avoid repeated arith.constant emission for builtin constants.
  • Preserve gates and subroutines as calls to prevent parse-time IR expansion.
  • Add benchmarks for 10k/100k statements, large mostly-unused registers, nested control flow, repeated indexing, and deeply composed gates. Track wall time, peak memory, generated operation count, and compiler build time; enforce a 15% regression threshold after establishing the new frontend baseline.

Test Plan

  • Import the official OpenQASM example and grammar-validation corpora for both 3.0 and 3.1.
  • Maintain a generated conformance matrix mapping every grammar statement, expression, type, and versioned standard gate to “parses”, “semantically validates”, “lowers”, or “diagnosed unsupported”.
  • Add focused regressions for:
    • width-bearing declarations, numeric bases/separators, all precedence levels, casts, and trailing commas;
    • version placement, duplicate versions, unsupported versions, and versioned standard gates;
    • absent/present stdgates.inc, custom includes, cycles, and included-file diagnostics;
    • measurement with and without targets and measurement inside each control-flow region;
    • mutable scalars and registers across if, ascending/descending/empty for, and while;
    • dynamic zero steps, boundary overflow, non-divisible ranges, sets, and expression iteration;
    • block-local shadowing, anonymous scopes, IO, and undefined values;
    • every standard gate, broadcasting, modifiers, duplicate operands, and recursive definitions;
    • invalid programs producing diagnostics without crashes, hangs, or malformed modules.
  • Verify every successful result before comparison, then run QC cleanup and the supported QC→QCO/QIR pipelines.
  • Add parser fuzzing and differential grammar tests; arbitrary bytes and syntactically invalid inputs must terminate with bounded diagnostics.
  • Preserve all current equivalence tests, but supplement them with structural tests that inspect region arguments/results and classical storage rather than comparing only happy-path circuits.

Assumptions

@denialhaag

denialhaag commented Jul 15, 2026

Copy link
Copy Markdown
Member Author

The stability improvements generally look good to me. I only have two comments:

  1. I'm curious to see how it rejects dynamic zeros. I had a first version of this, but it wouldn't have been lowerable to jeff.
  2. The global clang-tidy ignore is intentional. This is how the MLIR project itself does it. We also do the same in jeff-mlir.

Now to the bigger part: If we really want to go with an oq3 dialect, we might as well do it now. This PR feels too big to be purely temporary code that is refactored again completely soon. That said, I do not see how an oq3 dialect would necessarily help us in not having two layers of errors, unless the operands of the operations are mostly typed as the MLIR equivalent of Any. I feel like the current errors in the emission are either temporary limitations or typing-related. But I might be missing something.

Generally speaking, I don't really know how to deal with all of this efficiently. Doing too many LLM-generated refactors is very annoying. Given the size this PR is already at, I spent way too long reading and streamlining LLM-generated code. I agree that the result is still not ideal, and we should improve upon it. I might not be having a point, but I guess I want to say that we should be really sure what we want this to look like. Any LLM-generated refactor is another start from zero. I would really like to get to a point where we have a stable foundation we know and can easily work with.

@burgholzer

Copy link
Copy Markdown
Member

Now to the bigger part: If we really want to go with an oq3 dialect, we might as well do it now. This PR feels too big to be purely temporary code that is refactored again completely soon. That said, I do not see how an oq3 dialect would necessarily help us in not having two layers of errors, unless the operands of the operations are mostly typed as the MLIR equivalent of Any. I feel like the current errors in the emission are either temporary limitations or typing-related. But I might be missing something.

Generally speaking, I don't really know how to deal with all of this efficiently. Doing too many LLM-generated refactors is very annoying. Given the size this PR is already at, I spent way too long reading and streamlining LLM-generated code. I agree that the result is still not ideal, and we should improve upon it. I might not be having a point, but I guess I want to say that we should be really sure what we want this to look like. Any LLM-generated refactor is another start from zero. I would really like to get to a point where we have a stable foundation we know and can easily work with.

Let me try the following: I'll hand the full rewrite from the plan above to Codex and let it create a new draft PR showing how the result would look like. Based on that, we can (hopefully) better judge whether that is a direction worth pursuing.

@mergify mergify Bot added the conflict label Jul 15, 2026
@denialhaag

Copy link
Copy Markdown
Member Author

Two more things that just came to mind and can maybe be thrown into Codex as well:

  1. In an oq3 dialect, can the semantic analysis be done exclusively with operation-level MLIR verifiers, or do we need module-level verification passes as well? If we need module-level verification, is it really not better to do that during program creation (i.e., as it is done now)? We should avoid walking the same program more than once.
  2. Is it feasible to report the original SMLocs if semantic analysis fails? The MLIR locations are likely useless for a user.

@denialhaag
denialhaag marked this pull request as draft July 22, 2026 17:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

conflict 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