diff --git a/.agent/plans/oq3-foundation.md b/.agent/plans/oq3-foundation.md new file mode 100644 index 0000000000..a2a6d61f88 --- /dev/null +++ b/.agent/plans/oq3-foundation.md @@ -0,0 +1,774 @@ +# Complete a direct, specification-driven OpenQASM-to-QC frontend + +This ExecPlan is a living document. The sections `Progress`, +`Surprises & Discoveries`, `Decision Log`, and `Outcomes & Retrospective` must +be kept current as work proceeds. + +This ExecPlan must be maintained in accordance with `.agent/PLANS.md` from the +repository root. + +## Purpose / Big Picture + +MQT Core must accept as many valid OpenQASM programs as its compiler can +faithfully process, diagnose unsupported language features at the QC emission +boundary, and carry every accepted program through the complete compiler. After +this work, a user can give OpenQASM to `QCProgram::fromQASMString`, receive +verified QC directly, optimize it in QCO, optionally serialize and deserialize +it through `jeff`, convert it back to QC, and obtain valid QIR. The standard +acceptance path is QC to QCO to reconstructed QC to QIR; `jeff` compatibility is +tracked independently and never blocks valid QC or QIR. The unit tests make that +path observable stage by stage. + +The parser and semantic analyzer remain independent of MLIR and continue to +recognize valid source even when the selected compiler target lacks a concept. +The gate `pow @` modifier is the defining example: parsing and semantic analysis +succeed, but the direct QC emitter reports a source-located unsupported-feature +diagnostic until QC gains power semantics. There is no OQ3 MLIR dialect or +OQ3-to-QC conversion. An intermediate dialect that cannot proceed through the +compiler adds maintenance and test surface without user value. + +The scope is the staged frontend under `mlir/include/mlir/Target/OpenQASM` and +`mlir/lib/Target/OpenQASM`, direct QC translation under +`mlir/lib/Dialect/QC/Translation`, the OpenQASM fixture corpus and compiler +tests, and only those existing conversion files for which a full-chain fixture +demonstrates a real defect. Preserve the legacy `QuantumComputation` parser and +unrelated behavior. Do not push or publish GitHub text under this plan. + +## Progress + +- [x] (2026-07-15) Replaced the legacy stream adapter with an LLVM-native lexer, + grammar-only parser, persistent syntax tree, and separate semantic + analyzer. +- [x] (2026-07-16) Implemented source locations and includes, lexical scope, + types, definite initialization, assignments, expressions, custom gates, + broadcasting, dynamic indices, measurement, `if`, `for`, `while`, and + loop-carried scalar and bit state in the staged frontend. +- [x] (2026-07-16) Ported 27 healthy behavior fixtures from the earlier OpenQASM + implementation and completed a cleanup and clean-build validation of that + implementation. +- [x] (2026-07-16) Re-evaluated the architecture after review and concluded that + the reduced OQ3 dialect is unnecessary. Inspected the public compiler + program APIs and identified the compiler paths that require separate + acceptance contracts. +- [x] (2026-07-16) Revised this plan after critical review: isolate direct + emission in a private emitter, build the full-chain harness before + changing conversions, and require evidence-backed minimal regression + fixes. +- [x] (2026-07-16) Removed the OQ3 dialect, OQ3-to-QC conversion, registrations, + documentation, and dialect-specific tests, while relocating the gate + catalog to the frontend. +- [x] (2026-07-16) Implemented direct typed-program-to-QC emission behind + private `OpenQASMToQCEmitter` files and kept `TranslateQASM3ToQC.cpp` as a + small public adapter. +- [x] (2026-07-16) Converted target tests from OQ3 inspection to direct QC + behavior and precise target diagnostics, including rejection of `pow @`. +- [x] (2026-07-16) Defined a shared `{name, source}` OpenQASM compiler corpus + and added public-API full-chain tests, including both direct composition + and `runDefaultPipeline`. +- [x] (2026-07-16) Used failing full-chain stages to isolate the `jeff` + entry-point round-trip defect and added a parser-independent native + regression. Retained the structured QC-to-QCO changes with their existing + native regressions. +- [x] (2026-07-16) Minimized the complete diff against `origin/main`, removing + superseded OQ3 code, duplicated dispatch data, stale registrations, and + iteration artifacts. Retained only the QC-to-QCO structured-state and + `jeff` entry-point corrections backed by native regressions. +- [x] (2026-07-16) Added maintained parser, semantic, QC-emission, standard + Adaptive, `jeff`, and Base support matrices. +- [x] (2026-07-16) Ran the affected frontend, translation, conversion, compiler, + QIR, and legacy-parser tests; warning-as-error documentation; repository + lint; diff checks; and sequential coverage. The substantive frontend and + emitter surface reached 90.7 percent line coverage. +- [x] (2026-07-16) Incorporated the first post-implementation review: tightened + the accepted-input contract for runtime indices, made structured custom + gate capability checks transitive, added native result-bearing SCF + conversion regressions and verified result types across `jeff`. +- [x] (2026-07-16) Ran the complete affected validation and repository checks + after the first review fixes. Clean sequential coverage reached 91.0 + percent lines over the substantive frontend and emitter sources. +- [x] (2026-07-16) Incorporated the final review: replaced Boolean index + resolvability with a small constant lattice, made literal branches and + equal-constant joins precise, rejected multi-iteration induction indices, + and collapsed dispatch and custom-gate expansion into one overflow-safe + projected-emission budget. +- [x] (2026-07-16) Added a mutable floating-point `for`/`while` fixture to the + initial `jeff` corpus, exact QIR output-recording assertions, and stronger + native result-bearing `if`/`while` conversion semantics. +- [x] (2026-07-16) Closed the checked-integer acceptance gap by rejecting + non-folded checked integer arithmetic and ranges at the QC boundary with a + source-located diagnostic while preserving frontend support. +- [x] (2026-07-16) Ran final documentation, lint, architecture, affected unit, + legacy-parser, and clean sequential coverage validation. Coverage is 89.9 + percent (4117/4579); the remaining lines do not justify test-only padding. +- [x] (2026-07-16) Re-read the complete effective branch diff after the final + implementation commits. Removed the unused gate-policy field from the + resolved program model and corrected stale plan claims; retained the + production and regression surface required by the demonstrated contracts. +- [x] (2026-07-16) Repaired CI portability after Windows MSVC builds rejected + Clang/GCC-only signed-overflow builtins in semantic constant evaluation. + The evaluator now uses LLVM's portable overflow helpers. Restored the + non-inheriting MLIR clang-tidy configuration after the rebase accidentally + enabled the repository-wide checks and produced 1,194 unrelated reports. +- [x] (2026-07-16) Began the post-review correction by fixing the ten remaining + clang-tidy diagnostics and moving the frontend's implementation headers + from the library source directory to + `mlir/include/mlir/Target/OpenQASM/Detail`. The owning containers remain + standard vectors, while local bounded working sets continue to use LLVM + small vectors. +- [x] (2026-07-16) Removed `jeff`-derived QC-emission rejection of runtime + indices, varying induction indices, and non-folded integer expressions. + Implemented faithful runtime signed and unsigned integer arithmetic, + retained dynamic index bounds assertions, and replaced division-based + range trip counts with constant structured bounds or comparison-driven + inclusive loops. +- [x] (2026-07-16) Split compiler contracts into a 17-program standard + QC-to-QCO-to-QC-to-QIR corpus, a 6-program `jeff`-compatible round-trip + corpus, and four explicit `jeff`-boundary failure tests. Added MLIR's + canonical `cf.assert`-to-LLVM lowering to QIR with a parser-independent + regression so runtime source checks survive the standard path. +- [x] (2026-07-16) Rebuilt and ran all affected frontend, translation, + conversion, compiler, QIR, and legacy-parser tests. The warning-as-error + documentation session, repository lint, and diff checks pass. + +## Surprises & Discoveries + +- Observation: the old MLIR entry point copied the `SourceMgr` main buffer into + `std::istringstream`, losing the source manager's include and location model. + Evidence: the replacement parser consumes LLVM buffers directly and its + persistent program retains included source identity. + +- Observation: the legacy scanner recognized `for` and `while`, but its parser + could not construct those statements. Evidence: the staged frontend now has + source fixtures that produce and exercise standard SCF regions. + +- Observation: valid source and target support are distinct, but a dialect is + not required to preserve that distinction. Evidence: the typed semantic + program already retains modifiers and source locations, so a QC emitter can + reject `pow @` before creating target IR. + +- Observation: the OQ3 dialect has shrunk to gate declarations, applications, + and modifiers while classical computation and control flow already use + standard MLIR. Its conversion mostly expands typed custom gates and maps a + gate catalog to QC, work that can be performed directly from the typed model. + +- Observation: the initial QC-to-QCO changes for structured classical and + quantum state lacked independent evidence. The retained implementation is now + justified by four parser-independent conversion regressions distilled from + full-chain failures, rather than by the mere existence of structured OpenQASM. + +- Observation: `runDefaultPipeline` covers QC to QCO optimization, QCO back to + QC, and QC to QIR, but intentionally does not include a `jeff` round trip. + Therefore the standard path is the acceptance contract; `jeff` has separate + positive and explicit boundary-failure suites. + +- Observation: structured control flow generally requires the Adaptive QIR + profile, while straight-line circuits can exercise both Base and Adaptive + profiles. Encoding expected failures as fixture flags would hide unsupported + behavior, so the corpus contains only names and sources and profile grouping + is expressed by the test suites that select it. + +- Observation: `jeff` round trips preserved entry functions with observable bit + results, but `JeffToQCO` restored the `entry_point` marker only for + result-less functions. Evidence: the first explicit chain reached + reconstructed QC but Adaptive QIR reported that no entry point existed. The + native `jeff` regression now proves that nonempty result types and the marker + survive together. + +- Observation: the `jeff` representation cannot preserve the frontend's runtime + `cf.assert` bounds checks. Evidence: genuinely runtime-dynamic indexing + reaches verified QC, optimized QCO, reconstructed QC, and QIR, while + QCO-to-`jeff` rejects `cf.assert`. The limitation is now reported at + `intoJeff()` rather than by the source emitter. + +- Observation: `scf.for` is an automatic allocation scope, so selecting the + nearest such scope left result-bearing `scf.if` scratch storage inside a loop. + Evidence: the nested native regression found the alloca in the loop body. + Hoisting to the enclosing function allocates each conditional's storage once. + +- Observation: successful final QIR alone does not prove that observable entry + results survived intermediate formats. Evidence: the full-chain tests now + compare entry result types in QC, optimized QCO, `jeff` bytes and restored + `jeff`, reconstructed QCO, and reconstructed QC before checking the QIR status + signature and output-recording calls. + +- Observation: static loop bounds do not make a multi-iteration induction value + static at each source use, but this is not a QC limitation. Evidence: + `for uint i in [0:2] { x q[i]; }` now reaches QIR with runtime bounds checks + and fails only at QCO-to-`jeff`. + +- Observation: separate dynamic-dispatch and custom-gate expansion limits can + each pass while their composition is excessive. Evidence: 4096 dispatch leaves + applying a 25-operation custom gate project 102400 primitive emissions. + +- Observation: non-folded checked integer expressions emit i128 arithmetic and + `cf.assert` operations. MLIR's control-flow-to-LLVM conversion requires the + assert pattern to be registered separately. Once registered, checked integer + state reaches QIR; `jeff` still rejects it at its conversion boundary. + +## Decision Log + +- Decision: remove the OQ3 MLIR dialect and emit QC directly from the typed + frontend program. Rationale: OpenQASM is compiler input, and successful import + should mean that the program can enter the compiler's supported dialects. + Unsupported target concepts remain diagnosable from the typed source model. + Date/Author: 2026-07-16 / Codex. + +- Decision: retain the staged lexer, syntax, and semantic design. Rationale: + parsing, source-language validity, and target emission have different + responsibilities, and includes, scope, and precise diagnostics already rely on + that separation. Date/Author: 2026-07-16 / Codex. + +- Decision: keep the existing `oq3::frontend` namespace unless a narrow rename + is independently justified. Rationale: the namespace denotes the OpenQASM 3 + language frontend and is not itself an MLIR dialect; renaming every frontend + type would add churn without changing behavior. Only dialect-specific + identifiers must disappear. Date/Author: 2026-07-16 / Codex. + +- Decision: put direct emission in private `OpenQASMToQCEmitter.h` and + `OpenQASMToQCEmitter.cpp`, leaving `TranslateQASM3ToQC.cpp` as a small public + adapter. Rationale: a large emitter should not obscure the stable translation + entry points, and private files avoid exposing a second public API. + Date/Author: 2026-07-16 / Codex. + +- Decision: rename the frontend library target to `MLIROpenQASMFrontend`. + Rationale: after emission moves to QC translation, the target contains only + lexing, parsing, persistent syntax, semantic analysis, and the gate catalog; + the name should describe that boundary. Date/Author: 2026-07-16 / Codex. + +- Decision: keep custom-gate expansion limits and QC capability preflight in the + emitter. Rationale: semantics validates source legality, including recursion, + while expansion cost and target representability depend on the selected + output. Date/Author: 2026-07-16 / Codex. + +- Decision: reject unsupported gate modifiers before emitting any part of the + affected application. Rationale: target failure must be precise and cannot + silently alter or partially lower source semantics. Date/Author: 2026-07-16 / + Codex. + +- Decision: build the full compiler-chain corpus before altering downstream + conversions and do not blanket-revert QC-to-QCO. Rationale: the current branch + may contain both necessary and speculative hunks. Stage-specific failures and + minimized native-IR regressions provide the evidence needed to retain, + simplify, or remove each change safely. Date/Author: 2026-07-16 / Codex. + +- Decision: conversion unit tests remain parser-independent. Rationale: a QC, + QCO, `jeff`, or QIR conversion regression should construct or parse the + smallest native MLIR that demonstrates the conversion invariant. OpenQASM + belongs only in translation and compiler integration tests. Date/Author: + 2026-07-16 / Codex. + +- Decision: share at most `{name, source}` across OpenQASM compiler fixtures. + Rationale: per-fixture expected-failure or capability flags turn gaps into + accepted behavior. Separate positive suites select the source subset they are + required to support. Date/Author: 2026-07-16 / Codex. + +- Decision: preserve observable `jeff` entry-point results when restoring QCO. + Rationale: `jeff` serialization already retains those results; replacing them + with a synthetic status code discarded program output and prevented the + reconstructed QC module from reaching QIR. Result-less legacy entry points + still receive the historical i64 status result. Date/Author: 2026-07-16 / + Codex. + +- Decision: accept runtime dynamic indices at the QC boundary and retain their + bounds assertions. Rationale: QC and QIR represent the checks faithfully; + inability to serialize them through `jeff` is an optional-format limitation + reported by `intoJeff()`, not a reason to reject OpenQASM. Date/Author: + 2026-07-16 / Codex. + +- Decision: retain the second structured-terminator conversion phase in + QC-to-QCO. Rationale: result-bearing `if`, `for`, and `while` need the final + region-local QCO value maps, and converting terminators in the first worklist + makes correctness depend on traversal order. Four native regressions now cover + the parent and terminator contracts. Date/Author: 2026-07-16 / Codex. + +- Decision: compute structured-control capability transitively and memoize it + per reachable custom gate. Rationale: modifiers on a wrapper around a looped + gate are just as unsupported as modifiers on the looped gate itself, while + unused definitions must have no effect on accepted source. Date/Author: + 2026-07-16 / Codex. + +- Decision: lower all valid induction-variable indices with runtime bounds + checks. Rationale: varying indices are first-class QC and QIR behavior; + constant simplification is an optimization rather than an acceptance rule. + Date/Author: 2026-07-16 / Codex. + +- Decision: enforce one 100000-operation projected-emission budget that composes + custom-gate expansion and register dispatch with overflow-safe multiplication. + Rationale: emitted work, not either mechanism independently, is the relevant + safety bound. Date/Author: 2026-07-16 / Codex. + +- Decision: emit non-folded signed integer arithmetic with overflow assertions, + unsigned arithmetic with 64-bit wrap semantics, and inclusive ranges without + division-based trip counts. Rationale: these operations traverse the standard + QC-to-QIR pipeline faithfully. Unsupported `jeff` serialization is tested at + that boundary. Date/Author: 2026-07-16 / Codex. + +- Decision: use constant `scf.for` bounds when all range components are known + and a comparison-driven `scf.while` otherwise. Rationale: positive, negative, + empty, singleton, non-divisible, and boundary ranges avoid endpoint overflow, + while dynamic zero steps remain guarded by `cf.assert`. Date/Author: + 2026-07-16 / Codex. + +- Decision: accept the measured 89.9 percent frontend and emitter line coverage + without adding tests whose only purpose is crossing a round-number threshold. + Rationale: final review identified no missing behavior-driven regression, and + line-only tests would not improve evidence for the supported contracts. + Date/Author: 2026-07-16 / Codex. + +## Outcomes & Retrospective + +The completed frontend groundwork is retained: the native parser and semantic +analyzer cover the source-language behavior needed by the compiler. The earlier +OQ3 target architecture has been removed in favor of direct QC emission. + +The direct architecture and end-to-end behavior are implemented. Seventeen +OpenQASM fixtures traverse direct QC, QCO cleanup and optimization, +reconstructed QC, and Adaptive QIR; the same fixtures pass `runDefaultPipeline`. +Four straight-line fixtures also reach Base QIR. A separate six-program corpus +round-trips through `jeff`, while four explicitly tracked cases reach optimized +QCO and then fail at `intoJeff()`. The standard corpus includes runtime and +induction-variable indexing, checked integer state, and dynamic ranges in +addition to custom gates and structured control flow. + +The downstream production corrections are constrained to demonstrated conversion +invariants. QC-to-QCO preserves classical results alongside linear quantum state +through `if`, `for`, and `while`, converts their terminators after region +contents, and allocates conditional scratch storage once per function. +`JeffToQCO` restores entry-point markers without losing observable results. Both +areas have parser-independent native regressions. + +Runtime-dynamic indices, multi-iteration induction indices, and non-folded +integer expressions now produce verified QC and reach QIR. Signed operations +assert overflow and invalid division, unsigned operations wrap at 64 bits, and +dynamic ranges use comparison-driven structured control flow. MLIR's canonical +`cf.assert` conversion preserves these checks in QIR. Cases that `jeff` cannot +represent fail at its conversion boundary instead of reducing source support. + +## Context and Orientation + +`mlir/lib/Target/OpenQASM/Frontend.cpp` owns source buffers and orchestrates +parsing. Implementation headers live under +`mlir/include/mlir/Target/OpenQASM/Detail`; their corresponding sources under +`mlir/lib/Target/OpenQASM` implement tokenization, grammar, recovery, persistent +syntax, and semantic analysis. `OpenQASMSemantics.cpp` resolves syntax into the +`TypedProgram` declared in `mlir/include/mlir/Target/OpenQASM/Frontend.h`. These +files use LLVM support but do not require an `MLIRContext`. A `TypedProgram` is +a compact resolved representation containing expressions, conditions, +declarations, statements, gate definitions, source locations, and output +registers. + +Direct QC construction lives in the private +`mlir/lib/Dialect/QC/Translation/OpenQASMToQCEmitter.cpp`. The reusable gate +metadata lives in `mlir/include/mlir/Target/OpenQASM/GateCatalog.h` and +`mlir/lib/Target/OpenQASM/GateCatalog.cpp`, where semantic analysis and target +emission share one authoritative catalog. + +The stable user-facing translation functions are declared in +`mlir/include/mlir/Dialect/QC/Translation/TranslateQASM3ToQC.h` and implemented +in `mlir/lib/Dialect/QC/Translation/TranslateQASM3ToQC.cpp`. They accept either +an LLVM source manager or source text and return an owning reference to an MLIR +module. `QCProgram::fromQASMString` in `mlir/lib/Compiler/Programs.cpp` calls +this API. The translation source must stay small; a new private emitter beside +it owns all typed-program-to-QC construction. + +QC uses reference-like qubits. QCO is the optimizer dialect and uses linear SSA +values, meaning each quantum operation returns the next value representing its +qubit. QC-to-QCO and QCO-to-QC bridge those models. `jeff` is a serializable +exchange representation reached from QCO. QIR is LLVM-based output reached from +QC. The compiler program wrappers in `mlir/include/mlir/Compiler/Programs.h` +provide ownership-safe transitions between these representations. + +`mlir/unittests/programs/qasm_programs.cpp` and its header contain reusable +OpenQASM source fixtures. Translation equivalence tests live in +`mlir/unittests/Dialect/QC/Translation/test_qasm3_translation.cpp`. The complete +public compiler path belongs in +`mlir/unittests/Compiler/test_compiler_pipeline.cpp`. Tests directly attached to +QC-to-QCO, QCO-to-QC, `jeff`, and QIR must use their dialect-native builders or +small MLIR strings, not invoke the OpenQASM parser. + +## Plan of Work + +### Milestone 1: remove the intermediate dialect and establish direct emission + +Delete `mlir/include/mlir/Dialect/OQ3`, `mlir/lib/Dialect/OQ3`, +`mlir/include/mlir/Conversion/OQ3ToQC`, `mlir/lib/Conversion/OQ3ToQC`, and +`mlir/unittests/Dialect/OQ3`. Remove their `add_subdirectory` entries, generated +operation dependencies, tool dialect registrations, unit-test registration, and +link libraries from the adjacent CMake files. Delete `docs/mlir/OQ3.md` and +remove its navigation entries. Do not remove `oq3::frontend` merely because its +name contains `oq3`; it is language code rather than a dialect identifier. + +Move the gate catalog to `mlir/include/mlir/Target/OpenQASM/GateCatalog.h` and +`mlir/lib/Target/OpenQASM/GateCatalog.cpp`, retaining one authoritative table +for language gates, standard-library gates, compatibility aliases, canonical QC +primitives, parameter counts, control counts, target counts, variadic controls, +and inverse aliases. Update semantic includes and namespaces without duplicating +the table. + +Rename the CMake library in `mlir/lib/Target/OpenQASM/CMakeLists.txt` from +`MLIROpenQASMTarget` to `MLIROpenQASMFrontend`. It contains `Frontend.cpp`, the +lexer, syntax, semantics, and `GateCatalog.cpp`, and links only what those +stages use. Remove `mlir/include/mlir/Target/OpenQASM/OpenQASM.h` and the old +emitter source after direct emission has replaced their behavior. + +Add private `mlir/lib/Dialect/QC/Translation/OpenQASMToQCEmitter.h` and +`OpenQASMToQCEmitter.cpp`. The header declares only a translation-internal +function that accepts a resolved `oq3::frontend::TypedProgram` and an +`MLIRContext` and returns `OwningOpRef`. `TranslateQASM3ToQC.cpp` +parses, analyzes, prints collected source diagnostics on failure, invokes this +private function, verifies the returned QC module, and contains no lowering +implementation. + +The emitter reuses standard `arith`, `cf`, `func`, `math`, `memref`, `scf`, and +`ub` operations for classical behavior and emits QC operations directly. Port +only the target logic from OQ3-to-QC: catalog-to-primitive dispatch, implicit +and variadic controls, inverse aliases, the four-parameter `cu` phase behavior, +ordered inverse/positive-control/negative-control modifiers, and recursive +inlining of typed custom-gate bodies. Semantic analysis continues to reject +source-illegal recursion. The emitter preflights reachable custom-gate expansion +cost, target support, modifier operands, and structured custom-gate limitations +before creating each affected application. A `pow @` modifier produces a +source-located error and a null translation result; scalar exponentiation and +the scalar `pow()` function remain supported. + +Acceptance for this milestone is a clean build with no OQ3 dialect or conversion +target and direct QC translation for existing supported sources. Repository +searches for `OQ3Dialect`, OQ3 operation class names, `createOQ3ToQCPass`, and +`MLIROQ3` must be empty. A search for `oq3::frontend` is not an acceptance +failure. + +### Milestone 2: convert target tests to direct behavior + +Refactor `mlir/unittests/Target/OpenQASM/test_openqasm.cpp` so parser tests +inspect parse results, semantic tests inspect `TypedProgram`, and target tests +inspect verified QC or emitted diagnostics. Remove all tests whose only purpose +is OQ3 operation verification. Preserve behavior tests for source ownership, +recovery, includes, scope, initialization, expressions, broadcasting, dynamic +dispatch, control flow, recursion, and cost bounds. + +Add a positive direct-emission test for representative primitive and custom +gates and a negative test proving that a valid `pow @` program parses and +analyzes but `qc::translateQASM3ToQC` returns null with a source-located message +stating that QC power support is unavailable. Add equivalent focused cases for +every other frontend-accepted feature that the emitter rejects. Update +`mlir/unittests/Target/OpenQASM/CMakeLists.txt` to link `MLIROpenQASMFrontend`, +`MLIRQCTranslation`, and only directly used test libraries. + +Keep exact QC equivalence tests in +`mlir/unittests/Dialect/QC/Translation/test_qasm3_translation.cpp`. They compare +canonicalized direct translation against QC builder references and should cover +catalog aliases, controls, inverses, custom-gate expansion, expressions, +broadcasting, measurement, and structured control flow where a stable reference +is practical. + +### Milestone 3: build the full compiler-chain corpus + +In `mlir/unittests/programs/qasm_programs.h` and `qasm_programs.cpp`, expose a +small shared corpus whose descriptors contain only a stable name and source. +Keep the sources themselves as the existing named constants where useful. Do not +attach capability flags to descriptors. Expose separate source groups for the +broad standard QC-to-QCO-to-QC-to-QIR path, the smaller `jeff` round trip, known +`jeff`-boundary failures, and the straight-line Base subset. Include nested +control flow, loop-carried state, runtime and induction-variable indexing, +checked integer arithmetic, dynamic ranges, gates, reset, barrier, and +observable outputs. + +Add a parameterized integration suite to +`mlir/unittests/Compiler/test_compiler_pipeline.cpp`. For every source in the +standard group, use the public APIs in exactly this order: + + QCProgram::fromQASMString + QCProgram::intoQCO + QCOProgram::cleanup + QCOProgram::runPassPipeline("mqt-qco-default") + QCOProgram::cleanup + QCOProgram::intoQC + QCProgram::cleanup + QCProgram::intoQIR(QIRProfile::Adaptive) + QIRProgram::llvmIR and QIRProgram::toBitcode + +The `jeff`-positive group inserts serialization and deserialization between +optimized QCO and reconstructed QC. The incompatible group requires every stage +through optimized QCO to succeed and `intoJeff()` to fail. Retain copies at +ownership boundaries, require nonempty LLVM IR and valid bitcode, and run the +straight-line subset through Base and Adaptive QIR. + +Add a separate parameterized call to `runDefaultPipeline` for every broad corpus +source, requesting Adaptive QIR and checking its LLVM IR and bitcode. This +proves the production default path independently; it does not replace the +explicit chain. Test failure messages must include the source name and stage. + +### Milestone 4: isolate and fix demonstrated downstream defects + +Run the full-chain corpus against the current conversion implementations. For +each failure, save the smallest native QC, QCO, or `jeff` MLIR that reproduces +the stage failure. Add that reduced program to the appropriate conversion unit +test using existing program builders when they express it cleanly, otherwise a +small MLIR string. Do not make these unit tests parse OpenQASM. + +Inspect the branch diff in `mlir/lib/Conversion/QCToQCO/QCToQCO.cpp` hunk by +hunk. Retain a change only when a reduced regression proves that it is needed, +and simplify it to the smallest dialect-native correction. Pay particular +attention to SCF operands and results, region arguments, `scf.yield`, +`scf.condition`, measurement results, and the distinction between classical +state and linear quantum state. Apply the same evidence rule to +`mlir/lib/Conversion/QCOToQC`, `jeff` conversions, and QC-to-QIR. Do not edit a +downstream conversion merely because it was named in this plan. + +After each fix, run its focused native conversion test first, then the failing +full-chain fixture, then the entire corpus. If a feature cannot be represented +faithfully by the current pipeline, move its failure to the direct QC emitter +only when the limitation is intrinsic to accepted compiler dialects rather than +a correctable conversion bug, and document the diagnostic and matrix status. +Never add a fixture flag that makes the integration test accept failure. + +### Milestone 5: minimize, document, and validate + +Inspect the effective diff against `origin/main`, including all commits and +unstaged files. Delete obsolete OQ3 concepts, duplicate gate dispatch, +superseded tests, stale target names, temporary compatibility wrappers, +iteration comments, and downstream conversion hunks lacking native regression +evidence. Keep `TranslateQASM3ToQC.cpp` small and keep production dependencies +pointing in one direction: QC translation depends on the OpenQASM frontend, not +the reverse. + +Create `docs/mlir/OpenQASM.md` and link it from `docs/mlir/index.md` and the +relevant translation overview. It contains two maintained feature tables but +does not duplicate the language specification. The first covers parser and +semantic behavior. The second has columns for feature, Parse, Semantics, QC, +standard Adaptive QIR, `jeff`, Base, restriction or rejection reason, and the +representative test. Use precise statuses such as supported, recognized and +rejected semantically, or accepted by the frontend and rejected by QC. Mark +structured fixtures Adaptive-only and record Base support only for the tested +straight-line subset. List `pow @` as parsed and semantically valid but rejected +by QC. Update `CHANGELOG.md` to describe direct OpenQASM import without an OQ3 +dialect claim. + +Run formatting, all affected unit binaries, the legacy parser regression, +warning-as-error documentation, coverage, and repository lint after cleanup. +Record the final evidence in this plan's progress, discoveries, outcomes, and +artifacts sections. + +## Concrete Steps + +Run all commands from the repository root. Preserve unrelated changes and +inspect status before editing: + + git status --short --branch + git diff --stat origin/main...HEAD + git diff --stat origin/main + +Configure a clean debug build using an installed MLIR 22.1 CMake package. The +path is supplied by the environment and must not be committed to this plan: + + MLIR_DIR=/path/to/mlir/lib/cmake/mlir cmake --preset debug + +Build the direct frontend, translation, conversion, and compiler tests: + + cmake --build build/debug --target \ + mqt-core-mlir-unittest-openqasm-target \ + mqt-core-mlir-unittest-qc-translation \ + mqt-core-mlir-unittest-qc-to-qco \ + mqt-core-mlir-unittest-qco-to-qc \ + mqt-core-mlir-unittest-jeff-round-trip \ + mqt-core-mlir-unittests-compiler -j4 + +Run the binaries directly so stage failures are visible: + + ./build/debug/mlir/unittests/Target/OpenQASM/mqt-core-mlir-unittest-openqasm-target + ./build/debug/mlir/unittests/Dialect/QC/Translation/mqt-core-mlir-unittest-qc-translation + ./build/debug/mlir/unittests/Conversion/QCToQCO/mqt-core-mlir-unittest-qc-to-qco + ./build/debug/mlir/unittests/Conversion/QCOToQC/mqt-core-mlir-unittest-qco-to-qc + ./build/debug/mlir/unittests/Conversion/JeffRoundTrip/mqt-core-mlir-unittest-jeff-round-trip + ./build/debug/mlir/unittests/Compiler/mqt-core-mlir-unittests-compiler + +Build and run the QC-to-QIR Base and Adaptive test targets discovered under +`mlir/unittests/Conversion/QCToQIR`, and run all configured MLIR unit tests to +catch target-name or registration omissions: + + cmake --build build/debug --target mqt-core-mlir-unittests -j4 + ctest --test-dir build/debug --output-on-failure -L mqt-mlir-unittests + +Build and run the unaffected legacy parser regression: + + cmake --build build/debug --target mqt-core-ir-test -j4 + (cd build/debug/test/ir && ./mqt-core-ir-test --gtest_filter='Qasm3ParserTest.*') + +Check the architecture after deletion. These searches are deliberately limited +to dialect-specific identifiers and must return no matches: + + rg 'OQ3Dialect|OQ3Ops|ApplyGateOp|GateDeclOp|createOQ3ToQCPass|MLIROQ3' mlir docs + rg 'add_subdirectory\(OQ3\)|OQ3ToQC' mlir + +Build documentation and run repository policy checks: + + MLIR_DIR=/path/to/mlir/lib/cmake/mlir uvx nox --non-interactive -s docs + uvx nox -s lint + git diff --check origin/main + git status --short --branch + +For coverage, use the coverage preset, delete only ignored stale coverage +counters, and run the affected binaries sequentially because concurrent runs can +corrupt shared counters. Report line and branch coverage for +`mlir/lib/Target/OpenQASM` and the private direct emitter. Keep generated output +under `build/coverage` and record the final command and summary here when run. + +## Validation and Acceptance + +The frontend is accepted when parsing and semantic analysis require no MLIR +context, included buffers retain accurate source locations, valid programs +produce a resolved typed program, and invalid source returns collected +diagnostics at the owning stage. + +Direct emission is accepted when supported programs return verified modules +containing QC and standard MLIR dialects only. Primitive aliases, custom gates, +broadcasting, controls, inverse and negative controls, expressions, dynamic +indices, measurements, reset, barrier, and structured control flow must retain +their tested behavior. A valid `pow @` program must parse and analyze, then fail +QC translation with a precise source-located message and no fallback IR. + +The complete compiler is accepted when every broad corpus fixture passes the +explicit public API chain through optimized QCO, reconstructed QC, Adaptive QIR, +LLVM IR, and bitcode, and also passes `runDefaultPipeline`. Every source in the +straight-line subset must additionally produce Base QIR. The smaller `jeff` +corpus must round-trip, while tracked incompatible programs must succeed through +optimized QCO and fail specifically at `intoJeff()`. + +Every retained downstream conversion change is accepted only with a focused +parser-independent native-IR regression that fails without the change and passes +with it. The related full-chain OpenQASM fixture must also pass. No conversion +test may link the OpenQASM frontend solely to construct its input. + +The architecture is accepted when there is no OQ3 dialect, OQ3 operation, +OQ3-to-QC pass, generated OQ3 target, tool registration, or dialect test. The +`oq3::frontend` namespace may remain. There is one gate catalog, the frontend +library is named `MLIROpenQASMFrontend`, the public translation adapter is +small, and direct emission is private to QC translation. + +Final acceptance requires all affected and full MLIR unit tests, the legacy +parser regression, documentation with warnings treated as errors, measured +coverage of the substantive newly added frontend/emitter lines, +`uvx nox -s lint`, and `git diff --check origin/main` to pass. Coverage tests +must exercise useful behavior rather than pad a numeric threshold. The final +diff must contain no build output, generated documentation, temporary +workaround, or unjustified production conversion change. + +## Idempotence and Recovery + +Configuration, compilation, unit tests, documentation, lint, and diff checks are +repeatable and write only to ignored build directories. If CMake retains deleted +OQ3 targets, remove the ignored `build` and `docs/_build` directories and +configure again; do not add source-tree cleanup workarounds. + +Make the architecture transition in coherent local commits when useful, but do +not push. Before removing an old source, ensure its required direct-emission +behavior has moved into the private emitter and its tests pass. If a downstream +fixture fails, preserve the failing source, reduce it to native IR, and repair +the owning conversion instead of introducing a parser-side special case. + +Never discard unrelated user changes or edit another task worktree. This plan +does not authorize pushing, changing pull request state, resolving review +threads, or publishing comments. Any later public action requires explicit human +authorization and the disclosure required by `docs/ai_usage.md`. + +## Artifacts and Notes + +The completed groundwork before this revision comprised an LLVM-native staged +frontend, 27 imported behavior fixtures, source control flow and carried state, +and clean focused validation. It also comprised an OQ3 dialect and OQ3-to-QC +pass that this plan now deliberately removes. Earlier OQ3-specific test counts +are historical evidence, not revised acceptance evidence. + +The target-boundary proof after implementation must read: + + analyzeOpenQASM(pow-source) succeeds. + translateQASM3ToQC(pow-source) fails at the pow modifier location. + No OQ3 module is constructed. + +The standard-chain proof must record a representative structured fixture +reaching: + + OpenQASM -> QC -> QCO -> optimized QCO -> QC -> Adaptive QIR + -> LLVM IR and bitcode + +The final corpus contains seventeen standard programs, six `jeff` round-trip +programs, four `jeff`-incompatible programs, and four Base programs. One native +`JeffToQCO` regression proves that a serialized entry point with observable +results regains its marker without losing those results. A native QC-to-QIR +regression proves that `cf.assert` lowers through LLVM. The latest focused +validation results are: + + OpenQASM frontend and target: 96 tests passed. + OpenQASM compiler corpus: 48 tests passed. + Native QC-to-QIR assertion regression: passed. + QC translation: 241 tests passed. + QC-to-QCO: 124 tests passed. + QCO-to-QC: 121 tests passed. + `jeff` round trip: 113 tests passed. + Compiler pipeline: 164 tests passed, including 48 corpus cases. + QC-to-QIR Adaptive: 126 tests passed. + QC-to-QIR Base: 107 tests passed. + Legacy OpenQASM parser: 97 tests passed. + Warning-as-error documentation: passed. + Repository lint and diff checks: passed. + Frontend and direct-emitter line coverage: 89.9 percent (4117/4579). + +No public GitHub action is authorized by this plan. + +## Interfaces and Dependencies + +The source frontend continues to expose from +`mlir/include/mlir/Target/OpenQASM/Frontend.h`: + + ParseResult parseOpenQASM(llvm::SourceMgr&); + ParseResult parseOpenQASM(llvm::StringRef); + AnalysisResult analyzeOpenQASM(const ParsedProgram&, + const FrontendOptions& = {}); + AnalysisResult analyzeOpenQASM(llvm::SourceMgr&, + const FrontendOptions& = {}); + AnalysisResult analyzeOpenQASM(llvm::StringRef, + const FrontendOptions& = {}); + +`ParseResult` and `AnalysisResult` carry diagnostics as data. `ParsedProgram` +owns persistent syntax. `TypedProgram` owns resolved source semantics. These +interfaces remain in `oq3::frontend` unless a separate, evidence-backed rename +is approved. + +The public QC translation interface remains only the existing overloads in +`mlir/include/mlir/Dialect/QC/Translation/TranslateQASM3ToQC.h`: + + OwningOpRef translateQASM3ToQC(llvm::SourceMgr&, + MLIRContext*); + OwningOpRef translateQASM3ToQC(llvm::StringRef, + MLIRContext*); + +The private emitter header beside the translation source declares an internal +typed-program-to-QC function; it is not installed as a public header and no +compiler caller uses it directly. `MLIRQCTranslation` links +`MLIROpenQASMFrontend`, QC and its builder, and the standard MLIR dialects used +by the emitter. `MLIROpenQASMFrontend` must not link QC or depend on the +translation library. + +The compiler acceptance interfaces are `QCProgram::fromQASMString`, +`QCProgram::intoQCO`, `QCOProgram::cleanup`, `QCOProgram::runPassPipeline`, +`QCOProgram::intoJeff`, `JeffProgram::cleanup`, `JeffProgram::toBytes`, +`JeffProgram::fromBytes`, `JeffProgram::intoQCO`, `QCOProgram::intoQC`, +`QCProgram::cleanup`, `QCProgram::intoQIR`, `QIRProgram::llvmIR`, +`QIRProgram::toBitcode`, and `runDefaultPipeline`. Tests must respect their +move-only ownership contracts by copying at explicit branch points. + +Revision note (2026-07-16): this plan replaces the completed OQ3-intermediate +architecture with direct QC emission. Review feedback moved the implementation +into private emitter files, renamed the frontend target, assigned custom-gate +target preflight to emission, made full-chain tests precede downstream changes, +required parser-independent conversion regressions, removed fixture capability +flags, and defined exact `jeff` and QIR acceptance paths. The final whole-branch +cleanup removed unused resolved-program state and replaced the arbitrary +90-percent coverage gate with behavior-driven coverage evidence. + +Revision note (2026-07-16): post-review work made QC-to-QCO-to-QC-to-QIR the +primary acceptance path and moved `jeff` compatibility to separate positive and +boundary-failure suites. It removed the constant-lattice preflight, implemented +runtime integer and inclusive-range semantics, relocated implementation headers, +fixed the remaining lint diagnostics, and added canonical `cf.assert` lowering +to QIR. diff --git a/.gitignore b/.gitignore index 097b972471..9a03636339 100644 --- a/.gitignore +++ b/.gitignore @@ -79,7 +79,7 @@ docs/api/ # PyBuilder .pybuilder/ -target/ +/target/ # Jupyter Notebook .ipynb_checkpoints diff --git a/CHANGELOG.md b/CHANGELOG.md index d1e47345b5..1664f52403 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,9 @@ releases may include breaking changes. ### Added +- ✨ Add an LLVM-native staged OpenQASM frontend with typed semantic analysis + and direct QC emission, including lexical scope, assignment, inclusive ranges, + and structured control flow ([#1910]) ([**@burgholzer**]) - ✨ Add Python bindings for the MQT Compiler Collection ([#1815]) ([**@burgholzer**], [**@denialhaag**]) - ✨ Add support for QDMI child devices to the driver and FoMaC libraries @@ -638,6 +641,7 @@ changelogs._ [#1914]: https://github.com/munich-quantum-toolkit/core/pull/1914 +[#1910]: https://github.com/munich-quantum-toolkit/core/pull/1910 [#1904]: https://github.com/munich-quantum-toolkit/core/pull/1904 [#1897]: https://github.com/munich-quantum-toolkit/core/pull/1897 [#1895]: https://github.com/munich-quantum-toolkit/core/pull/1895 diff --git a/docs/mlir/OpenQASM.md b/docs/mlir/OpenQASM.md new file mode 100644 index 0000000000..6494455337 --- /dev/null +++ b/docs/mlir/OpenQASM.md @@ -0,0 +1,57 @@ +# OpenQASM input + +The compiler reads OpenQASM 3 through a staged lexer, parser, and semantic +analyzer, then emits the QC dialect directly. OpenQASM is an input language, not +an intermediate dialect: a successful translation contains QC and standard MLIR +operations only. Translation fails before returning a module when the QC target +cannot represent an accepted source feature. + +## Parser and semantic support + +| Feature | Status | Restriction | Representative test | +| -------------------------------------------------------- | ------------------------------------- | -------------------------------------------------------------------------------------------------------- | ------------------------------------------------------ | +| OpenQASM 3.0 and 3.1 | Supported | Later 3.x versions are rejected | `PreservesExactAndOptionalVersionSemantics` | +| `stdgates.inc` and nested includes | Supported | Includes are expanded textually with bounded depth and source locations | `ExpandsNestedIncludesAtTheirSourceLocations` | +| Qubits, bits, and `bool`, `int`, `uint`, `float` scalars | Supported | Width-qualified integer and floating types are not yet supported | `RejectsUnsupportedIntegerDeclarations` | +| Lexical scope, assignment, and constants | Supported | Mutable global values cannot be captured by gate definitions | `TracksLexicalScopeAndEnclosingAssignments` | +| `if`, inclusive `for`, and `while` | Supported | Gate bodies contain gate calls and loops over gate calls only | `EmitsStructuredLoopsWithCarriedMutableState` | +| Expressions and scalar math functions | Supported | Operations are checked against the implemented scalar type rules | `AcceptsAllScalarOperatorsAndComparisonPredicates` | +| Dynamic qubit and bit indexing | Supported | Target restrictions and the combined emission budget are listed below | `DispatchesDynamicQubitGatesWithStructuredControlFlow` | +| Primitive, broadcast, and custom gates | Supported | Recursive definitions and mismatched broadcast widths are rejected | `BroadcastsRegistersAlongsideScalarQubits` | +| `inv`, `ctrl`, `negctrl`, and `pow` modifiers | Parsed and semantically checked | Target support differs below | `RejectsInvalidGateControlAndBroadcastShapes` | +| `input` declarations, subroutines, and `extern` | Recognized and rejected by the parser | `input`, `def`, `return`, and `extern` are reserved but are not in the implemented grammar | `DiagnosesUnsupportedReservedFeatureSyntax` | +| Calibration, timing, `duration`, and `stretch` | Recognized and rejected by the parser | `defcalgrammar`, `cal`, `defcal`, `delay`, `durationof`, `duration`, and `stretch` are not implemented | `DiagnosesUnsupportedReservedFeatureSyntax` | +| `array`, `complex`, `angle`, and aliases | Recognized and rejected by the parser | Aggregate, complex, angle, and `let` alias declarations have no typed representation yet | `RejectsUnsupportedReservedWordsAsIdentifiers` | +| `switch`, `break`, and `continue` | Recognized and rejected by the parser | These control-flow forms are reserved but are not in the implemented grammar | `RejectsUnsupportedReservedWordsAsIdentifiers` | +| Bitwise and shift operators | Parsed and rejected semantically | Explicitly sized `uint`, `bit`, or `angle` operands are required by the language and are not implemented | `RejectsInvalidProgramsAcrossSemanticFamilies` | + +## Translation and compiler support + +The standard compiler path is OpenQASM to QC, optimized QCO, reconstructed QC, +and QIR. This is the primary acceptance contract. Serialization through `jeff` +is optional: a smaller positive corpus exercises it, while known incompatible +programs are accepted by the standard path and fail explicitly when QCO is +converted to `jeff`. Base refers to direct production of the QIR Base Profile. + +| Feature | Parse | Semantics | QC | Standard Adaptive QIR | `jeff` | Base | Restriction or rejection reason | Representative test | +| ----------------------------------------------- | --------- | --------- | -------------------------------------------- | --------------------- | ------------------------------------- | --------------------------------- | ------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------- | +| Primitive and custom gates | Supported | Supported | Supported | Supported | Supported in the positive corpus | Supported | Custom gates are expanded during QC emission | `broadcast_custom_gate` | +| Gate arithmetic and math parameters | Supported | Supported | Supported | Supported | Supported in the positive corpus | Supported | Scalar `pow()` is distinct from the gate modifier | `math_parameters` | +| Broadcast gates | Supported | Supported | Supported | Supported | Supported in the positive corpus | Supported | Operands must have compatible widths | `broadcast_custom_gate` | +| `inv`, `ctrl`, and `negctrl` | Supported | Supported | Supported | Supported | Supported in the positive corpus | Not in the tested Base subset | Modifiers on custom gates that require structured control flow are rejected | `RejectsModifiersOnTransitivelyStructuredCustomGatesAtQCTarget` | +| `pow @` | Supported | Supported | Rejected | Rejected | Rejected before `jeff` | Rejected | The QC dialect has no power modifier yet; translation reports the source location | `RejectsPowerAtTheQCTargetBoundary` | +| `if` and nested `if`/`for` | Supported | Supported | Supported | Supported | Supported in the positive corpus | Adaptive only | The Base corpus is intentionally straight-line | `nested_static_control_flow` | +| Measurement-controlled `while` | Supported | Supported | Supported | Supported | Supported in the positive corpus | Adaptive only | Requires runtime classical control | `measurement_controlled_while` | +| Loop-carried mutable bit and floating state | Supported | Supported | Supported | Supported | Supported in the positive corpus | Adaptive only | Carried state remains SSA values through QC, QCO, and reconstructed QC | `mutable_loop_state`, `scalar_loop_state` | +| Checked signed and wrapping unsigned arithmetic | Supported | Supported | Supported with runtime assertions | Supported | Rejected at QCO-to-`jeff` when live | Not in the tested Base subset | Signed overflow and invalid division are asserted; unsigned arithmetic wraps at 64 bits | `checked_integer_state` | +| Constant inclusive ranges | Supported | Supported | Supported with constant `scf.for` bounds | Supported | Supported when the body is compatible | Adaptive only | Positive, negative, empty, singleton, non-divisible, and boundary ranges avoid runtime trip-count math | `UsesConstantBoundsForStaticInclusiveRanges` | +| Dynamic inclusive ranges | Supported | Supported | Supported with comparison-driven `scf.while` | Supported | Rejected at QCO-to-`jeff` | Adaptive only | A dynamic zero step is asserted; iteration uses no division-based trip count | `dynamic_range` | +| Dynamic indexing resolved by optimization | Supported | Supported | Supported with bounds assertions | Supported | Supported when assertions fold away | Not in the tested Base subset | Straight-line constants and equal-constant branch joins can be simplified before `jeff` | `resolved_dynamic_index`, `equal_constant_index_join` | +| General runtime and induction-variable indexing | Supported | Supported | Supported with bounds assertions | Supported | Rejected at QCO-to-`jeff` | Not in the tested Base subset | The standard QIR path lowers the assertions; `jeff` cannot currently represent them | `runtime_dynamic_index`, `induction_variable_index` | +| Measurement, reset, and barrier | Supported | Supported | Supported | Supported | Supported in the positive corpus | Measurement and barrier supported | Reset is Adaptive-only | `reset`, `barrier` | +| Projected QC emission budget | Supported | Supported | Rejected above 100000 primitive applications | Rejected | Rejected before `jeff` | Rejected | One overflow-safe projection composes custom-gate expansion with dynamic-dispatch multiplicity | `ComposesDispatchAndCustomGateExpansionBudgets` | + +The integration suites use public compiler APIs. The broad corpus must complete +the standard pipeline and the smaller `jeff` corpus must round-trip. The +incompatible corpus records failures at `intoJeff()` so those limitations do not +reduce the set of programs accepted by QC or QIR. diff --git a/docs/mlir/index.md b/docs/mlir/index.md index 2b3e3bdfb2..2a376d333c 100644 --- a/docs/mlir/index.md +++ b/docs/mlir/index.md @@ -18,6 +18,8 @@ We define multiple dialects, each with its dedicated purpose: - The {doc}`QTensor dialect ` adds support for one-dimensional tensors of qubits with linear typing and is used in the QCO dialect to represent collections of qubits such as registers. +- The {doc}`OpenQASM frontend ` translates supported OpenQASM 3 input + directly to QC. These dialects define various canonicalization and transformation passes that enable the compilation of quantum programs to native quantum hardware. For @@ -30,6 +32,7 @@ python_compiler_collection QC QCO QTensor +OpenQASM Conversions ``` diff --git a/mlir/.clang-tidy b/mlir/.clang-tidy index 3fb5c4cc0e..aeeb0ff52c 100644 --- a/mlir/.clang-tidy +++ b/mlir/.clang-tidy @@ -1,13 +1,63 @@ -InheritParentConfig: true -Checks: | - llvm-namespace-comment, - llvm-prefer-isa-or-dyn-cast-in-conditionals, - llvm-prefer-register-over-unsigned, - llvm-prefer-static-over-anonymous-namespace, - llvm-twine-local, - -bugprone-throwing-static-initialization, - -cppcoreguidelines-avoid-non-const-global-variables, - -cppcoreguidelines-pro-bounds-avoid-unchecked-container-access, - -misc-use-anonymous-namespace, - -modernize-type-traits, - -*-const-correctness, +InheritParentConfig: false + +Checks: > + -*, + bugprone-argument-comment, + bugprone-assert-side-effect, + bugprone-branch-clone, + bugprone-copy-constructor-init, + bugprone-dangling-handle, + bugprone-dynamic-static-initializers, + bugprone-macro-parentheses, + bugprone-macro-repeated-side-effects, + bugprone-misplaced-widening-cast, + bugprone-move-forwarding-reference, + bugprone-multiple-statement-macro, + bugprone-suspicious-semicolon, + bugprone-swapped-arguments, + bugprone-terminating-continue, + bugprone-unused-raii, + bugprone-unused-return-value, + misc-redundant-expression, + misc-static-assert, + misc-unused-using-decls, + modernize-use-bool-literals, + modernize-loop-convert, + modernize-make-unique, + modernize-raw-string-literal, + modernize-use-equals-default, + modernize-use-default-member-init, + modernize-use-emplace, + modernize-use-nullptr, + modernize-use-override, + modernize-use-using, + performance-for-range-copy, + performance-implicit-conversion-in-loop, + performance-inefficient-algorithm, + performance-inefficient-vector-operation, + performance-move-const-arg, + performance-no-automatic-move, + performance-trivially-destructible, + performance-unnecessary-copy-initialization, + performance-unnecessary-value-param, + readability-avoid-const-params-in-decls, + readability-const-return-type, + readability-container-size-empty, + readability-inconsistent-declaration-parameter-name, + readability-misleading-indentation, + readability-redundant-control-flow, + readability-redundant-smartptr-get, + readability-simplify-boolean-expr, + readability-simplify-subscript-expr, + readability-use-anyofallof + +CheckOptions: + - key: readability-identifier-naming.MemberCase + value: camelBack + - key: readability-identifier-naming.ParameterCase + value: camelBack + - key: readability-identifier-naming.VariableCase + value: camelBack + - key: modernize-use-using.IgnoreExternC + value: true +FormatStyle: file diff --git a/mlir/include/mlir/Target/OpenQASM/Detail/OpenQASMLexer.h b/mlir/include/mlir/Target/OpenQASM/Detail/OpenQASMLexer.h new file mode 100644 index 0000000000..8829270ae3 --- /dev/null +++ b/mlir/include/mlir/Target/OpenQASM/Detail/OpenQASMLexer.h @@ -0,0 +1,168 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#pragma once + +#include +#include +#include + +#include + +namespace mlir::oq3::frontend::detail { + +/// The kind of a lexical token. +enum class TokenKind : uint8_t { + Eof, + Error, + UnterminatedComment, + UnsupportedKeyword, + + // Keywords + OpenQASM, + Include, + Const, + Qubit, + Qreg, + Bit, + CReg, + Gate, + Opaque, + Output, + Barrier, + Reset, + Measure, + If, + Else, + For, + While, + In, + Gphase, + Inv, + Pow, + Ctrl, + NegCtrl, + + // Types + Int, + Uint, + Bool, + Float, + Angle, + Duration, + True, + False, + + // Identifiers and literals + Identifier, + HardwareQubit, + StringLiteral, + IntegerLiteral, + FloatLiteral, + + // Punctuation + LParen, + RParen, + LBracket, + RBracket, + LBrace, + RBrace, + Comma, + Semicolon, + Colon, + Arrow, + At, + + // Operators + Equals, + Plus, + Minus, + Asterisk, + DoubleAsterisk, + Slash, + Percent, + Tilde, + Amp, + Pipe, + Caret, + ShiftLeft, + ShiftRight, + ExclamationPoint, + AmpAmp, // `&&` + PipePipe, // `||` + CompoundAssign, // `+=`, `-=`, ... + + // Comparisons + EqualsEquals, + NotEquals, + Less, + LessEquals, + Greater, + GreaterEquals, +}; + +/// A human-readable name for @p kind, used in diagnostics. +[[nodiscard]] StringRef describe(TokenKind kind); + +/** + * @brief A single lexical token. + * + * @details + * Spellings are zero-copy views into the source buffer. Literals are + * pre-parsed. + */ +struct Token { + TokenKind kind = TokenKind::Eof; + SMLoc loc; + StringRef identifier; ///< For `Identifier` tokens. + StringRef stringValue; ///< For `StringLiteral` tokens. + StringRef spelling; ///< Zero-copy source spelling. + uint64_t intValue = 0; + double floatValue = 0.0; +}; + +/** + * @brief A zero-copy lexer over an OpenQASM 3 source buffer. + * + * @details + * The lexer holds pointers into the buffer and produces tokens on demand + * without allocating. Token locations are `SMLoc`s into the buffer, so they can + * be resolved to line/column via the owning `llvm::SourceMgr`. + */ +class Lexer { +public: + explicit Lexer(StringRef buffer) : cur(buffer.begin()), end(buffer.end()) {} + + /// Produce the next token, or an `Eof`/`Error` token at the end/on failure. + [[nodiscard]] Token next(); + +private: + [[nodiscard]] bool atEnd() const { return cur == end; } + + /// The character after the cursor, or `'\0'` at the end of the buffer. + [[nodiscard]] char peek() const { return (cur + 1) != end ? cur[1] : '\0'; } + + /** + * @brief Skip whitespace and comments. + * @return The start of an unterminated block comment, or `nullptr` if the + * trivia is well-formed. + */ + [[nodiscard]] const char* skipTrivia(); + + [[nodiscard]] Token lexIdentifierOrKeyword(const char* start); + [[nodiscard]] Token lexNumber(const char* start); + [[nodiscard]] Token lexString(const char* start, char quote); + [[nodiscard]] Token lexHardwareQubit(const char* start); + + const char* cur; + const char* end; +}; + +} // namespace mlir::oq3::frontend::detail diff --git a/mlir/include/mlir/Target/OpenQASM/Detail/OpenQASMParser.h b/mlir/include/mlir/Target/OpenQASM/Detail/OpenQASMParser.h new file mode 100644 index 0000000000..89e3d799d3 --- /dev/null +++ b/mlir/include/mlir/Target/OpenQASM/Detail/OpenQASMParser.h @@ -0,0 +1,1546 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#pragma once + +#include "mlir/Target/OpenQASM/Detail/OpenQASMLexer.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace mlir::oq3::frontend::detail { + +/// An exact OpenQASM version, preserving the decimal minor component. +struct Version { + std::uint32_t major = 0; + std::uint32_t minor = 0; +}; + +enum class ScalarKind : std::uint8_t { Bool, Int, Uint, Float }; + +/** + * @defgroup ParseVocabulary Transient parse vocabulary + * @brief The vocabulary the parser hands to a sink. + * + * @details + * These types are cheap and trivially destructible. Expressions are allocated + * in a bump allocator; other values borrow parser-local storage for the + * duration of a sink call. `SyntaxBuilder` copies each completed construct into + * the persistent, owning syntax program. + */ + +/** + * @ingroup ParseVocabulary + * @brief A (sub-)expression. + * + * @details + * Bump-allocated; children are borrowed pointers. + */ +struct Expr { + enum class Kind : uint8_t { + Int, + Float, + Bool, + Identifier, + Index, + Measurement, + Neg, + Not, + BitNot, + Add, + Sub, + Mul, + Div, + Equal, + NotEqual, + Less, + LessEqual, + Greater, + GreaterEqual, + And, + Or, + BitAnd, + BitOr, + BitXor, + ShiftLeft, + ShiftRight, + // Built-in math functions + ArcCos, + ArcSin, + ArcTan, + Cos, + Exp, + Log, + Mod, + BuiltinMod, + BuiltinPow, + Pow, + Sin, + Sqrt, + Tan, + }; + + SMLoc loc; + Kind kind = Kind::Int; + uint64_t intValue = 0; + double floatValue = 0.0; + bool boolValue = false; + StringRef identifier; + std::optional hardwareQubit; + const Expr* lhs = nullptr; + const Expr* rhs = nullptr; +}; + +/// Get the kind of the built-in math function @p name. +[[nodiscard]] inline std::optional +getMathFunctionKind(StringRef name) { + return llvm::StringSwitch>(name) + .Case("arccos", Expr::Kind::ArcCos) + .Case("arcsin", Expr::Kind::ArcSin) + .Case("arctan", Expr::Kind::ArcTan) + .Case("cos", Expr::Kind::Cos) + .Case("exp", Expr::Kind::Exp) + .Case("log", Expr::Kind::Log) + .Case("ln", Expr::Kind::Log) + .Case("mod", Expr::Kind::BuiltinMod) + .Case("pow", Expr::Kind::BuiltinPow) + .Case("sin", Expr::Kind::Sin) + .Case("sqrt", Expr::Kind::Sqrt) + .Case("tan", Expr::Kind::Tan) + .Default(std::nullopt); +} + +/** + * @ingroup ParseVocabulary + * @brief A gate modifier: `inv @`, `pow(e) @`, `ctrl(e) @`, or `negctrl(e) @`. + */ +struct Modifier { + enum class Kind : uint8_t { Inv, Pow, Ctrl, NegCtrl }; + Kind kind = Kind::Inv; + const Expr* argument = nullptr; +}; + +/** + * @ingroup ParseVocabulary + * @brief A gate operand: a (possibly indexed) identifier, or a hardware qubit. + */ +struct Operand { + SMLoc loc; + StringRef identifier; + const Expr* index = nullptr; + std::optional hardwareQubit; +}; + +/// A (possibly indexed) classical reference (e.g., `c` or `c[0]`). +struct BitReference { + SMLoc loc; + StringRef identifier; + const Expr* index = nullptr; +}; + +/** + * @ingroup ParseVocabulary + * @brief A parsed gate call. + * + * @details + * Array members are borrowed for the duration of the sink call. + */ +struct GateCall { + SMLoc loc; + StringRef identifier; + ArrayRef modifiers; + ArrayRef parameters; + ArrayRef operands; +}; + +//===----------------------------------------------------------------------===// +// Sink concept +//===----------------------------------------------------------------------===// + +/** + * @brief The interface a `Parser` drives to materialize parsed constructs. + * + * @details + * A sink consumes the events produced by `Parser`. The production sink copies + * them into target-independent persistent syntax; the parser is templated so + * dispatch remains static. Diagnostics are routed through `error`. + * + * Control flow uses continuations so the persistent syntax builder can select + * the destination body while the parser recursively consumes a source block. + */ +template +concept QASMSink = + requires(S s, SMLoc loc, StringRef str, const Expr& expr, + const Operand& operand, const BitReference& reference, + const GateCall& call, ArrayRef operands, + ArrayRef names, function_ref cont, + Version version, bool flag) { + s.error(loc, str); + s.version(loc, version); + s.include(loc, str); + s.scalarDecl(loc, ScalarKind::Int, str, &expr, flag); + s.assignment(loc, reference, expr); + s.qubitRegister(loc, str, &expr); + s.classicalRegister(loc, str, &expr, &expr, flag); + s.measure(loc, &reference, operand); + s.reset(loc, operand); + s.barrier(loc, operands); + s.gateCall(call); + s.gateDefinition(loc, str, names, names, cont); + s.ifStmt(loc, expr, cont, cont); + s.forStmt(loc, str, flag, expr, expr, expr, cont); + s.whileStmt(loc, expr, cont); + }; + +//===----------------------------------------------------------------------===// +// Parser +//===----------------------------------------------------------------------===// + +/** + * @brief A single-pass recursive-descent parser for OpenQASM 3. + * + * @details + * The parser is target-independent. Its builder materializes a persistent + * syntax program; expressions and temporary gate-definition vocabulary are + * bump-allocated only for the duration of parsing. + */ +template + requires QASMSink +class Parser { +public: + Parser(Lexer& lexer, Sink& sink, llvm::BumpPtrAllocator& allocator) + : lexer(lexer), sink(sink), allocator(allocator) { + currentToken = lexer.next(); + nextToken = lexer.next(); + } + + [[nodiscard]] LogicalResult parseProgram() { + bool hadError = false; + while (!atEnd()) { + if (failed(parseStatement())) { + hadError = true; + synchronizeAfterError(); + } + } + return success(!hadError); + } + +private: + //===--- Token scaffolding --------------------------------------------===// + + void advance() { + currentToken = nextToken; + nextToken = lexer.next(); + } + + void synchronizeAfterError() { + while (!atEnd() && current().kind != TokenKind::Semicolon && + current().kind != TokenKind::RBrace) { + advance(); + } + if (!atEnd()) { + advance(); + } + } + + [[nodiscard]] const Token& current() const { return currentToken; } + [[nodiscard]] const Token& peek() const { return nextToken; } + [[nodiscard]] bool atEnd() const { + return currentToken.kind == TokenKind::Eof; + } + + [[nodiscard]] LogicalResult expect(const TokenKind kind) { + if (current().kind != kind) { + if (current().kind == TokenKind::UnsupportedKeyword) { + return unsupportedKeyword(); + } + return sink.error(current().loc, Twine("expected ") + describe(kind) + + ", got " + describe(current().kind)); + } + advance(); + return success(); + } + + [[nodiscard]] LogicalResult unsupportedKeyword() { + return sink.error(current().loc, Twine("reserved keyword '") + + current().spelling + + "' is not supported"); + } + + [[nodiscard]] LogicalResult expectedIdentifier(const StringRef message) { + if (current().kind == TokenKind::UnsupportedKeyword) { + return unsupportedKeyword(); + } + return sink.error(current().loc, message); + } + + //===--- Allocation helpers -------------------------------------------===// + + [[nodiscard]] Expr* makeExpr() { + return new (allocator.Allocate()) Expr(); + } + + //===--- Program and statements ---------------------------------------===// + + [[nodiscard]] LogicalResult parseStatement() { + switch (current().kind) { + case TokenKind::OpenQASM: + return parseVersion(); + case TokenKind::Include: + return parseInclude(); + case TokenKind::Const: + case TokenKind::Bool: + case TokenKind::Int: + case TokenKind::Uint: + case TokenKind::Float: + return parseScalarDeclaration(); + case TokenKind::Angle: + case TokenKind::Duration: + return sink.error(current().loc, + "'angle' and 'duration' declarations are not supported " + "yet"); + case TokenKind::Qubit: + return parseQuantumDecl(); + case TokenKind::Qreg: + return parseQregDecl(); + case TokenKind::Bit: + return parseClassicalDecl(/*isOutput=*/false); + case TokenKind::CReg: + return parseCregDecl(); + case TokenKind::Output: + return parseOutputDecl(); + case TokenKind::Gate: + return parseGateStatement(); + case TokenKind::Opaque: + return sink.error(current().loc, + "opaque gate declarations are not supported"); + case TokenKind::Barrier: + return parseBarrier(); + case TokenKind::Reset: + return parseReset(); + case TokenKind::Measure: + return parseMeasure(); + case TokenKind::If: + return parseIf(); + case TokenKind::For: + return parseFor(); + case TokenKind::While: + return parseWhile(); + case TokenKind::Inv: + case TokenKind::Pow: + case TokenKind::Ctrl: + case TokenKind::NegCtrl: + case TokenKind::Gphase: + return parseGateCallStatement(); + case TokenKind::Identifier: + switch (peek().kind) { + case TokenKind::LBracket: + case TokenKind::Equals: + case TokenKind::CompoundAssign: + return parseAssignment(); + default: + return parseGateCallStatement(); + } + case TokenKind::UnterminatedComment: + return sink.error(current().loc, "unterminated block comment"); + case TokenKind::UnsupportedKeyword: + return unsupportedKeyword(); + default: + return sink.error(current().loc, "unexpected token"); + } + } + + /** + * @brief Parse a `{ ... }` block or a single statement. + * + * @details + * The block is parsed in a new scope. + */ + [[nodiscard]] LogicalResult parseBlock() { + ++blockDepth; + const auto result = parseBlockInScope(); + --blockDepth; + return result; + } + + /** + * @brief Parse a `{ ... }` block or a single statement. + * + * @details + * The block is parsed into the current scope. + */ + [[nodiscard]] LogicalResult parseBlockInScope() { + if (current().kind == TokenKind::LBrace) { + advance(); + while (!atEnd() && current().kind != TokenKind::RBrace) { + if (failed(parseStatement())) { + return failure(); + } + } + return expect(TokenKind::RBrace); + } + return parseStatement(); + } + + //===--- Helpers ------------------------------------------------------===// + + [[nodiscard]] FailureOr parseDesignator() { + if (failed(expect(TokenKind::LBracket))) { + return failure(); + } + auto expr = parseExpression(); + if (failed(expr)) { + return failure(); + } + if (failed(expect(TokenKind::RBracket))) { + return failure(); + } + return *expr; + } + + //===--- Version ------------------------------------------------------===// + + [[nodiscard]] LogicalResult parseVersion() { + const auto loc = current().loc; + advance(); // OPENQASM + if (current().kind != TokenKind::FloatLiteral && + current().kind != TokenKind::IntegerLiteral) { + return sink.error(current().loc, + "version must be a float or integer literal"); + } + auto majorText = current().spelling; + StringRef minorText; + bool hasMinor = false; + if (const auto separator = majorText.find('.'); + separator != StringRef::npos) { + hasMinor = true; + minorText = majorText.drop_front(separator + 1); + majorText = majorText.take_front(separator); + } + const auto decimalDigits = [](const StringRef text) { + return !text.empty() && llvm::all_of(text, [](const char value) { + return value >= '0' && value <= '9'; + }); + }; + std::uint64_t major = 0; + std::uint64_t minor = 0; + if (!decimalDigits(majorText) || (hasMinor && !decimalDigits(minorText)) || + majorText.getAsInteger(10, major) || + (!minorText.empty() && minorText.getAsInteger(10, minor)) || + major > std::numeric_limits::max() || + minor > std::numeric_limits::max()) { + return sink.error(current().loc, "invalid OpenQASM version string"); + } + const Version version{.major = static_cast(major), + .minor = static_cast(minor)}; + advance(); + if (failed(expect(TokenKind::Semicolon))) { + return failure(); + } + return sink.version(loc, version); + } + + //===--- Include ------------------------------------------------------===// + + [[nodiscard]] LogicalResult parseInclude() { + const auto loc = current().loc; + if (blockDepth != 0) { + return sink.error(loc, "include directives are only allowed globally"); + } + advance(); // include + if (current().kind != TokenKind::StringLiteral) { + return sink.error(current().loc, "expected a string literal"); + } + const auto filename = current().stringValue; + advance(); + if (failed(expect(TokenKind::Semicolon))) { + return failure(); + } + return sink.include(loc, filename); + } + + //===--- Declarations -------------------------------------------------===// + + /// Parse `[const] (int|uint|float|bool) = ;`. + [[nodiscard]] LogicalResult parseScalarDeclaration() { + const auto loc = current().loc; + + bool isConst = false; + if (current().kind == TokenKind::Const) { + isConst = true; + advance(); // const + } + + const auto kind = current().kind; + switch (kind) { + case TokenKind::Bool: + case TokenKind::Int: + case TokenKind::Uint: + case TokenKind::Float: + break; + case TokenKind::Angle: + case TokenKind::Duration: + return sink.error(current().loc, + "'angle' and 'duration' declarations are not supported " + "yet"); + case TokenKind::UnsupportedKeyword: + return unsupportedKeyword(); + default: + return sink.error(current().loc, "expected a scalar type"); + } + advance(); // type + + if ((kind == TokenKind::Int || kind == TokenKind::Uint) && + current().kind == TokenKind::LBracket) { + return sink.error(current().loc, + "Integer declarations currently require the default " + "64-bit width"); + } + if (current().kind != TokenKind::Identifier) { + return expectedIdentifier("expected identifier"); + } + const auto id = current().identifier; + advance(); + + const bool hasInitializer = current().kind == TokenKind::Equals; + if (hasInitializer) { + advance(); + } + if (isConst && !hasInitializer) { + return sink.error(loc, Twine("'const' declaration of '") + id + + "' requires an initializer"); + } + + const Expr* initializer = nullptr; + if (hasInitializer) { + auto value = parseExpression(); + if (failed(value)) { + return failure(); + } + initializer = *value; + } + if (failed(expect(TokenKind::Semicolon))) { + return failure(); + } + const auto scalarKind = kind == TokenKind::Bool ? ScalarKind::Bool + : kind == TokenKind::Int ? ScalarKind::Int + : kind == TokenKind::Uint ? ScalarKind::Uint + : ScalarKind::Float; + return sink.scalarDecl(loc, scalarKind, id, initializer, isConst); + } + + /// Parse `qubit[] ;`. + [[nodiscard]] LogicalResult parseQuantumDecl() { + const auto loc = current().loc; + advance(); // qubit + const Expr* size = nullptr; + if (current().kind == TokenKind::LBracket) { + auto designator = parseDesignator(); + if (failed(designator)) { + return failure(); + } + size = *designator; + } + if (current().kind != TokenKind::Identifier) { + return expectedIdentifier("expected identifier"); + } + const auto id = current().identifier; + advance(); + if (failed(expect(TokenKind::Semicolon))) { + return failure(); + } + return sink.qubitRegister(loc, id, size); + } + + /// Parse `qreg [];`. + [[nodiscard]] LogicalResult parseQregDecl() { + const auto loc = current().loc; + advance(); // qreg + if (current().kind != TokenKind::Identifier) { + return expectedIdentifier("expected identifier"); + } + const auto id = current().identifier; + advance(); + const Expr* size = nullptr; + if (current().kind == TokenKind::LBracket) { + auto designator = parseDesignator(); + if (failed(designator)) { + return failure(); + } + size = *designator; + } + if (failed(expect(TokenKind::Semicolon))) { + return failure(); + } + return sink.qubitRegister(loc, id, size); + } + + /// Parse `output bit[] (= );`. + [[nodiscard]] LogicalResult parseOutputDecl() { + const auto loc = current().loc; + advance(); // output + if (current().kind == TokenKind::UnsupportedKeyword) { + return unsupportedKeyword(); + } + if (current().kind != TokenKind::Bit) { + return sink.error(current().loc, + "only 'bit' registers can be declared as outputs"); + } + return parseClassicalDecl(/*isOutput=*/true); + } + + /// Parse `bit[] (= );`. + [[nodiscard]] LogicalResult parseClassicalDecl(bool isOutput) { + const auto loc = current().loc; + advance(); // bit + const Expr* size = nullptr; + if (current().kind == TokenKind::LBracket) { + auto designator = parseDesignator(); + if (failed(designator)) { + return failure(); + } + size = *designator; + } + if (current().kind != TokenKind::Identifier) { + return expectedIdentifier("expected identifier"); + } + const auto id = current().identifier; + advance(); + + const Expr* initializer = nullptr; + std::optional measureSource; + if (current().kind == TokenKind::Equals) { + advance(); + if (current().kind == TokenKind::Measure) { + advance(); + auto operand = parseGateOperand(); + if (failed(operand)) { + return failure(); + } + measureSource = *operand; + } else { + auto expression = parseExpression(); + if (failed(expression)) { + return failure(); + } + initializer = *expression; + } + } + if (failed(expect(TokenKind::Semicolon))) { + return failure(); + } + + if (failed(sink.classicalRegister(loc, id, size, initializer, isOutput))) { + return failure(); + } + if (measureSource) { + const BitReference target{.loc = loc, .identifier = id, .index = nullptr}; + return sink.measure(loc, &target, *measureSource); + } + return success(); + } + + /// Parse `creg [];`. + [[nodiscard]] LogicalResult parseCregDecl() { + const auto loc = current().loc; + advance(); // creg + if (current().kind != TokenKind::Identifier) { + return expectedIdentifier("expected identifier"); + } + const auto id = current().identifier; + advance(); + const Expr* size = nullptr; + if (current().kind == TokenKind::LBracket) { + auto designator = parseDesignator(); + if (failed(designator)) { + return failure(); + } + size = *designator; + } + if (failed(expect(TokenKind::Semicolon))) { + return failure(); + } + return sink.classicalRegister(loc, id, size, /*initializer=*/nullptr, + /*output=*/false); + } + + //===--- Assignment ---------------------------------------------------===// + + /// Parse an assignment. + [[nodiscard]] LogicalResult parseAssignment() { + const auto loc = current().loc; + auto target = parseBitReference(); + if (failed(target)) { + return failure(); + } + + const bool compound = current().kind == TokenKind::CompoundAssign; + const auto compoundLocation = current().loc; + const auto compoundSpelling = current().spelling; + if (compound) { + if (target->index != nullptr) { + return sink.error(current().loc, + "indexed compound assignments are not supported"); + } + advance(); + } else if (failed(expect(TokenKind::Equals))) { + return failure(); + } + + if (!compound && current().kind == TokenKind::Measure) { + advance(); + auto operand = parseGateOperand(); + if (failed(operand)) { + return failure(); + } + if (failed(expect(TokenKind::Semicolon))) { + return failure(); + } + return sink.measure(loc, &*target, *operand); + } + + auto value = parseExpression(); + if (failed(value)) { + return failure(); + } + const Expr* assignedValue = *value; + if (compound) { + const auto kind = llvm::StringSwitch>( + compoundSpelling.drop_back()) + .Case("+", Expr::Kind::Add) + .Case("-", Expr::Kind::Sub) + .Case("*", Expr::Kind::Mul) + .Case("/", Expr::Kind::Div) + .Case("%", Expr::Kind::Mod) + .Case("**", Expr::Kind::Pow) + .Case("&", Expr::Kind::BitAnd) + .Case("|", Expr::Kind::BitOr) + .Case("^", Expr::Kind::BitXor) + .Case("<<", Expr::Kind::ShiftLeft) + .Case(">>", Expr::Kind::ShiftRight) + .Default(std::nullopt); + if (!kind) { + return sink.error(compoundLocation, + "unsupported compound assignment operator"); + } + auto* previous = makeExpr(); + previous->loc = loc; + previous->kind = Expr::Kind::Identifier; + previous->identifier = target->identifier; + assignedValue = + makeBinary(*kind, previous, assignedValue, compoundLocation); + } + if (failed(expect(TokenKind::Semicolon))) { + return failure(); + } + return sink.assignment(loc, *target, *assignedValue); + } + + //===--- Measure ------------------------------------------------------===// + + [[nodiscard]] LogicalResult parseMeasure() { + const auto loc = current().loc; + advance(); // measure + auto operand = parseGateOperand(); + if (failed(operand)) { + return failure(); + } + if (current().kind == TokenKind::Semicolon) { + advance(); + return sink.measure(loc, /*target=*/nullptr, *operand); + } + if (failed(expect(TokenKind::Arrow))) { + return failure(); + } + auto target = parseBitReference(); + if (failed(target)) { + return failure(); + } + if (failed(expect(TokenKind::Semicolon))) { + return failure(); + } + return sink.measure(loc, &*target, *operand); + } + + //===--- Reset --------------------------------------------------------===// + + [[nodiscard]] LogicalResult parseReset() { + const auto loc = current().loc; + advance(); // reset + auto operand = parseGateOperand(); + if (failed(operand)) { + return failure(); + } + if (failed(expect(TokenKind::Semicolon))) { + return failure(); + } + return sink.reset(loc, *operand); + } + + //===--- Barrier ------------------------------------------------------===// + + [[nodiscard]] LogicalResult parseBarrier() { + const auto loc = current().loc; + advance(); // barrier + SmallVector operands; + while (current().kind != TokenKind::Semicolon) { + auto operand = parseGateOperand(); + if (failed(operand)) { + return failure(); + } + operands.push_back(*operand); + if (current().kind != TokenKind::Semicolon && + failed(expect(TokenKind::Comma))) { + return failure(); + } + } + advance(); // ; + return sink.barrier(loc, operands); + } + + //===--- Gate definitions and calls -----------------------------------===// + + [[nodiscard]] LogicalResult parseGateStatement() { + const auto loc = current().loc; + advance(); // gate + if (current().kind != TokenKind::Identifier) { + return expectedIdentifier("expected gate name"); + } + const auto id = current().identifier; + advance(); + + // Parse parameters + SmallVector parameters; + if (current().kind == TokenKind::LParen) { + advance(); + if (current().kind != TokenKind::RParen && + failed(parseIdentifierList(parameters))) { + return failure(); + } + if (failed(expect(TokenKind::RParen))) { + return failure(); + } + } + + // Parse target qubits + SmallVector targets; + if (failed(parseIdentifierList(targets))) { + return failure(); + } + if (current().kind != TokenKind::LBrace) { + return sink.error(current().loc, + "expected '{' to begin gate definition body"); + } + + return sink.gateDefinition(loc, id, parameters, targets, + [this] { return parseBlock(); }); + } + + [[nodiscard]] LogicalResult parseGateCallStatement() { + // The scratch buffers must outlive the sink call, so they live here rather + // than inside `parseGateCall`. + SmallVector modifiers; + SmallVector parameters; + SmallVector operands; + auto call = parseGateCall(modifiers, parameters, operands); + if (failed(call)) { + return failure(); + } + return sink.gateCall(*call); + } + + [[nodiscard]] FailureOr + parseGateCall(SmallVectorImpl& modifiers, + SmallVectorImpl& parameters, + SmallVectorImpl& operands) { + GateCall call; + call.loc = current().loc; + + while (current().kind == TokenKind::Inv || + current().kind == TokenKind::Pow || + current().kind == TokenKind::Ctrl || + current().kind == TokenKind::NegCtrl) { + auto modifier = parseModifier(); + if (failed(modifier)) { + return failure(); + } + modifiers.push_back(*modifier); + if (failed(expect(TokenKind::At))) { + return failure(); + } + } + + switch (current().kind) { + case TokenKind::Gphase: { + call.identifier = "gphase"; + advance(); + break; + } + case TokenKind::Identifier: { + call.identifier = current().identifier; + advance(); + break; + } + default: + if (current().kind == TokenKind::UnsupportedKeyword) { + return unsupportedKeyword(); + } + return sink.error(current().loc, "expected gate name"); + } + + // Parse parameters + if (current().kind == TokenKind::LParen) { + advance(); + while (current().kind != TokenKind::RParen) { + auto parameter = parseExpression(); + if (failed(parameter)) { + return failure(); + } + parameters.push_back(*parameter); + if (current().kind != TokenKind::RParen && + failed(expect(TokenKind::Comma))) { + return failure(); + } + } + advance(); // ) + } + + if (current().kind == TokenKind::LBracket) { + return sink.error(current().loc, + "gate-call designators are outside the supported " + "OpenQASM subset"); + } + + // Parse target qubits + while (current().kind != TokenKind::Semicolon) { + auto operand = parseGateOperand(); + if (failed(operand)) { + return failure(); + } + operands.push_back(*operand); + if (current().kind != TokenKind::Semicolon && + failed(expect(TokenKind::Comma))) { + return failure(); + } + } + advance(); // ; + + call.modifiers = modifiers; + call.parameters = parameters; + call.operands = operands; + return call; + } + + [[nodiscard]] FailureOr parseModifier() { + Modifier modifier; + switch (current().kind) { + case TokenKind::Inv: + modifier.kind = Modifier::Kind::Inv; + advance(); + return modifier; + case TokenKind::Pow: + modifier.kind = Modifier::Kind::Pow; + advance(); + if (failed(expect(TokenKind::LParen))) { + return failure(); + } + { + auto argument = parseExpression(); + if (failed(argument)) { + return failure(); + } + modifier.argument = *argument; + } + if (failed(expect(TokenKind::RParen))) { + return failure(); + } + return modifier; + case TokenKind::Ctrl: + case TokenKind::NegCtrl: + modifier.kind = current().kind == TokenKind::Ctrl + ? Modifier::Kind::Ctrl + : Modifier::Kind::NegCtrl; + advance(); + if (current().kind == TokenKind::LParen) { + advance(); + auto argument = parseExpression(); + if (failed(argument)) { + return failure(); + } + modifier.argument = *argument; + if (failed(expect(TokenKind::RParen))) { + return failure(); + } + } + return modifier; + default: + return sink.error(current().loc, "expected a gate modifier"); + } + } + + [[nodiscard]] FailureOr parseGateOperand() { + Operand operand; + operand.loc = current().loc; + if (current().kind == TokenKind::HardwareQubit) { + operand.hardwareQubit = static_cast(current().intValue); + advance(); + return operand; + } + if (current().kind != TokenKind::Identifier) { + return expectedIdentifier("expected a gate operand"); + } + operand.identifier = current().identifier; + advance(); + const Expr* index = nullptr; + if (current().kind == TokenKind::LBracket) { + auto designator = parseDesignator(); + if (failed(designator)) { + return failure(); + } + index = *designator; + } + operand.index = index; + return operand; + } + + [[nodiscard]] FailureOr parseBitReference() { + BitReference reference; + reference.loc = current().loc; + if (current().kind != TokenKind::Identifier) { + return expectedIdentifier("expected an identifier"); + } + reference.identifier = current().identifier; + advance(); + const Expr* index = nullptr; + if (current().kind == TokenKind::LBracket) { + auto designator = parseDesignator(); + if (failed(designator)) { + return failure(); + } + index = *designator; + } + reference.index = index; + return reference; + } + + [[nodiscard]] LogicalResult + parseIdentifierList(SmallVectorImpl& identifiers) { + if (current().kind != TokenKind::Identifier) { + return expectedIdentifier("expected an identifier"); + } + identifiers.push_back(current().identifier); + advance(); + while (current().kind == TokenKind::Comma) { + advance(); + if (current().kind != TokenKind::Identifier) { + break; + } + identifiers.push_back(current().identifier); + advance(); + } + return success(); + } + + //===--- Control flow -------------------------------------------------===// + + [[nodiscard]] LogicalResult parseIf() { + const auto loc = current().loc; + advance(); // if + if (failed(expect(TokenKind::LParen))) { + return failure(); + } + auto conditionOrFailure = parseExpression(); + if (failed(conditionOrFailure)) { + return failure(); + } + if (failed(expect(TokenKind::RParen))) { + return failure(); + } + return sink.ifStmt( + loc, **conditionOrFailure, [this] { return parseBlock(); }, + [this] { return parseElse(); }); + } + + [[nodiscard]] LogicalResult parseElse() { + if (current().kind != TokenKind::Else) { + return success(); + } + advance(); // else + return parseBlock(); + } + + [[nodiscard]] LogicalResult parseFor() { + const auto loc = current().loc; + advance(); // for + + if (current().kind == TokenKind::UnsupportedKeyword) { + return unsupportedKeyword(); + } + if (current().kind != TokenKind::Int && current().kind != TokenKind::Uint) { + return sink.error(current().loc, "expected 'int' or 'uint' after 'for'"); + } + const bool isUnsigned = current().kind == TokenKind::Uint; + advance(); // type + + if (current().kind != TokenKind::Identifier) { + return expectedIdentifier("expected loop variable"); + } + const auto iv = current(); + advance(); // identifier + + if (failed(expect(TokenKind::In)) || failed(expect(TokenKind::LBracket))) { + return failure(); + } + auto start = parseExpression(); + if (failed(start)) { + return failure(); + } + if (failed(expect(TokenKind::Colon))) { + return failure(); + } + auto second = parseExpression(); + if (failed(second)) { + return failure(); + } + + const Expr* step = nullptr; + const Expr* stop = nullptr; + if (current().kind == TokenKind::Colon) { + advance(); + auto third = parseExpression(); + if (failed(third)) { + return failure(); + } + step = *second; + stop = *third; + } else { + auto* one = makeExpr(); + one->loc = loc; + one->kind = Expr::Kind::Int; + one->intValue = 1; + step = one; + stop = *second; + } + if (failed(expect(TokenKind::RBracket))) { + return failure(); + } + + return sink.forStmt(loc, iv.identifier, isUnsigned, **start, *step, *stop, + [this] { return parseBlock(); }); + } + + [[nodiscard]] LogicalResult parseWhile() { + const auto loc = current().loc; + advance(); // while + if (failed(expect(TokenKind::LParen))) { + return failure(); + } + auto conditionOrFailure = parseExpression(); + if (failed(conditionOrFailure)) { + return failure(); + } + if (failed(expect(TokenKind::RParen))) { + return failure(); + } + return sink.whileStmt(loc, **conditionOrFailure, + [this] { return parseBlock(); }); + } + + //===--- Expressions --------------------------------------------------===// + + /// Parse an expression using OpenQASM's precedence hierarchy. + [[nodiscard]] FailureOr parseExpression() { + auto lhs = parseLogicalAnd(); + if (failed(lhs)) { + return failure(); + } + const Expr* result = *lhs; + while (current().kind == TokenKind::PipePipe) { + const auto loc = current().loc; + advance(); + auto rhs = parseLogicalAnd(); + if (failed(rhs)) { + return failure(); + } + result = makeBinary(Expr::Kind::Or, result, *rhs, loc); + } + return result; + } + + [[nodiscard]] FailureOr parseLogicalAnd() { + auto lhs = parseBitwiseOr(); + if (failed(lhs)) { + return failure(); + } + const Expr* result = *lhs; + while (current().kind == TokenKind::AmpAmp) { + const auto loc = current().loc; + advance(); + auto rhs = parseBitwiseOr(); + if (failed(rhs)) { + return failure(); + } + result = makeBinary(Expr::Kind::And, result, *rhs, loc); + } + return result; + } + + [[nodiscard]] FailureOr parseBitwiseOr() { + auto lhs = parseBitwiseXor(); + if (failed(lhs)) { + return failure(); + } + const Expr* result = *lhs; + while (current().kind == TokenKind::Pipe) { + const auto loc = current().loc; + advance(); + auto rhs = parseBitwiseXor(); + if (failed(rhs)) { + return failure(); + } + result = makeBinary(Expr::Kind::BitOr, result, *rhs, loc); + } + return result; + } + + [[nodiscard]] FailureOr parseBitwiseXor() { + auto lhs = parseBitwiseAnd(); + if (failed(lhs)) { + return failure(); + } + const Expr* result = *lhs; + while (current().kind == TokenKind::Caret) { + const auto loc = current().loc; + advance(); + auto rhs = parseBitwiseAnd(); + if (failed(rhs)) { + return failure(); + } + result = makeBinary(Expr::Kind::BitXor, result, *rhs, loc); + } + return result; + } + + [[nodiscard]] FailureOr parseBitwiseAnd() { + auto lhs = parseEquality(); + if (failed(lhs)) { + return failure(); + } + const Expr* result = *lhs; + while (current().kind == TokenKind::Amp) { + const auto loc = current().loc; + advance(); + auto rhs = parseEquality(); + if (failed(rhs)) { + return failure(); + } + result = makeBinary(Expr::Kind::BitAnd, result, *rhs, loc); + } + return result; + } + + [[nodiscard]] FailureOr parseEquality() { + auto lhs = parseRelational(); + if (failed(lhs)) { + return failure(); + } + const Expr* result = *lhs; + while (current().kind == TokenKind::EqualsEquals || + current().kind == TokenKind::NotEquals) { + const auto kind = current().kind == TokenKind::EqualsEquals + ? Expr::Kind::Equal + : Expr::Kind::NotEqual; + const auto loc = current().loc; + advance(); + auto rhs = parseRelational(); + if (failed(rhs)) { + return failure(); + } + result = makeBinary(kind, result, *rhs, loc); + } + return result; + } + + [[nodiscard]] FailureOr parseRelational() { + auto lhs = parseShift(); + if (failed(lhs)) { + return failure(); + } + const auto kind = [&]() -> std::optional { + switch (current().kind) { + case TokenKind::Less: + return Expr::Kind::Less; + case TokenKind::LessEquals: + return Expr::Kind::LessEqual; + case TokenKind::Greater: + return Expr::Kind::Greater; + case TokenKind::GreaterEquals: + return Expr::Kind::GreaterEqual; + default: + return std::nullopt; + } + }(); + if (!kind) { + return lhs; + } + const auto loc = current().loc; + advance(); + auto rhs = parseShift(); + if (failed(rhs)) { + return failure(); + } + return makeBinary(*kind, *lhs, *rhs, loc); + } + + [[nodiscard]] FailureOr parseShift() { + auto lhs = parseAdditive(); + if (failed(lhs)) { + return failure(); + } + const Expr* result = *lhs; + while (current().kind == TokenKind::ShiftLeft || + current().kind == TokenKind::ShiftRight) { + const auto kind = current().kind == TokenKind::ShiftLeft + ? Expr::Kind::ShiftLeft + : Expr::Kind::ShiftRight; + const auto loc = current().loc; + advance(); + auto rhs = parseAdditive(); + if (failed(rhs)) { + return failure(); + } + result = makeBinary(kind, result, *rhs, loc); + } + return result; + } + + [[nodiscard]] FailureOr parseAdditive() { + auto lhs = parseTerm(); + if (failed(lhs)) { + return failure(); + } + const Expr* result = *lhs; + while (current().kind == TokenKind::Plus || + current().kind == TokenKind::Minus) { + const auto kind = + current().kind == TokenKind::Plus ? Expr::Kind::Add : Expr::Kind::Sub; + const auto loc = current().loc; + advance(); + auto rhs = parseTerm(); + if (failed(rhs)) { + return failure(); + } + result = makeBinary(kind, result, *rhs, loc); + } + return result; + } + + [[nodiscard]] FailureOr parseTerm() { + auto lhs = parseUnary(); + if (failed(lhs)) { + return failure(); + } + const Expr* result = *lhs; + while (current().kind == TokenKind::Asterisk || + current().kind == TokenKind::Slash || + current().kind == TokenKind::Percent) { + const auto kind = current().kind == TokenKind::Asterisk ? Expr::Kind::Mul + : current().kind == TokenKind::Slash ? Expr::Kind::Div + : Expr::Kind::Mod; + const auto loc = current().loc; + advance(); + auto rhs = parseUnary(); + if (failed(rhs)) { + return failure(); + } + result = makeBinary(kind, result, *rhs, loc); + } + return result; + } + + [[nodiscard]] FailureOr parseUnary() { + if (current().kind == TokenKind::Minus || + current().kind == TokenKind::ExclamationPoint || + current().kind == TokenKind::Tilde) { + const auto loc = current().loc; + const auto kind = current().kind == TokenKind::Minus ? Expr::Kind::Neg + : current().kind == TokenKind::Tilde + ? Expr::Kind::BitNot + : Expr::Kind::Not; + advance(); + auto operand = parseUnary(); + if (failed(operand)) { + return failure(); + } + auto* expr = makeExpr(); + expr->loc = loc; + expr->kind = kind; + expr->lhs = *operand; + return expr; + } + return parsePower(); + } + + [[nodiscard]] FailureOr parsePower() { + auto lhs = parsePrimary(); + if (failed(lhs) || current().kind != TokenKind::DoubleAsterisk) { + return lhs; + } + const auto loc = current().loc; + advance(); + auto rhs = parseUnary(); + if (failed(rhs)) { + return failure(); + } + return makeBinary(Expr::Kind::Pow, *lhs, *rhs, loc); + } + + [[nodiscard]] FailureOr parsePrimary() { + auto* expr = makeExpr(); + expr->loc = current().loc; + switch (current().kind) { + case TokenKind::True: + case TokenKind::False: + expr->kind = Expr::Kind::Bool; + expr->boolValue = current().kind == TokenKind::True; + advance(); + return expr; + case TokenKind::FloatLiteral: + expr->kind = Expr::Kind::Float; + expr->floatValue = current().floatValue; + advance(); + return expr; + case TokenKind::IntegerLiteral: + expr->kind = Expr::Kind::Int; + expr->intValue = current().intValue; + advance(); + return expr; + case TokenKind::Identifier: { + if (peek().kind == TokenKind::LParen) { + const auto kind = getMathFunctionKind(current().identifier); + if (!kind) { + return sink.error(current().loc, Twine("unknown function '") + + current().identifier + "'"); + } + return parseMathCall(*kind, expr); + } + expr->kind = Expr::Kind::Identifier; + expr->identifier = current().identifier; + advance(); + if (current().kind == TokenKind::LBracket) { + auto designator = parseDesignator(); + if (failed(designator)) { + return failure(); + } + expr->kind = Expr::Kind::Index; + expr->lhs = *designator; + } + return expr; + } + case TokenKind::Measure: { + advance(); + auto operand = parseGateOperand(); + if (failed(operand)) { + return failure(); + } + expr->kind = Expr::Kind::Measurement; + expr->identifier = operand->identifier; + expr->hardwareQubit = operand->hardwareQubit; + expr->lhs = operand->index; + return expr; + } + // `pow` is also a gate modifier, so it has a dedicated token. + case TokenKind::Pow: + return parseMathCall(Expr::Kind::BuiltinPow, expr); + case TokenKind::LParen: { + advance(); + auto inner = parseExpression(); + if (failed(inner)) { + return failure(); + } + if (failed(expect(TokenKind::RParen))) { + return failure(); + } + return *inner; + } + case TokenKind::UnsupportedKeyword: + return unsupportedKeyword(); + default: + return sink.error(current().loc, "expected expression"); + } + } + + /// Parse the argument list of a call to the built-in math function @p kind. + [[nodiscard]] FailureOr parseMathCall(Expr::Kind kind, + Expr* expr) { + expr->kind = kind; + advance(); // function name + + if (failed(expect(TokenKind::LParen))) { + return failure(); + } + + auto lhs = parseExpression(); + if (failed(lhs)) { + return failure(); + } + expr->lhs = *lhs; + + if (kind == Expr::Kind::BuiltinMod || kind == Expr::Kind::BuiltinPow) { + if (failed(expect(TokenKind::Comma))) { + return failure(); + } + auto rhs = parseExpression(); + if (failed(rhs)) { + return failure(); + } + expr->rhs = *rhs; + } + + if (failed(expect(TokenKind::RParen))) { + return failure(); + } + return expr; + } + + [[nodiscard]] Expr* makeBinary(const Expr::Kind kind, const Expr* lhs, + const Expr* rhs, const SMLoc loc) { + auto* expr = makeExpr(); + expr->loc = loc; + expr->kind = kind; + expr->lhs = lhs; + expr->rhs = rhs; + return expr; + } + + Lexer& lexer; + Sink& sink; + llvm::BumpPtrAllocator& allocator; + Token currentToken; + Token nextToken; + std::size_t blockDepth = 0; +}; + +} // namespace mlir::oq3::frontend::detail diff --git a/mlir/include/mlir/Target/OpenQASM/Detail/OpenQASMSemantics.h b/mlir/include/mlir/Target/OpenQASM/Detail/OpenQASMSemantics.h new file mode 100644 index 0000000000..11f44292d8 --- /dev/null +++ b/mlir/include/mlir/Target/OpenQASM/Detail/OpenQASMSemantics.h @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#pragma once + +#include "mlir/Target/OpenQASM/Detail/OpenQASMSyntax.h" +#include "mlir/Target/OpenQASM/Frontend.h" + +#include + +namespace mlir::oq3::frontend::detail { + +[[nodiscard]] AnalysisResult +analyzeSyntaxProgram(const SyntaxProgram& syntax, + const llvm::SourceMgr& sources, + const FrontendOptions& options); + +[[nodiscard]] SourceLocation sourceLocation(const llvm::SourceMgr& sources, + llvm::SMLoc location); + +} // namespace mlir::oq3::frontend::detail diff --git a/mlir/include/mlir/Target/OpenQASM/Detail/OpenQASMSyntax.h b/mlir/include/mlir/Target/OpenQASM/Detail/OpenQASMSyntax.h new file mode 100644 index 0000000000..3edbf470ac --- /dev/null +++ b/mlir/include/mlir/Target/OpenQASM/Detail/OpenQASMSyntax.h @@ -0,0 +1,235 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#pragma once + +#include "mlir/Target/OpenQASM/Detail/OpenQASMParser.h" + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace mlir::oq3::frontend::detail { + +using SyntaxExpressionId = std::uint32_t; +using SyntaxStatementId = std::uint32_t; + +struct SyntaxExpression { + Expr::Kind kind = Expr::Kind::Int; + SMLoc location; + std::uint64_t integer = 0; + double floatingPoint = 0.0; + bool boolean = false; + StringRef identifier; + std::optional hardwareQubit; + std::optional lhs; + std::optional rhs; +}; + +struct SyntaxOperand { + SMLoc location; + StringRef identifier; + std::optional index; + std::optional hardwareQubit; +}; + +struct SyntaxBitReference { + SMLoc location; + StringRef identifier; + std::optional index; +}; + +struct SyntaxModifier { + Modifier::Kind kind = Modifier::Kind::Inv; + std::optional argument; +}; + +struct SyntaxGateCall { + SMLoc location; + StringRef identifier; + std::vector modifiers; + std::vector parameters; + std::vector operands; +}; + +struct SyntaxScalarDeclaration { + ScalarKind kind = ScalarKind::Int; + StringRef identifier; + std::optional initializer; + bool isConst = false; +}; + +struct SyntaxAssignment { + SyntaxBitReference target; + SyntaxExpressionId value = 0; +}; + +struct SyntaxQubitDeclaration { + StringRef identifier; + std::optional size; +}; + +struct SyntaxBitDeclaration { + StringRef identifier; + std::optional size; + std::optional initializer; + bool output = false; +}; + +struct SyntaxMeasurement { + std::optional target; + SyntaxOperand source; +}; + +struct SyntaxReset { + SyntaxOperand operand; +}; + +struct SyntaxBarrier { + std::vector operands; +}; + +struct SyntaxGateDefinition { + StringRef identifier; + std::vector parameters; + std::vector qubits; + std::vector body; +}; + +struct SyntaxIf { + SyntaxExpressionId condition = 0; + std::vector thenStatements; + std::vector elseStatements; +}; + +struct SyntaxFor { + StringRef inductionVariable; + bool isUnsigned = false; + SyntaxExpressionId start = 0; + SyntaxExpressionId step = 0; + SyntaxExpressionId stop = 0; + std::vector body; +}; + +struct SyntaxWhile { + SyntaxExpressionId condition = 0; + std::vector body; +}; + +struct SyntaxStandardLibraryInclude {}; + +using SyntaxStatementData = + std::variant; + +struct SyntaxStatement { + SMLoc location; + SyntaxStatementData data; +}; + +struct SyntaxInclude { + SMLoc location; + StringRef filename; + std::size_t bodyOffset = 0; +}; + +struct SyntaxProgram { + std::optional version; + SMLoc versionLocation; + std::vector includes; + std::vector expressions; + std::vector statements; + std::vector body; +}; + +struct SyntaxDiagnostic { + SMLoc location; + std::string message; +}; + +class SyntaxBuilder { +public: + [[nodiscard]] LogicalResult error(SMLoc location, const Twine& message); + [[nodiscard]] LogicalResult version(SMLoc location, Version value); + [[nodiscard]] LogicalResult include(SMLoc location, StringRef filename); + [[nodiscard]] SyntaxStatementId standardLibraryInclude(SMLoc location); + [[nodiscard]] LogicalResult scalarDecl(SMLoc location, ScalarKind kind, + StringRef identifier, + const Expr* initializer, bool isConst); + [[nodiscard]] LogicalResult + assignment(SMLoc location, const BitReference& target, const Expr& value); + [[nodiscard]] LogicalResult + qubitRegister(SMLoc location, StringRef identifier, const Expr* size); + [[nodiscard]] LogicalResult + classicalRegister(SMLoc location, StringRef identifier, const Expr* size, + const Expr* initializer, bool output); + [[nodiscard]] LogicalResult + measure(SMLoc location, const BitReference* target, const Operand& source); + [[nodiscard]] LogicalResult reset(SMLoc location, const Operand& operand); + [[nodiscard]] LogicalResult barrier(SMLoc location, + ArrayRef operands); + [[nodiscard]] LogicalResult gateCall(const GateCall& call); + [[nodiscard]] LogicalResult + gateDefinition(SMLoc location, StringRef identifier, + ArrayRef parameters, ArrayRef qubits, + function_ref continuation); + [[nodiscard]] LogicalResult + ifStmt(SMLoc location, const Expr& condition, + function_ref thenContinuation, + function_ref elseContinuation); + [[nodiscard]] LogicalResult + forStmt(SMLoc location, StringRef inductionVariable, bool isUnsigned, + const Expr& start, const Expr& step, const Expr& stop, + function_ref continuation); + [[nodiscard]] LogicalResult + whileStmt(SMLoc location, const Expr& condition, + function_ref continuation); + + [[nodiscard]] SyntaxProgram takeProgram() { return std::move(program); } + [[nodiscard]] const std::vector& getDiagnostics() const { + return diagnostics; + } + [[nodiscard]] ArrayRef getIncludes() const { + return program.includes; + } + [[nodiscard]] ArrayRef getBody() const { + return program.body; + } + void replaceBody(std::vector body); + +private: + [[nodiscard]] SyntaxExpressionId copyExpression(const Expr& expression); + [[nodiscard]] SyntaxOperand copyOperand(const Operand& operand); + [[nodiscard]] SyntaxBitReference + copyBitReference(const BitReference& reference); + [[nodiscard]] SyntaxGateCall copyGateCall(const GateCall& call); + [[nodiscard]] SyntaxStatementId addStatement(SMLoc location, + SyntaxStatementData data); + [[nodiscard]] FailureOr> + parseNestedBody(function_ref continuation); + + SyntaxProgram program; + std::vector diagnostics; + SmallVector*> bodyStack{&program.body}; + bool sawConstruct = false; +}; + +} // namespace mlir::oq3::frontend::detail diff --git a/mlir/include/mlir/Target/OpenQASM/Detail/OpenQASMUnicode.h b/mlir/include/mlir/Target/OpenQASM/Detail/OpenQASMUnicode.h new file mode 100644 index 0000000000..fe2e03e586 --- /dev/null +++ b/mlir/include/mlir/Target/OpenQASM/Detail/OpenQASMUnicode.h @@ -0,0 +1,274 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#pragma once + +#include +#include +#include + +namespace mlir::oq3::frontend::detail { + +struct UnicodeRange { + std::uint32_t first; + std::uint32_t last; +}; + +// Unicode general categories Lu, Ll, Lt, Lm, Lo, and Nl, as required by the +// OpenQASM 3 identifier grammar. ASCII letters are handled directly by the +// lexer and are intentionally omitted from this table. +inline constexpr UnicodeRange OPENQASM_IDENTIFIER_RANGES[] = { + {0xAA, 0xAA}, {0xB5, 0xB5}, {0xBA, 0xBA}, + {0xC0, 0xD6}, {0xD8, 0xF6}, {0xF8, 0x2C1}, + {0x2C6, 0x2D1}, {0x2E0, 0x2E4}, {0x2EC, 0x2EC}, + {0x2EE, 0x2EE}, {0x370, 0x374}, {0x376, 0x377}, + {0x37A, 0x37D}, {0x37F, 0x37F}, {0x386, 0x386}, + {0x388, 0x38A}, {0x38C, 0x38C}, {0x38E, 0x3A1}, + {0x3A3, 0x3F5}, {0x3F7, 0x481}, {0x48A, 0x52F}, + {0x531, 0x556}, {0x559, 0x559}, {0x560, 0x588}, + {0x5D0, 0x5EA}, {0x5EF, 0x5F2}, {0x620, 0x64A}, + {0x66E, 0x66F}, {0x671, 0x6D3}, {0x6D5, 0x6D5}, + {0x6E5, 0x6E6}, {0x6EE, 0x6EF}, {0x6FA, 0x6FC}, + {0x6FF, 0x6FF}, {0x710, 0x710}, {0x712, 0x72F}, + {0x74D, 0x7A5}, {0x7B1, 0x7B1}, {0x7CA, 0x7EA}, + {0x7F4, 0x7F5}, {0x7FA, 0x7FA}, {0x800, 0x815}, + {0x81A, 0x81A}, {0x824, 0x824}, {0x828, 0x828}, + {0x840, 0x858}, {0x860, 0x86A}, {0x870, 0x887}, + {0x889, 0x88E}, {0x8A0, 0x8C9}, {0x904, 0x939}, + {0x93D, 0x93D}, {0x950, 0x950}, {0x958, 0x961}, + {0x971, 0x980}, {0x985, 0x98C}, {0x98F, 0x990}, + {0x993, 0x9A8}, {0x9AA, 0x9B0}, {0x9B2, 0x9B2}, + {0x9B6, 0x9B9}, {0x9BD, 0x9BD}, {0x9CE, 0x9CE}, + {0x9DC, 0x9DD}, {0x9DF, 0x9E1}, {0x9F0, 0x9F1}, + {0x9FC, 0x9FC}, {0xA05, 0xA0A}, {0xA0F, 0xA10}, + {0xA13, 0xA28}, {0xA2A, 0xA30}, {0xA32, 0xA33}, + {0xA35, 0xA36}, {0xA38, 0xA39}, {0xA59, 0xA5C}, + {0xA5E, 0xA5E}, {0xA72, 0xA74}, {0xA85, 0xA8D}, + {0xA8F, 0xA91}, {0xA93, 0xAA8}, {0xAAA, 0xAB0}, + {0xAB2, 0xAB3}, {0xAB5, 0xAB9}, {0xABD, 0xABD}, + {0xAD0, 0xAD0}, {0xAE0, 0xAE1}, {0xAF9, 0xAF9}, + {0xB05, 0xB0C}, {0xB0F, 0xB10}, {0xB13, 0xB28}, + {0xB2A, 0xB30}, {0xB32, 0xB33}, {0xB35, 0xB39}, + {0xB3D, 0xB3D}, {0xB5C, 0xB5D}, {0xB5F, 0xB61}, + {0xB71, 0xB71}, {0xB83, 0xB83}, {0xB85, 0xB8A}, + {0xB8E, 0xB90}, {0xB92, 0xB95}, {0xB99, 0xB9A}, + {0xB9C, 0xB9C}, {0xB9E, 0xB9F}, {0xBA3, 0xBA4}, + {0xBA8, 0xBAA}, {0xBAE, 0xBB9}, {0xBD0, 0xBD0}, + {0xC05, 0xC0C}, {0xC0E, 0xC10}, {0xC12, 0xC28}, + {0xC2A, 0xC39}, {0xC3D, 0xC3D}, {0xC58, 0xC5A}, + {0xC5D, 0xC5D}, {0xC60, 0xC61}, {0xC80, 0xC80}, + {0xC85, 0xC8C}, {0xC8E, 0xC90}, {0xC92, 0xCA8}, + {0xCAA, 0xCB3}, {0xCB5, 0xCB9}, {0xCBD, 0xCBD}, + {0xCDD, 0xCDE}, {0xCE0, 0xCE1}, {0xCF1, 0xCF2}, + {0xD04, 0xD0C}, {0xD0E, 0xD10}, {0xD12, 0xD3A}, + {0xD3D, 0xD3D}, {0xD4E, 0xD4E}, {0xD54, 0xD56}, + {0xD5F, 0xD61}, {0xD7A, 0xD7F}, {0xD85, 0xD96}, + {0xD9A, 0xDB1}, {0xDB3, 0xDBB}, {0xDBD, 0xDBD}, + {0xDC0, 0xDC6}, {0xE01, 0xE30}, {0xE32, 0xE33}, + {0xE40, 0xE46}, {0xE81, 0xE82}, {0xE84, 0xE84}, + {0xE86, 0xE8A}, {0xE8C, 0xEA3}, {0xEA5, 0xEA5}, + {0xEA7, 0xEB0}, {0xEB2, 0xEB3}, {0xEBD, 0xEBD}, + {0xEC0, 0xEC4}, {0xEC6, 0xEC6}, {0xEDC, 0xEDF}, + {0xF00, 0xF00}, {0xF40, 0xF47}, {0xF49, 0xF6C}, + {0xF88, 0xF8C}, {0x1000, 0x102A}, {0x103F, 0x103F}, + {0x1050, 0x1055}, {0x105A, 0x105D}, {0x1061, 0x1061}, + {0x1065, 0x1066}, {0x106E, 0x1070}, {0x1075, 0x1081}, + {0x108E, 0x108E}, {0x10A0, 0x10C5}, {0x10C7, 0x10C7}, + {0x10CD, 0x10CD}, {0x10D0, 0x10FA}, {0x10FC, 0x1248}, + {0x124A, 0x124D}, {0x1250, 0x1256}, {0x1258, 0x1258}, + {0x125A, 0x125D}, {0x1260, 0x1288}, {0x128A, 0x128D}, + {0x1290, 0x12B0}, {0x12B2, 0x12B5}, {0x12B8, 0x12BE}, + {0x12C0, 0x12C0}, {0x12C2, 0x12C5}, {0x12C8, 0x12D6}, + {0x12D8, 0x1310}, {0x1312, 0x1315}, {0x1318, 0x135A}, + {0x1380, 0x138F}, {0x13A0, 0x13F5}, {0x13F8, 0x13FD}, + {0x1401, 0x166C}, {0x166F, 0x167F}, {0x1681, 0x169A}, + {0x16A0, 0x16EA}, {0x16EE, 0x16F8}, {0x1700, 0x1711}, + {0x171F, 0x1731}, {0x1740, 0x1751}, {0x1760, 0x176C}, + {0x176E, 0x1770}, {0x1780, 0x17B3}, {0x17D7, 0x17D7}, + {0x17DC, 0x17DC}, {0x1820, 0x1878}, {0x1880, 0x1884}, + {0x1887, 0x18A8}, {0x18AA, 0x18AA}, {0x18B0, 0x18F5}, + {0x1900, 0x191E}, {0x1950, 0x196D}, {0x1970, 0x1974}, + {0x1980, 0x19AB}, {0x19B0, 0x19C9}, {0x1A00, 0x1A16}, + {0x1A20, 0x1A54}, {0x1AA7, 0x1AA7}, {0x1B05, 0x1B33}, + {0x1B45, 0x1B4C}, {0x1B83, 0x1BA0}, {0x1BAE, 0x1BAF}, + {0x1BBA, 0x1BE5}, {0x1C00, 0x1C23}, {0x1C4D, 0x1C4F}, + {0x1C5A, 0x1C7D}, {0x1C80, 0x1C8A}, {0x1C90, 0x1CBA}, + {0x1CBD, 0x1CBF}, {0x1CE9, 0x1CEC}, {0x1CEE, 0x1CF3}, + {0x1CF5, 0x1CF6}, {0x1CFA, 0x1CFA}, {0x1D00, 0x1DBF}, + {0x1E00, 0x1F15}, {0x1F18, 0x1F1D}, {0x1F20, 0x1F45}, + {0x1F48, 0x1F4D}, {0x1F50, 0x1F57}, {0x1F59, 0x1F59}, + {0x1F5B, 0x1F5B}, {0x1F5D, 0x1F5D}, {0x1F5F, 0x1F7D}, + {0x1F80, 0x1FB4}, {0x1FB6, 0x1FBC}, {0x1FBE, 0x1FBE}, + {0x1FC2, 0x1FC4}, {0x1FC6, 0x1FCC}, {0x1FD0, 0x1FD3}, + {0x1FD6, 0x1FDB}, {0x1FE0, 0x1FEC}, {0x1FF2, 0x1FF4}, + {0x1FF6, 0x1FFC}, {0x2071, 0x2071}, {0x207F, 0x207F}, + {0x2090, 0x209C}, {0x2102, 0x2102}, {0x2107, 0x2107}, + {0x210A, 0x2113}, {0x2115, 0x2115}, {0x2119, 0x211D}, + {0x2124, 0x2124}, {0x2126, 0x2126}, {0x2128, 0x2128}, + {0x212A, 0x212D}, {0x212F, 0x2139}, {0x213C, 0x213F}, + {0x2145, 0x2149}, {0x214E, 0x214E}, {0x2160, 0x2188}, + {0x2C00, 0x2CE4}, {0x2CEB, 0x2CEE}, {0x2CF2, 0x2CF3}, + {0x2D00, 0x2D25}, {0x2D27, 0x2D27}, {0x2D2D, 0x2D2D}, + {0x2D30, 0x2D67}, {0x2D6F, 0x2D6F}, {0x2D80, 0x2D96}, + {0x2DA0, 0x2DA6}, {0x2DA8, 0x2DAE}, {0x2DB0, 0x2DB6}, + {0x2DB8, 0x2DBE}, {0x2DC0, 0x2DC6}, {0x2DC8, 0x2DCE}, + {0x2DD0, 0x2DD6}, {0x2DD8, 0x2DDE}, {0x2E2F, 0x2E2F}, + {0x3005, 0x3007}, {0x3021, 0x3029}, {0x3031, 0x3035}, + {0x3038, 0x303C}, {0x3041, 0x3096}, {0x309D, 0x309F}, + {0x30A1, 0x30FA}, {0x30FC, 0x30FF}, {0x3105, 0x312F}, + {0x3131, 0x318E}, {0x31A0, 0x31BF}, {0x31F0, 0x31FF}, + {0x3400, 0x4DBF}, {0x4E00, 0xA48C}, {0xA4D0, 0xA4FD}, + {0xA500, 0xA60C}, {0xA610, 0xA61F}, {0xA62A, 0xA62B}, + {0xA640, 0xA66E}, {0xA67F, 0xA69D}, {0xA6A0, 0xA6EF}, + {0xA717, 0xA71F}, {0xA722, 0xA788}, {0xA78B, 0xA7CD}, + {0xA7D0, 0xA7D1}, {0xA7D3, 0xA7D3}, {0xA7D5, 0xA7DC}, + {0xA7F2, 0xA801}, {0xA803, 0xA805}, {0xA807, 0xA80A}, + {0xA80C, 0xA822}, {0xA840, 0xA873}, {0xA882, 0xA8B3}, + {0xA8F2, 0xA8F7}, {0xA8FB, 0xA8FB}, {0xA8FD, 0xA8FE}, + {0xA90A, 0xA925}, {0xA930, 0xA946}, {0xA960, 0xA97C}, + {0xA984, 0xA9B2}, {0xA9CF, 0xA9CF}, {0xA9E0, 0xA9E4}, + {0xA9E6, 0xA9EF}, {0xA9FA, 0xA9FE}, {0xAA00, 0xAA28}, + {0xAA40, 0xAA42}, {0xAA44, 0xAA4B}, {0xAA60, 0xAA76}, + {0xAA7A, 0xAA7A}, {0xAA7E, 0xAAAF}, {0xAAB1, 0xAAB1}, + {0xAAB5, 0xAAB6}, {0xAAB9, 0xAABD}, {0xAAC0, 0xAAC0}, + {0xAAC2, 0xAAC2}, {0xAADB, 0xAADD}, {0xAAE0, 0xAAEA}, + {0xAAF2, 0xAAF4}, {0xAB01, 0xAB06}, {0xAB09, 0xAB0E}, + {0xAB11, 0xAB16}, {0xAB20, 0xAB26}, {0xAB28, 0xAB2E}, + {0xAB30, 0xAB5A}, {0xAB5C, 0xAB69}, {0xAB70, 0xABE2}, + {0xAC00, 0xD7A3}, {0xD7B0, 0xD7C6}, {0xD7CB, 0xD7FB}, + {0xF900, 0xFA6D}, {0xFA70, 0xFAD9}, {0xFB00, 0xFB06}, + {0xFB13, 0xFB17}, {0xFB1D, 0xFB1D}, {0xFB1F, 0xFB28}, + {0xFB2A, 0xFB36}, {0xFB38, 0xFB3C}, {0xFB3E, 0xFB3E}, + {0xFB40, 0xFB41}, {0xFB43, 0xFB44}, {0xFB46, 0xFBB1}, + {0xFBD3, 0xFD3D}, {0xFD50, 0xFD8F}, {0xFD92, 0xFDC7}, + {0xFDF0, 0xFDFB}, {0xFE70, 0xFE74}, {0xFE76, 0xFEFC}, + {0xFF21, 0xFF3A}, {0xFF41, 0xFF5A}, {0xFF66, 0xFFBE}, + {0xFFC2, 0xFFC7}, {0xFFCA, 0xFFCF}, {0xFFD2, 0xFFD7}, + {0xFFDA, 0xFFDC}, {0x10000, 0x1000B}, {0x1000D, 0x10026}, + {0x10028, 0x1003A}, {0x1003C, 0x1003D}, {0x1003F, 0x1004D}, + {0x10050, 0x1005D}, {0x10080, 0x100FA}, {0x10140, 0x10174}, + {0x10280, 0x1029C}, {0x102A0, 0x102D0}, {0x10300, 0x1031F}, + {0x1032D, 0x1034A}, {0x10350, 0x10375}, {0x10380, 0x1039D}, + {0x103A0, 0x103C3}, {0x103C8, 0x103CF}, {0x103D1, 0x103D5}, + {0x10400, 0x1049D}, {0x104B0, 0x104D3}, {0x104D8, 0x104FB}, + {0x10500, 0x10527}, {0x10530, 0x10563}, {0x10570, 0x1057A}, + {0x1057C, 0x1058A}, {0x1058C, 0x10592}, {0x10594, 0x10595}, + {0x10597, 0x105A1}, {0x105A3, 0x105B1}, {0x105B3, 0x105B9}, + {0x105BB, 0x105BC}, {0x105C0, 0x105F3}, {0x10600, 0x10736}, + {0x10740, 0x10755}, {0x10760, 0x10767}, {0x10780, 0x10785}, + {0x10787, 0x107B0}, {0x107B2, 0x107BA}, {0x10800, 0x10805}, + {0x10808, 0x10808}, {0x1080A, 0x10835}, {0x10837, 0x10838}, + {0x1083C, 0x1083C}, {0x1083F, 0x10855}, {0x10860, 0x10876}, + {0x10880, 0x1089E}, {0x108E0, 0x108F2}, {0x108F4, 0x108F5}, + {0x10900, 0x10915}, {0x10920, 0x10939}, {0x10980, 0x109B7}, + {0x109BE, 0x109BF}, {0x10A00, 0x10A00}, {0x10A10, 0x10A13}, + {0x10A15, 0x10A17}, {0x10A19, 0x10A35}, {0x10A60, 0x10A7C}, + {0x10A80, 0x10A9C}, {0x10AC0, 0x10AC7}, {0x10AC9, 0x10AE4}, + {0x10B00, 0x10B35}, {0x10B40, 0x10B55}, {0x10B60, 0x10B72}, + {0x10B80, 0x10B91}, {0x10C00, 0x10C48}, {0x10C80, 0x10CB2}, + {0x10CC0, 0x10CF2}, {0x10D00, 0x10D23}, {0x10D4A, 0x10D65}, + {0x10D6F, 0x10D85}, {0x10E80, 0x10EA9}, {0x10EB0, 0x10EB1}, + {0x10EC2, 0x10EC4}, {0x10F00, 0x10F1C}, {0x10F27, 0x10F27}, + {0x10F30, 0x10F45}, {0x10F70, 0x10F81}, {0x10FB0, 0x10FC4}, + {0x10FE0, 0x10FF6}, {0x11003, 0x11037}, {0x11071, 0x11072}, + {0x11075, 0x11075}, {0x11083, 0x110AF}, {0x110D0, 0x110E8}, + {0x11103, 0x11126}, {0x11144, 0x11144}, {0x11147, 0x11147}, + {0x11150, 0x11172}, {0x11176, 0x11176}, {0x11183, 0x111B2}, + {0x111C1, 0x111C4}, {0x111DA, 0x111DA}, {0x111DC, 0x111DC}, + {0x11200, 0x11211}, {0x11213, 0x1122B}, {0x1123F, 0x11240}, + {0x11280, 0x11286}, {0x11288, 0x11288}, {0x1128A, 0x1128D}, + {0x1128F, 0x1129D}, {0x1129F, 0x112A8}, {0x112B0, 0x112DE}, + {0x11305, 0x1130C}, {0x1130F, 0x11310}, {0x11313, 0x11328}, + {0x1132A, 0x11330}, {0x11332, 0x11333}, {0x11335, 0x11339}, + {0x1133D, 0x1133D}, {0x11350, 0x11350}, {0x1135D, 0x11361}, + {0x11380, 0x11389}, {0x1138B, 0x1138B}, {0x1138E, 0x1138E}, + {0x11390, 0x113B5}, {0x113B7, 0x113B7}, {0x113D1, 0x113D1}, + {0x113D3, 0x113D3}, {0x11400, 0x11434}, {0x11447, 0x1144A}, + {0x1145F, 0x11461}, {0x11480, 0x114AF}, {0x114C4, 0x114C5}, + {0x114C7, 0x114C7}, {0x11580, 0x115AE}, {0x115D8, 0x115DB}, + {0x11600, 0x1162F}, {0x11644, 0x11644}, {0x11680, 0x116AA}, + {0x116B8, 0x116B8}, {0x11700, 0x1171A}, {0x11740, 0x11746}, + {0x11800, 0x1182B}, {0x118A0, 0x118DF}, {0x118FF, 0x11906}, + {0x11909, 0x11909}, {0x1190C, 0x11913}, {0x11915, 0x11916}, + {0x11918, 0x1192F}, {0x1193F, 0x1193F}, {0x11941, 0x11941}, + {0x119A0, 0x119A7}, {0x119AA, 0x119D0}, {0x119E1, 0x119E1}, + {0x119E3, 0x119E3}, {0x11A00, 0x11A00}, {0x11A0B, 0x11A32}, + {0x11A3A, 0x11A3A}, {0x11A50, 0x11A50}, {0x11A5C, 0x11A89}, + {0x11A9D, 0x11A9D}, {0x11AB0, 0x11AF8}, {0x11BC0, 0x11BE0}, + {0x11C00, 0x11C08}, {0x11C0A, 0x11C2E}, {0x11C40, 0x11C40}, + {0x11C72, 0x11C8F}, {0x11D00, 0x11D06}, {0x11D08, 0x11D09}, + {0x11D0B, 0x11D30}, {0x11D46, 0x11D46}, {0x11D60, 0x11D65}, + {0x11D67, 0x11D68}, {0x11D6A, 0x11D89}, {0x11D98, 0x11D98}, + {0x11EE0, 0x11EF2}, {0x11F02, 0x11F02}, {0x11F04, 0x11F10}, + {0x11F12, 0x11F33}, {0x11FB0, 0x11FB0}, {0x12000, 0x12399}, + {0x12400, 0x1246E}, {0x12480, 0x12543}, {0x12F90, 0x12FF0}, + {0x13000, 0x1342F}, {0x13441, 0x13446}, {0x13460, 0x143FA}, + {0x14400, 0x14646}, {0x16100, 0x1611D}, {0x16800, 0x16A38}, + {0x16A40, 0x16A5E}, {0x16A70, 0x16ABE}, {0x16AD0, 0x16AED}, + {0x16B00, 0x16B2F}, {0x16B40, 0x16B43}, {0x16B63, 0x16B77}, + {0x16B7D, 0x16B8F}, {0x16D40, 0x16D6C}, {0x16E40, 0x16E7F}, + {0x16F00, 0x16F4A}, {0x16F50, 0x16F50}, {0x16F93, 0x16F9F}, + {0x16FE0, 0x16FE1}, {0x16FE3, 0x16FE3}, {0x17000, 0x187F7}, + {0x18800, 0x18CD5}, {0x18CFF, 0x18D08}, {0x1AFF0, 0x1AFF3}, + {0x1AFF5, 0x1AFFB}, {0x1AFFD, 0x1AFFE}, {0x1B000, 0x1B122}, + {0x1B132, 0x1B132}, {0x1B150, 0x1B152}, {0x1B155, 0x1B155}, + {0x1B164, 0x1B167}, {0x1B170, 0x1B2FB}, {0x1BC00, 0x1BC6A}, + {0x1BC70, 0x1BC7C}, {0x1BC80, 0x1BC88}, {0x1BC90, 0x1BC99}, + {0x1D400, 0x1D454}, {0x1D456, 0x1D49C}, {0x1D49E, 0x1D49F}, + {0x1D4A2, 0x1D4A2}, {0x1D4A5, 0x1D4A6}, {0x1D4A9, 0x1D4AC}, + {0x1D4AE, 0x1D4B9}, {0x1D4BB, 0x1D4BB}, {0x1D4BD, 0x1D4C3}, + {0x1D4C5, 0x1D505}, {0x1D507, 0x1D50A}, {0x1D50D, 0x1D514}, + {0x1D516, 0x1D51C}, {0x1D51E, 0x1D539}, {0x1D53B, 0x1D53E}, + {0x1D540, 0x1D544}, {0x1D546, 0x1D546}, {0x1D54A, 0x1D550}, + {0x1D552, 0x1D6A5}, {0x1D6A8, 0x1D6C0}, {0x1D6C2, 0x1D6DA}, + {0x1D6DC, 0x1D6FA}, {0x1D6FC, 0x1D714}, {0x1D716, 0x1D734}, + {0x1D736, 0x1D74E}, {0x1D750, 0x1D76E}, {0x1D770, 0x1D788}, + {0x1D78A, 0x1D7A8}, {0x1D7AA, 0x1D7C2}, {0x1D7C4, 0x1D7CB}, + {0x1DF00, 0x1DF1E}, {0x1DF25, 0x1DF2A}, {0x1E030, 0x1E06D}, + {0x1E100, 0x1E12C}, {0x1E137, 0x1E13D}, {0x1E14E, 0x1E14E}, + {0x1E290, 0x1E2AD}, {0x1E2C0, 0x1E2EB}, {0x1E4D0, 0x1E4EB}, + {0x1E5D0, 0x1E5ED}, {0x1E5F0, 0x1E5F0}, {0x1E7E0, 0x1E7E6}, + {0x1E7E8, 0x1E7EB}, {0x1E7ED, 0x1E7EE}, {0x1E7F0, 0x1E7FE}, + {0x1E800, 0x1E8C4}, {0x1E900, 0x1E943}, {0x1E94B, 0x1E94B}, + {0x1EE00, 0x1EE03}, {0x1EE05, 0x1EE1F}, {0x1EE21, 0x1EE22}, + {0x1EE24, 0x1EE24}, {0x1EE27, 0x1EE27}, {0x1EE29, 0x1EE32}, + {0x1EE34, 0x1EE37}, {0x1EE39, 0x1EE39}, {0x1EE3B, 0x1EE3B}, + {0x1EE42, 0x1EE42}, {0x1EE47, 0x1EE47}, {0x1EE49, 0x1EE49}, + {0x1EE4B, 0x1EE4B}, {0x1EE4D, 0x1EE4F}, {0x1EE51, 0x1EE52}, + {0x1EE54, 0x1EE54}, {0x1EE57, 0x1EE57}, {0x1EE59, 0x1EE59}, + {0x1EE5B, 0x1EE5B}, {0x1EE5D, 0x1EE5D}, {0x1EE5F, 0x1EE5F}, + {0x1EE61, 0x1EE62}, {0x1EE64, 0x1EE64}, {0x1EE67, 0x1EE6A}, + {0x1EE6C, 0x1EE72}, {0x1EE74, 0x1EE77}, {0x1EE79, 0x1EE7C}, + {0x1EE7E, 0x1EE7E}, {0x1EE80, 0x1EE89}, {0x1EE8B, 0x1EE9B}, + {0x1EEA1, 0x1EEA3}, {0x1EEA5, 0x1EEA9}, {0x1EEAB, 0x1EEBB}, + {0x20000, 0x2A6DF}, {0x2A700, 0x2B739}, {0x2B740, 0x2B81D}, + {0x2B820, 0x2CEA1}, {0x2CEB0, 0x2EBE0}, {0x2EBF0, 0x2EE5D}, + {0x2F800, 0x2FA1D}, {0x30000, 0x3134A}, {0x31350, 0x323AF}, +}; + +[[nodiscard]] constexpr bool +isOpenQASMIdentifierCodePoint(const std::uint32_t codePoint) noexcept { + std::size_t first = 0; + std::size_t last = std::size(OPENQASM_IDENTIFIER_RANGES); + while (first < last) { + const auto middle = first + (last - first) / 2; + const auto range = OPENQASM_IDENTIFIER_RANGES[middle]; + if (codePoint < range.first) { + last = middle; + } else if (codePoint > range.last) { + first = middle + 1; + } else { + return true; + } + } + return false; +} + +} // namespace mlir::oq3::frontend::detail diff --git a/mlir/include/mlir/Target/OpenQASM/Frontend.h b/mlir/include/mlir/Target/OpenQASM/Frontend.h new file mode 100644 index 0000000000..a50c8849b5 --- /dev/null +++ b/mlir/include/mlir/Target/OpenQASM/Frontend.h @@ -0,0 +1,323 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#pragma once + +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace llvm { +class SourceMgr; +} // namespace llvm + +namespace mlir::oq3::frontend { + +using ExpressionId = std::uint32_t; +using RegisterId = std::uint32_t; +using ScalarId = std::uint32_t; +using ConditionId = std::uint32_t; +using StatementId = std::uint32_t; + +struct SourceLocation { + std::string filename = ""; + std::uint32_t line = 1; + std::uint32_t column = 1; +}; + +struct Diagnostic { + SourceLocation location; + std::string message; +}; + +enum class GatePolicy : std::uint8_t { + Strict, + MQTCompatibility, +}; + +struct FrontendOptions { + GatePolicy gatePolicy = GatePolicy::MQTCompatibility; +}; + +struct AnalysisResult; +struct ParseResult; + +class ParsedProgram { +public: + ParsedProgram(ParsedProgram&&) noexcept; + ParsedProgram& operator=(ParsedProgram&&) noexcept; + ~ParsedProgram(); + + ParsedProgram(const ParsedProgram&) = delete; + ParsedProgram& operator=(const ParsedProgram&) = delete; + +private: + struct Impl; + std::unique_ptr impl; + + explicit ParsedProgram(std::unique_ptr implementation); + + friend ParseResult parseOpenQASM(llvm::StringRef); + friend ParseResult parseOpenQASM(llvm::SourceMgr&); + friend AnalysisResult analyzeOpenQASM(const ParsedProgram&, + const FrontendOptions&); +}; + +struct ParseResult { + std::unique_ptr program; + std::vector diagnostics; + + [[nodiscard]] explicit operator bool() const { return program != nullptr; } +}; + +enum class ScalarType : std::uint8_t { + Bool, + Int, + Uint, + Float, +}; + +enum class ExpressionKind : std::uint8_t { + Constant, + GateParameter, + Variable, + Negate, + ArcCos, + ArcSin, + ArcTan, + Sin, + Cos, + Tan, + Exp, + Ln, + Sqrt, + Add, + Subtract, + Multiply, + Divide, + Modulo, + Power, +}; + +struct ScalarExpression { + ExpressionKind kind = ExpressionKind::Constant; + ScalarType type = ScalarType::Float; + std::variant constant = 0.0; + std::uint32_t parameter = 0; + ScalarId variable = 0; + ExpressionId lhs = 0; + ExpressionId rhs = 0; +}; + +struct ScalarDeclaration { + ScalarType type = ScalarType::Int; + std::string name; +}; + +enum class RegisterKind : std::uint8_t { + Qubit, + Bit, +}; + +struct RegisterDeclaration { + RegisterKind kind = RegisterKind::Qubit; + std::string name; + std::uint64_t width = 0; + SourceLocation location; +}; + +enum class QubitReferenceKind : std::uint8_t { + Register, + GateArgument, + Hardware, +}; + +struct QubitReference { + QubitReferenceKind kind = QubitReferenceKind::Register; + std::uint32_t symbol = 0; + std::uint64_t index = 0; + std::optional dynamicIndex; + + bool operator==(const QubitReference&) const = default; +}; + +struct BitReference { + RegisterId reg = 0; + std::uint64_t index = 0; + std::optional dynamicIndex; +}; + +enum class ComparisonKind : std::uint8_t { + Equal, + NotEqual, + Less, + LessEqual, + Greater, + GreaterEqual, +}; + +enum class ConditionKind : std::uint8_t { + Literal, + Scalar, + Bit, + Measurement, + Not, + And, + Or, + Comparison, +}; + +struct ConditionExpression { + ConditionKind kind = ConditionKind::Literal; + SourceLocation location; + bool literal = false; + ScalarId scalar = 0; + BitReference bit; + QubitReference measurement; + ConditionId lhs = 0; + ConditionId rhs = 0; + ExpressionId comparisonLhs = 0; + ExpressionId comparisonRhs = 0; + ComparisonKind comparison = ComparisonKind::Equal; +}; + +enum class ModifierKind : std::uint8_t { + Inv, + Ctrl, + NegCtrl, + Pow, +}; + +struct GateModifier { + ModifierKind kind = ModifierKind::Inv; + std::optional operand; +}; + +struct GateApplication { + std::string callee; + std::vector parameters; + std::vector qubits; + std::vector modifiers; +}; + +struct GateDefinition { + std::string name; + std::size_t parameterCount = 0; + std::size_t qubitCount = 0; + std::vector body; + SourceLocation location; +}; + +struct DeclarationStatement { + RegisterId reg = 0; +}; + +struct ScalarDeclarationStatement { + ScalarId scalar = 0; + std::optional initializer; + std::optional conditionInitializer; +}; + +struct ScalarAssignmentStatement { + ScalarId scalar = 0; + std::optional value; + std::optional condition; +}; + +struct BitAssignmentStatement { + BitReference target; + ConditionId value = 0; +}; + +struct MeasurementStatement { + std::vector targets; + std::vector qubits; +}; + +struct ResetStatement { + std::vector qubits; +}; + +struct BarrierStatement { + std::vector qubits; +}; + +struct IfStatement { + ConditionId condition = 0; + std::vector thenStatements; + std::vector elseStatements; +}; + +struct ForStatement { + ScalarId inductionVariable = 0; + ExpressionId start = 0; + ExpressionId step = 0; + ExpressionId stop = 0; + std::vector body; +}; + +struct WhileStatement { + ConditionId condition = 0; + std::vector body; +}; + +using StatementData = + std::variant; + +struct Statement { + StatementData data; + SourceLocation location; +}; + +struct TypedProgram { + bool openQASM2 = false; + bool standardLibraryIncluded = false; + std::vector expressions; + std::vector conditions; + std::vector scalars; + std::vector registers; + std::vector gates; + std::vector statements; + std::vector body; + std::vector outputs; +}; + +struct AnalysisResult { + std::unique_ptr program; + std::vector diagnostics; + + [[nodiscard]] explicit operator bool() const { return program != nullptr; } +}; + +[[nodiscard]] ParseResult parseOpenQASM(llvm::SourceMgr& sourceMgr); + +[[nodiscard]] ParseResult parseOpenQASM(llvm::StringRef source); + +[[nodiscard]] AnalysisResult +analyzeOpenQASM(const ParsedProgram& program, + const FrontendOptions& options = {}); + +[[nodiscard]] AnalysisResult +analyzeOpenQASM(llvm::SourceMgr& sourceMgr, + const FrontendOptions& options = {}); + +[[nodiscard]] AnalysisResult +analyzeOpenQASM(llvm::StringRef source, const FrontendOptions& options = {}); + +} // namespace mlir::oq3::frontend diff --git a/mlir/include/mlir/Target/OpenQASM/GateCatalog.h b/mlir/include/mlir/Target/OpenQASM/GateCatalog.h new file mode 100644 index 0000000000..c0fc14bc65 --- /dev/null +++ b/mlir/include/mlir/Target/OpenQASM/GateCatalog.h @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#pragma once + +#include +#include + +#include +#include + +namespace mlir::oq3::frontend { + +enum class GateAvailability : std::uint8_t { + Language, + StandardLibrary, + Compatibility, +}; + +struct GateCatalogEntry { + llvm::StringRef name; + llvm::StringRef primitive; + std::size_t parameterCount; + std::size_t controlCount; + std::size_t targetCount; + GateAvailability availability; + bool variadicControls = false; + bool inverse = false; + + [[nodiscard]] std::size_t qubitCount() const { + return controlCount + targetCount; + } +}; + +[[nodiscard]] llvm::ArrayRef getGateCatalog(); + +[[nodiscard]] const GateCatalogEntry* lookupGate(llvm::StringRef name); + +} // namespace mlir::oq3::frontend diff --git a/mlir/lib/CMakeLists.txt b/mlir/lib/CMakeLists.txt index 959d80e2f1..611313d860 100644 --- a/mlir/lib/CMakeLists.txt +++ b/mlir/lib/CMakeLists.txt @@ -10,3 +10,4 @@ add_subdirectory(Conversion) add_subdirectory(Compiler) add_subdirectory(Dialect) add_subdirectory(Support) +add_subdirectory(Target) diff --git a/mlir/lib/Conversion/JeffToQCO/JeffToQCO.cpp b/mlir/lib/Conversion/JeffToQCO/JeffToQCO.cpp index 7d516b1a50..74d31fb773 100644 --- a/mlir/lib/Conversion/JeffToQCO/JeffToQCO.cpp +++ b/mlir/lib/Conversion/JeffToQCO/JeffToQCO.cpp @@ -1134,23 +1134,27 @@ struct ConvertJeffMainToQCO final : OpConversionPattern { return failure(); } - // Update function signature and add passthrough attribute + // Restore the compiler entry-point marker. Jeff functions may retain + // observable classical results, so only synthesize the legacy i64 status + // result for a result-less entry point. rewriter.startOpModification(op); auto* ctx = rewriter.getContext(); - op.setType(FunctionType::get(ctx, {}, {rewriter.getI64Type()})); auto entryPointAttr = StringAttr::get(ctx, "entry_point"); op->setAttr("passthrough", ArrayAttr::get(ctx, {entryPointAttr})); + const bool needsStatusResult = op.getFunctionType().getResults().empty(); + if (needsStatusResult) { + op.setType(FunctionType::get(ctx, {}, {rewriter.getI64Type()})); + } rewriter.finalizeOpModification(op); - // Replace return operation - rewriter.setInsertionPointToStart(block); - auto constOp = arith::ConstantOp::create(rewriter, op.getLoc(), - rewriter.getI64IntegerAttr(0)); - - rewriter.setInsertionPointToEnd(block); - func::ReturnOp::create(rewriter, op.getLoc(), constOp.getResult()); - - rewriter.eraseOp(returnOp); + if (needsStatusResult) { + rewriter.setInsertionPointToStart(block); + auto constOp = arith::ConstantOp::create(rewriter, op.getLoc(), + rewriter.getI64IntegerAttr(0)); + rewriter.setInsertionPointToEnd(block); + func::ReturnOp::create(rewriter, op.getLoc(), constOp.getResult()); + rewriter.eraseOp(returnOp); + } return success(); } @@ -1201,8 +1205,8 @@ struct JeffToQCO final : impl::JeffToQCOBase { tensor::TensorDialect, scf::SCFDialect>(); target.addDynamicallyLegalOp([&](func::FuncOp op) { - return !(op.getSymName() == getEntryPointName(module) && - op.getFunctionType().getResults().empty()); + return op.getSymName() != getEntryPointName(module) || + op->hasAttr("passthrough"); }); target.addLegalOp(); diff --git a/mlir/lib/Conversion/QCToQCO/QCToQCO.cpp b/mlir/lib/Conversion/QCToQCO/QCToQCO.cpp index f778521d06..3ee99f94b8 100644 --- a/mlir/lib/Conversion/QCToQCO/QCToQCO.cpp +++ b/mlir/lib/Conversion/QCToQCO/QCToQCO.cpp @@ -116,6 +116,11 @@ struct LoweringState { DenseMap currentRegisters; }; + struct StructuredValues { + SmallVector qubits; + SmallVector memrefs; + }; + /// Per-region map from original QC qubit reference to its latest QCO SSA /// value. /// @@ -139,6 +144,11 @@ struct LoweringState { /// Map from an operation to its used QC memrefs inside its regions DenseMap> regionRegisterMap; + /// Original QC value order for already-converted structured operations. + /// Kept separate from the legality maps above so converted SCF operations + /// remain legal. + DenseMap structuredValues; + /// Stack of active modifier regions SmallVector modifierFrames; @@ -367,31 +377,36 @@ static void pushModifierFrame(LoweringState& state, ValueRange qcTargets, } } -/** @brief Pushes a new modifier frame seeded with aliased target values with - * registers */ -static void pushModifierFrameWithRegisters(LoweringState& state, - ValueRange qcTargets, - ValueRange memrefs, - ValueRange qcoTargets, - ValueRange tensors) { - auto& [yieldOrder, registers, currentQubits, currentRegisters] = - state.modifierFrames.emplace_back(); - llvm::append_range(yieldOrder, qcTargets); - for (auto [qcTarget, qcoTarget] : llvm::zip_equal(qcTargets, qcoTargets)) { - currentQubits.try_emplace(qcTarget, qcoTarget); - } - llvm::append_range(registers, memrefs); - for (auto [memref, tensor] : llvm::zip_equal(memrefs, tensors)) { - currentRegisters.try_emplace(memref, tensor); - } -} - /** @brief Pops the active modifier frame after lowering its yield. */ static void popModifierFrame(LoweringState& state) { assert(isInsideModifier(state) && "expected active modifier frame"); state.modifierFrames.pop_back(); } +/** @brief Returns the structured parent whose quantum values a terminator + * yields. */ +[[nodiscard]] static Operation* structuredValueOwner(Operation* operation) { + if (isa(operation)) { + return operation->getParentOp(); + } + return operation; +} + +/** @brief Seeds region-local QCO mappings for structured-control-flow block + * arguments. */ +static void seedRegionMappings(LoweringState& state, Region& region, + ValueRange qcQubits, ValueRange memrefs, + ValueRange qcoQubits, ValueRange tensors) { + auto& qubitMap = state.qubitMap[®ion]; + for (auto [qcQubit, qcoQubit] : llvm::zip_equal(qcQubits, qcoQubits)) { + qubitMap[qcQubit] = qcoQubit; + } + auto& tensorMap = state.tensorMap[®ion]; + for (auto [memref, tensor] : llvm::zip_equal(memrefs, tensors)) { + tensorMap[memref] = tensor; + } +} + /** * @brief Inserts extracted qubits that are not required by @p target back into * their tensors. @@ -405,7 +420,13 @@ static void insertQubitsBeforeOp(LoweringState& state, Operation* target, memrefs = currentModifierFrame(state).memrefs; anchor = target->getParentOp(); } else { - llvm::append_range(memrefs, state.regionRegisterMap[target]); + auto* owner = structuredValueOwner(target); + if (const auto it = state.structuredValues.find(owner); + it != state.structuredValues.end()) { + llvm::append_range(memrefs, it->second.memrefs); + } else { + llvm::append_range(memrefs, state.regionRegisterMap[owner]); + } anchor = target; } @@ -478,8 +499,15 @@ static void extractQubitsAfterOp(LoweringState& state, Operation* target, memrefs = frame.memrefs; qcQubits = frame.qcQubits; } else { - llvm::append_range(memrefs, state.regionRegisterMap[anchor]); - llvm::append_range(qcQubits, state.regionQubitMap[anchor]); + auto* owner = structuredValueOwner(anchor); + if (const auto it = state.structuredValues.find(owner); + it != state.structuredValues.end()) { + llvm::append_range(memrefs, it->second.memrefs); + llvm::append_range(qcQubits, it->second.qubits); + } else { + llvm::append_range(memrefs, state.regionRegisterMap[owner]); + llvm::append_range(qcQubits, state.regionQubitMap[owner]); + } } SmallVector qcoTargets; @@ -489,6 +517,34 @@ static void extractQubitsAfterOp(LoweringState& state, Operation* target, return qcoTargets; } +/** + * @brief Creates scalar scratch storage that is allocated once in the enclosing + * function. + * + * Hoisting the allocation outside enclosing loops lets result-bearing + * `scf.if` operations communicate classical branch results without growing the + * stack on every iteration. Each syntactic conditional gets distinct storage, + * and structured control flow executes it sequentially. + */ +[[nodiscard]] static FailureOr> +createHoistedScratchStorage(Operation* operation, TypeRange elementTypes, + ConversionPatternRewriter& rewriter) { + auto function = operation->getParentOfType(); + if (!function || function.getBody().empty()) { + return failure(); + } + + OpBuilder::InsertionGuard guard(rewriter); + rewriter.setInsertionPointToStart(&function.getBody().front()); + SmallVector storage; + storage.reserve(elementTypes.size()); + for (auto type : elementTypes) { + storage.push_back(memref::AllocaOp::create(rewriter, operation->getLoc(), + MemRefType::get({1}, type))); + } + return storage; +} + /** * @brief Collects all the QC qubit and memref references used by an scf * operation and stores them in the maps @@ -1239,38 +1295,47 @@ struct ConvertSCFForOp final : StatefulOpConversionPattern { // the ForOp insertQubitsBeforeOp(state, operation, rewriter); auto qcoTargets = resolveAllValues(state, operation); + const auto numOriginalResults = op.getNumResults(); + SmallVector initArgs(op.getInitArgs()); + llvm::append_range(initArgs, qcoTargets); // Create the new ForOp auto newForOp = scf::ForOp::create(rewriter, op.getLoc(), op.getLowerBound(), - op.getUpperBound(), op.getStep(), qcoTargets); + op.getUpperBound(), op.getStep(), initArgs); assignMappedTensors(state, op.getOperation(), registerMap, - newForOp.getResults().take_front(numRegisters)); + newForOp.getResults() + .drop_front(numOriginalResults) + .take_front(numRegisters)); assignMappedQubits(state, op.getOperation(), qubitMap, newForOp->getResults().take_back(numQubits)); - // Move the the contents from the old block into the new block + // Move the contents from the old block into the new block auto& srcBlock = op.getRegion().front(); auto& dstBlock = newForOp.getRegion().front(); dstBlock.getOperations().splice(dstBlock.end(), srcBlock.getOperations()); - rewriter.replaceAllUsesWith(op.getInductionVar(), - newForOp.getInductionVar()); + for (auto [oldArgument, newArgument] : llvm::zip_equal( + srcBlock.getArguments(), + dstBlock.getArguments().take_front(srcBlock.getNumArguments()))) { + rewriter.replaceAllUsesWith(oldArgument, newArgument); + } // Extract all the previously inserted qubits again rewriter.setInsertionPointAfter(newForOp); extractQubitsAfterOp(state, operation, rewriter); - auto qubits = qubitMap.getArrayRef(); - auto registers = registerMap.getArrayRef(); + SmallVector qubits(qubitMap.begin(), qubitMap.end()); + SmallVector memrefs(registerMap.begin(), registerMap.end()); + state.structuredValues[newForOp] = {.qubits = qubits, .memrefs = memrefs}; + seedRegionMappings(state, newForOp.getRegion(), qubits, memrefs, + dstBlock.getArguments().take_back(numQubits), + dstBlock.getArguments() + .drop_front(1 + numOriginalResults) + .take_front(numRegisters)); - // Push a new frame to the stack - pushModifierFrameWithRegisters( - state, qubits, registers, - dstBlock.getArguments().drop_front(1).take_back(numQubits), - dstBlock.getArguments().drop_front(1).take_front(numRegisters)); - - rewriter.eraseOp(op); + rewriter.replaceOp(op, + newForOp.getResults().take_front(numOriginalResults)); return success(); } @@ -1319,49 +1384,76 @@ struct ConvertSCFWhileOp final : StatefulOpConversionPattern { // the WhileOp insertQubitsBeforeOp(state, operation, rewriter); auto qcoTargets = resolveAllValues(state, operation); + const auto numOriginalResults = op.getNumResults(); + SmallVector initArgs(op.getInits()); + llvm::append_range(initArgs, qcoTargets); + SmallVector resultTypes(op.getResultTypes()); + llvm::append_range(resultTypes, + llvm::map_range(qcoTargets, [](Value value) { + return value.getType(); + })); // Create the new WhileOp - auto newWhileOp = scf::WhileOp::create( - rewriter, op.getLoc(), TypeRange(qcoTargets), ValueRange(qcoTargets)); + auto newWhileOp = + scf::WhileOp::create(rewriter, op.getLoc(), resultTypes, initArgs); assignMappedTensors(state, op.getOperation(), registerMap, - newWhileOp.getResults().take_front(numRegisters)); + newWhileOp.getResults() + .drop_front(numOriginalResults) + .take_front(numRegisters)); assignMappedQubits(state, op.getOperation(), qubitMap, newWhileOp->getResults().take_back(numQubits)); auto& newBeforeRegion = newWhileOp.getBefore(); auto& newAfterRegion = newWhileOp.getAfter(); - const SmallVector locs(qcoTargets.size(), op->getLoc()); + const SmallVector beforeLocs(initArgs.size(), op->getLoc()); + const SmallVector afterLocs(resultTypes.size(), op->getLoc()); // Create the new blocks and move the contents from the old blocks into the // new ones auto* newBeforeBlock = rewriter.createBlock( - &newBeforeRegion, {}, newWhileOp->getResultTypes(), locs); + &newBeforeRegion, {}, ValueRange(initArgs).getTypes(), beforeLocs); auto* newAfterBlock = rewriter.createBlock( - &newAfterRegion, {}, newWhileOp->getResultTypes(), locs); + &newAfterRegion, {}, TypeRange(resultTypes), afterLocs); + auto* oldBeforeBlock = op.getBeforeBody(); + auto* oldAfterBlock = op.getAfterBody(); newBeforeBlock->getOperations().splice(newBeforeBlock->end(), - op.getBeforeBody()->getOperations()); + oldBeforeBlock->getOperations()); newAfterBlock->getOperations().splice(newAfterBlock->end(), - op.getAfterBody()->getOperations()); + oldAfterBlock->getOperations()); + for (auto [oldArgument, newArgument] : + llvm::zip_equal(oldBeforeBlock->getArguments(), + newBeforeBlock->getArguments().take_front( + oldBeforeBlock->getNumArguments()))) { + rewriter.replaceAllUsesWith(oldArgument, newArgument); + } + for (auto [oldArgument, newArgument] : + llvm::zip_equal(oldAfterBlock->getArguments(), + newAfterBlock->getArguments().take_front( + oldAfterBlock->getNumArguments()))) { + rewriter.replaceAllUsesWith(oldArgument, newArgument); + } // Extract all the previously inserted qubits again rewriter.setInsertionPointAfter(newWhileOp); extractQubitsAfterOp(state, operation, rewriter); - auto qubits = qubitMap.getArrayRef(); - auto registers = registerMap.getArrayRef(); - - // Push the frames for the before and after region to the stack - pushModifierFrameWithRegisters( - state, qubits, registers, - newAfterBlock->getArguments().take_back(numQubits), - newAfterBlock->getArguments().take_front(numRegisters)); - pushModifierFrameWithRegisters( - state, qubits, registers, - newBeforeBlock->getArguments().take_back(numQubits), - newBeforeBlock->getArguments().take_front(numRegisters)); - - rewriter.eraseOp(op); + SmallVector qubits(qubitMap.begin(), qubitMap.end()); + SmallVector memrefs(registerMap.begin(), registerMap.end()); + state.structuredValues[newWhileOp] = {.qubits = qubits, .memrefs = memrefs}; + seedRegionMappings(state, newWhileOp.getBefore(), qubits, memrefs, + newBeforeBlock->getArguments().take_back(numQubits), + newBeforeBlock->getArguments() + .drop_front(numOriginalResults) + .take_front(numRegisters)); + seedRegionMappings(state, newWhileOp.getAfter(), qubits, memrefs, + newAfterBlock->getArguments().take_back(numQubits), + newAfterBlock->getArguments() + .drop_front(numOriginalResults) + .take_front(numRegisters)); + + rewriter.replaceOp(op, + newWhileOp.getResults().take_front(numOriginalResults)); return success(); } }; @@ -1402,6 +1494,12 @@ struct ConvertSCFIfOp final : StatefulOpConversionPattern { // the IfOp insertQubitsBeforeOp(state, operation, rewriter); auto qcoTargets = resolveAllValues(state, operation); + auto scratch = + createHoistedScratchStorage(operation, op.getResultTypes(), rewriter); + if (failed(scratch)) { + return rewriter.notifyMatchFailure( + op, "result-bearing scf.if requires an automatic allocation scope"); + } // Create the new IfOp auto newIfOp = @@ -1430,16 +1528,32 @@ struct ConvertSCFIfOp final : StatefulOpConversionPattern { thenBlock->getOperations().splice( thenBlock->end(), op.getThenRegion().front().getOperations()); - auto qubits = qubitMap.getArrayRef(); - auto registers = registerMap.getArrayRef(); + const auto spillResults = [&](Block* block) { + auto yield = cast(block->getTerminator()); + rewriter.setInsertionPoint(yield); + if (!scratch->empty()) { + auto index = arith::ConstantIndexOp::create(rewriter, op.getLoc(), 0); + for (auto [value, storage] : + llvm::zip_equal(yield.getResults(), *scratch)) { + memref::StoreOp::create(rewriter, op.getLoc(), value, storage, + ValueRange{index}); + } + } + rewriter.replaceOpWithNewOp(yield); + }; + spillResults(thenBlock); + + SmallVector qubits(qubitMap.begin(), qubitMap.end()); + SmallVector memrefs(registerMap.begin(), registerMap.end()); + state.structuredValues[newIfOp] = {.qubits = qubits, .memrefs = memrefs}; if (!op.getElseRegion().empty()) { elseBlock->getOperations().splice( elseBlock->end(), op.getElseRegion().front().getOperations()); - pushModifierFrameWithRegisters( - state, qubits, registers, - elseBlock->getArguments().take_back(numQubits), - elseBlock->getArguments().take_front(numRegisters)); + spillResults(elseBlock); + seedRegionMappings(state, newIfOp.getElseRegion(), qubits, memrefs, + elseBlock->getArguments().take_back(numQubits), + elseBlock->getArguments().take_front(numRegisters)); } else { // If the else block is empty, just create the new qco::YieldOp @@ -1447,12 +1561,21 @@ struct ConvertSCFIfOp final : StatefulOpConversionPattern { qco::YieldOp::create(rewriter, op->getLoc(), elseBlock->getArguments()); } - pushModifierFrameWithRegisters( - state, qubits, registers, - thenBlock->getArguments().take_back(numQubits), - thenBlock->getArguments().take_front(numRegisters)); + seedRegionMappings(state, newIfOp.getThenRegion(), qubits, memrefs, + thenBlock->getArguments().take_back(numQubits), + thenBlock->getArguments().take_front(numRegisters)); - rewriter.eraseOp(op); + rewriter.setInsertionPointAfter(newIfOp); + SmallVector classicalResults; + classicalResults.reserve(scratch->size()); + if (!scratch->empty()) { + auto index = arith::ConstantIndexOp::create(rewriter, op.getLoc(), 0); + for (auto storage : *scratch) { + classicalResults.push_back(memref::LoadOp::create( + rewriter, op.getLoc(), storage, ValueRange{index})); + } + } + rewriter.replaceOp(op, classicalResults); return success(); } }; @@ -1480,7 +1603,8 @@ struct ConvertSCFYieldOp final : StatefulOpConversionPattern { auto* operation = op.getOperation(); insertQubitsBeforeOp(state, op.getOperation(), rewriter); - SmallVector targets = resolveAllValues(state, operation); + SmallVector targets(op.getResults()); + llvm::append_range(targets, resolveAllValues(state, operation)); if (isa(op->getParentOp())) { rewriter.replaceOpWithNewOp(op, targets); @@ -1488,8 +1612,6 @@ struct ConvertSCFYieldOp final : StatefulOpConversionPattern { rewriter.replaceOpWithNewOp(op, targets); } - popModifierFrame(state); - return success(); } }; @@ -1518,12 +1640,11 @@ struct ConvertSCFConditionOp final auto* operation = op.getOperation(); insertQubitsBeforeOp(state, op.getOperation(), rewriter); - SmallVector targets = resolveAllValues(state, operation); + SmallVector targets(op.getArgs()); + llvm::append_range(targets, resolveAllValues(state, operation)); rewriter.replaceOpWithNewOp(op, op.getCondition(), targets); - popModifierFrame(state); - return success(); } }; @@ -1583,33 +1704,21 @@ struct QCToQCO final : impl::QCToQCOBase { auto& regionTensorMap = state.regionRegisterMap[op]; return regionQubitMap.empty() && regionTensorMap.empty(); }); - target.addDynamicallyLegalOp( - [](Operation* op) { - auto* parentOp = op->getParentOp(); - auto isQCOType = [](Type t) { - return TypeSwitch(t) - .Case([](auto) { return true; }) - .Case([](RankedTensorType t) { - return isa(t.getElementType()); - }) - .Default([](auto) { return false; }); - }; - - const auto parentHasQubitTypes = - llvm::any_of(parentOp->getOperandTypes(), isQCOType); - const auto terminatorHasQubitTypes = - llvm::any_of(op->getOperandTypes(), isQCOType); - return !parentHasQubitTypes || terminatorHasQubitTypes; - }); + // Structured terminators are lowered in a second conversion phase. The + // first phase must finish converting every operation in a region so the + // region-local maps contain the final QCO values that the terminator has + // to yield. Converting terminators in the same worklist would make the + // result depend on dialect-conversion traversal order. + target.addLegalOp(); // Register operation conversion patterns with state tracking. - patterns.add(typeConverter, context, - &state); + patterns + .add( + typeConverter, context, &state); // Not part of the central gate table. patterns.add>( @@ -1652,9 +1761,35 @@ struct QCToQCO final : impl::QCToQCOBase { // Conversion of qc types in control-flow ops (e.g., cf.br, cf.cond_br) populateBranchOpInterfaceTypeConversionPattern(patterns, typeConverter); - // Apply the conversion + // Convert structured parents and their contents first. if (failed(applyPartialConversion(module, target, std::move(patterns)))) { signalPassFailure(); + return; + } + + ConversionTarget terminatorTarget(*context); + terminatorTarget.markUnknownOpDynamicallyLegal( + [](Operation*) { return true; }); + terminatorTarget.addDynamicallyLegalOp( + [&](Operation* op) { + auto* parentOp = op->getParentOp(); + if (!state.structuredValues.contains(parentOp)) { + return true; + } + + const auto expectedTypes = parentOp->getResultTypes(); + if (auto condition = dyn_cast(op)) { + return llvm::equal(condition.getArgs().getTypes(), expectedTypes); + } + return llvm::equal(op->getOperandTypes(), expectedTypes); + }); + + RewritePatternSet terminatorPatterns(context); + terminatorPatterns.add( + typeConverter, context, &state); + if (failed(applyPartialConversion(module, terminatorTarget, + std::move(terminatorPatterns)))) { + signalPassFailure(); } } }; diff --git a/mlir/lib/Conversion/QCToQIR/QIRAdaptive/QCToQIRAdaptive.cpp b/mlir/lib/Conversion/QCToQIR/QIRAdaptive/QCToQIRAdaptive.cpp index 80a30c19bd..e03509d52c 100644 --- a/mlir/lib/Conversion/QCToQIR/QIRAdaptive/QCToQIRAdaptive.cpp +++ b/mlir/lib/Conversion/QCToQIR/QIRAdaptive/QCToQIRAdaptive.cpp @@ -685,6 +685,7 @@ struct QCToQIRAdaptive final : impl::QCToQIRAdaptiveBase { cf::populateControlFlowToLLVMConversionPatterns(typeConverter, stdPatterns); + cf::populateAssertToLLVMConversionPattern(typeConverter, stdPatterns); arith::populateArithToLLVMConversionPatterns(typeConverter, stdPatterns); if (applyPartialConversion(moduleOp, target, std::move(stdPatterns)) diff --git a/mlir/lib/Conversion/QCToQIR/QIRBase/QCToQIRBase.cpp b/mlir/lib/Conversion/QCToQIR/QIRBase/QCToQIRBase.cpp index 1e5a392a92..bd86d80b04 100644 --- a/mlir/lib/Conversion/QCToQIR/QIRBase/QCToQIRBase.cpp +++ b/mlir/lib/Conversion/QCToQIR/QIRBase/QCToQIRBase.cpp @@ -467,6 +467,7 @@ struct QCToQIRBase final : impl::QCToQIRBaseBase { cf::populateControlFlowToLLVMConversionPatterns(typeConverter, stdPatterns); + cf::populateAssertToLLVMConversionPattern(typeConverter, stdPatterns); arith::populateArithToLLVMConversionPatterns(typeConverter, stdPatterns); if (applyPartialConversion(moduleOp, target, std::move(stdPatterns)) diff --git a/mlir/lib/Dialect/QC/Translation/CMakeLists.txt b/mlir/lib/Dialect/QC/Translation/CMakeLists.txt index c1d403de3a..cffc0920e9 100644 --- a/mlir/lib/Dialect/QC/Translation/CMakeLists.txt +++ b/mlir/lib/Dialect/QC/Translation/CMakeLists.txt @@ -8,16 +8,21 @@ add_mlir_library( MLIRQCTranslation + OpenQASMToQCEmitter.cpp TranslateQuantumComputationToQC.cpp TranslateQASM3ToQC.cpp LINK_LIBS MLIRArithDialect MLIRFuncDialect + MLIROpenQASMFrontend + MLIRControlFlowDialect + MLIRMathDialect + MLIRMemRefDialect MLIRSCFDialect + MLIRUBDialect MLIRQCDialect MLIRQCProgramBuilder - MQT::CoreIR - MQT::CoreQASM) + MQT::CoreIR) mqt_mlir_target_use_project_options(MLIRQCTranslation) diff --git a/mlir/lib/Dialect/QC/Translation/OpenQASMToQCEmitter.cpp b/mlir/lib/Dialect/QC/Translation/OpenQASMToQCEmitter.cpp new file mode 100644 index 0000000000..33f83e20b4 --- /dev/null +++ b/mlir/lib/Dialect/QC/Translation/OpenQASMToQCEmitter.cpp @@ -0,0 +1,1800 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#include "OpenQASMToQCEmitter.h" + +#include "mlir/Dialect/QC/Builder/QCProgramBuilder.h" +#include "mlir/Dialect/QC/IR/QCDialect.h" +#include "mlir/Dialect/QC/IR/QCOps.h" +#include "mlir/Target/OpenQASM/GateCatalog.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace mlir::qc::detail { +namespace { + +namespace frontend = oq3::frontend; +using oq3::frontend::GateCatalogEntry; + +class OpenQASMToQCEmitter { +public: + OpenQASMToQCEmitter(const oq3::frontend::TypedProgram& typedProgram, + MLIRContext& mlirContext) + : program(typedProgram), context(mlirContext), builder(&context), + registerValues(program.registers.size()), + classicalRegisters(program.registers.size()), + bitValues(program.registers.size()), + scalarValues(program.scalars.size()) { + context + .loadDialect(); + builder.initialize(); + } + + OwningOpRef emit() { + if (!preflight()) { + return nullptr; + } + for (const auto statement : program.body) { + emitStatement(statement, {}, {}); + } + if (emissionFailed) { + return nullptr; + } + + SmallVector results; + for (const auto output : program.outputs) { + for (auto bit : bitValues[output]) { + if (!bit) { + llvm::errs() << "OpenQASM emission error: output register '" + << program.registers[output].name + << "' is not fully initialized.\n"; + return nullptr; + } + results.push_back(bit); + } + } + if (results.empty()) { + return builder.finalize(); + } + builder.retype(ValueRange(results).getTypes()); + return builder.finalize(results); + } + +private: + const oq3::frontend::TypedProgram& program; + MLIRContext& context; + qc::QCProgramBuilder builder; + std::vector> registerValues; + std::vector> + classicalRegisters; + std::vector> bitValues; + std::vector scalarValues; + DenseMap + structuredGateCapabilities; + bool emissionFailed = false; + + enum class StateKind : std::uint8_t { Scalar, Bit }; + + struct StateSlot { + StateKind kind = StateKind::Scalar; + std::uint32_t first = 0; + std::uint32_t second = 0; + }; + + [[nodiscard]] Location + getLocation(const frontend::SourceLocation& source) const { + return FileLineColLoc::get(&context, source.filename, source.line, + source.column); + } + + static constexpr std::size_t projectedEmissionLimit = 100000; + + [[nodiscard]] const oq3::frontend::GateDefinition* + findCustomGate(const StringRef name) const { + const auto found = llvm::find_if( + program.gates, [&](const auto& gate) { return gate.name == name; }); + return found == program.gates.end() ? nullptr : &*found; + } + + [[nodiscard]] bool statementsRequireStructuredControlFlow( + const ArrayRef statements) { + return llvm::any_of(statements, [&](const auto id) { + const auto& data = program.statements.at(id).data; + if (std::holds_alternative(data) || + std::holds_alternative(data) || + std::holds_alternative(data)) { + return true; + } + const auto* application = + std::get_if(&data); + const auto* callee = application == nullptr + ? nullptr + : findCustomGate(application->callee); + return callee != nullptr && gateRequiresStructuredControlFlow(*callee); + }); + } + + [[nodiscard]] bool + gateRequiresStructuredControlFlow(const oq3::frontend::GateDefinition& gate) { + if (const auto it = structuredGateCapabilities.find(&gate); + it != structuredGateCapabilities.end()) { + return it->second; + } + const bool requiresStructuredControlFlow = + statementsRequireStructuredControlFlow(gate.body); + structuredGateCapabilities[&gate] = requiresStructuredControlFlow; + return requiresStructuredControlFlow; + } + + [[nodiscard]] std::optional + staticCondition(const frontend::ConditionId id) const { + const auto& condition = program.conditions.at(id); + if (condition.kind == frontend::ConditionKind::Literal) { + return condition.literal; + } + return std::nullopt; + } + + [[nodiscard]] bool reportProjectedEmissionLimit( + const oq3::frontend::SourceLocation& source) const { + llvm::errs() << source.filename << ':' << source.line << ':' + << source.column + << ": OpenQASM QC emission error: projected emitted " + "operation count exceeds the safe lowering limit.\n"; + return false; + } + + [[nodiscard]] bool + projectedMultiplicity(const ArrayRef references, + const std::size_t parentMultiplicity, + const oq3::frontend::SourceLocation& source, + std::size_t& result) const { + result = parentMultiplicity; + for (const auto& reference : references) { + if (!reference.dynamicIndex) { + continue; + } + const auto width = static_cast( + program.registers.at(reference.symbol).width); + if (width != 0 && result > projectedEmissionLimit / width) { + return reportProjectedEmissionLimit(source); + } + result *= width; + } + return true; + } + + [[nodiscard]] bool + chargeProjectedEmission(const std::size_t amount, + std::size_t& projectedEmission, + const oq3::frontend::SourceLocation& source) const { + if (amount > projectedEmissionLimit - projectedEmission) { + return reportProjectedEmissionLimit(source); + } + projectedEmission += amount; + return true; + } + + [[nodiscard]] bool + chargeConditionEmission(const frontend::ConditionId id, + const std::size_t multiplicity, + std::size_t& projectedEmission, + const oq3::frontend::SourceLocation& source) const { + const auto& condition = program.conditions.at(id); + if (condition.kind == frontend::ConditionKind::Measurement) { + std::size_t operationMultiplicity = 0; + return projectedMultiplicity({condition.measurement}, multiplicity, + source, operationMultiplicity) && + chargeProjectedEmission(operationMultiplicity, projectedEmission, + source); + } + if (condition.kind == frontend::ConditionKind::Not) { + return chargeConditionEmission(condition.lhs, multiplicity, + projectedEmission, source); + } + if (condition.kind == frontend::ConditionKind::And || + condition.kind == frontend::ConditionKind::Or) { + return chargeConditionEmission(condition.lhs, multiplicity, + projectedEmission, source) && + chargeConditionEmission(condition.rhs, multiplicity, + projectedEmission, source); + } + return true; + } + + [[nodiscard]] bool + preflightStatements(const ArrayRef statements, + std::size_t& projectedEmission, + const std::size_t multiplicity = 1) { + for (const auto id : statements) { + const auto& statement = program.statements.at(id); + const auto* application = + std::get_if(&statement.data); + if (application == nullptr) { + if (const auto* conditional = + std::get_if(&statement.data)) { + if (!chargeConditionEmission(conditional->condition, multiplicity, + projectedEmission, statement.location)) { + return false; + } + if (const auto selected = staticCondition(conditional->condition)) { + const auto& selectedStatements = *selected + ? conditional->thenStatements + : conditional->elseStatements; + if (!preflightStatements(selectedStatements, projectedEmission, + multiplicity)) { + return false; + } + continue; + } + if (!preflightStatements(conditional->thenStatements, + projectedEmission, multiplicity) || + !preflightStatements(conditional->elseStatements, + projectedEmission, multiplicity)) { + return false; + } + } else if (const auto* loop = std::get_if( + &statement.data)) { + if (!preflightStatements(loop->body, projectedEmission, + multiplicity)) { + return false; + } + } else if (const auto* loop = + std::get_if( + &statement.data)) { + if (!chargeConditionEmission(loop->condition, multiplicity, + projectedEmission, statement.location)) { + return false; + } + if (!preflightStatements(loop->body, projectedEmission, + multiplicity)) { + return false; + } + } else if (const auto* declaration = + std::get_if( + &statement.data)) { + if (declaration->conditionInitializer && + !chargeConditionEmission(*declaration->conditionInitializer, + multiplicity, projectedEmission, + statement.location)) { + return false; + } + } else if (const auto* assignment = + std::get_if( + &statement.data)) { + if (assignment->condition && + !chargeConditionEmission(*assignment->condition, multiplicity, + projectedEmission, statement.location)) { + return false; + } + } else if (const auto* assignment = + std::get_if( + &statement.data)) { + if (!chargeConditionEmission(assignment->value, multiplicity, + projectedEmission, statement.location)) { + return false; + } + } else if (const auto* measurement = + std::get_if( + &statement.data)) { + for (const auto& qubit : measurement->qubits) { + std::size_t operationMultiplicity = 0; + if (!projectedMultiplicity({qubit}, multiplicity, + statement.location, + operationMultiplicity) || + !chargeProjectedEmission(operationMultiplicity, + projectedEmission, + statement.location)) { + return false; + } + } + } else if (const auto* reset = + std::get_if(&statement.data)) { + for (const auto& qubit : reset->qubits) { + std::size_t operationMultiplicity = 0; + if (!projectedMultiplicity({qubit}, multiplicity, + statement.location, + operationMultiplicity) || + !chargeProjectedEmission(operationMultiplicity, + projectedEmission, + statement.location)) { + return false; + } + } + } else if (const auto* barrier = + std::get_if( + &statement.data)) { + std::size_t operationMultiplicity = 0; + if (!projectedMultiplicity(barrier->qubits, multiplicity, + statement.location, + operationMultiplicity) || + !chargeProjectedEmission(operationMultiplicity, projectedEmission, + statement.location)) { + return false; + } + } + continue; + } + for (const auto& modifier : application->modifiers) { + if (modifier.kind == oq3::frontend::ModifierKind::Pow) { + const auto& source = statement.location; + llvm::errs() << source.filename << ':' << source.line << ':' + << source.column + << ": OpenQASM QC emission error: power gate modifiers " + "are not supported by the QC dialect.\n"; + return false; + } + } + std::size_t operationMultiplicity = 0; + if (!projectedMultiplicity(application->qubits, multiplicity, + statement.location, operationMultiplicity)) { + return false; + } + const auto* gate = findCustomGate(application->callee); + if (gate == nullptr) { + if (!chargeProjectedEmission(operationMultiplicity, projectedEmission, + statement.location)) { + return false; + } + continue; + } + if (!application->modifiers.empty() && + gateRequiresStructuredControlFlow(*gate)) { + const auto& source = statement.location; + llvm::errs() << source.filename << ':' << source.line << ':' + << source.column + << ": OpenQASM QC emission error: modifiers on custom " + "gates with structured control flow are not supported " + "by the QC dialect.\n"; + return false; + } + if (!preflightStatements(gate->body, projectedEmission, + operationMultiplicity)) { + return false; + } + } + return true; + } + + [[nodiscard]] bool preflight() { + std::size_t projectedEmission = 0; + return preflightStatements(program.body, projectedEmission); + } + + [[nodiscard]] Value checkedSignedResult(OpBuilder& opBuilder, Location loc, + Value wide, const StringRef message) { + auto i128 = opBuilder.getIntegerType(128); + auto minimum = arith::ConstantIntOp::create( + opBuilder, loc, i128, std::numeric_limits::min()); + auto maximum = arith::ConstantIntOp::create( + opBuilder, loc, i128, std::numeric_limits::max()); + auto aboveMinimum = arith::CmpIOp::create( + opBuilder, loc, arith::CmpIPredicate::sge, wide, minimum); + auto belowMaximum = arith::CmpIOp::create( + opBuilder, loc, arith::CmpIPredicate::sle, wide, maximum); + auto fits = + arith::AndIOp::create(opBuilder, loc, aboveMinimum, belowMaximum); + cf::AssertOp::create(opBuilder, loc, fits, message); + return arith::TruncIOp::create(opBuilder, loc, opBuilder.getI64Type(), + wide); + } + + [[nodiscard]] Value conditionalIntegerMultiply(OpBuilder& opBuilder, + Location loc, Value condition, + Value lhs, Value rhs, + const bool isUnsigned) { + if (isUnsigned) { + auto product = arith::MulIOp::create(opBuilder, loc, lhs, rhs); + return arith::SelectOp::create(opBuilder, loc, condition, product, lhs); + } + auto i128 = opBuilder.getIntegerType(128); + auto lhsWide = arith::ExtSIOp::create(opBuilder, loc, i128, lhs); + auto rhsWide = arith::ExtSIOp::create(opBuilder, loc, i128, rhs); + auto productWide = arith::MulIOp::create(opBuilder, loc, lhsWide, rhsWide); + auto minimum = arith::ConstantIntOp::create( + opBuilder, loc, i128, std::numeric_limits::min()); + auto maximum = arith::ConstantIntOp::create( + opBuilder, loc, i128, std::numeric_limits::max()); + auto aboveMinimum = arith::CmpIOp::create( + opBuilder, loc, arith::CmpIPredicate::sge, productWide, minimum); + auto belowMaximum = arith::CmpIOp::create( + opBuilder, loc, arith::CmpIPredicate::sle, productWide, maximum); + auto fits = + arith::AndIOp::create(opBuilder, loc, aboveMinimum, belowMaximum); + auto notRequired = arith::XOrIOp::create( + opBuilder, loc, condition, + arith::ConstantIntOp::create(opBuilder, loc, 1, 1)); + auto valid = arith::OrIOp::create(opBuilder, loc, notRequired, fits); + cf::AssertOp::create(opBuilder, loc, valid, "integer power overflows i64"); + auto product = arith::TruncIOp::create(opBuilder, loc, + opBuilder.getI64Type(), productWide); + return arith::SelectOp::create(opBuilder, loc, condition, product, lhs); + } + + [[nodiscard]] Value emitIntegerPower(OpBuilder& opBuilder, Location loc, + Value base, Value exponent, + const bool isUnsigned) { + auto zero = arith::ConstantIntOp::create(opBuilder, loc, 0, 64); + auto one = arith::ConstantIntOp::create(opBuilder, loc, 1, 64); + if (!isUnsigned) { + auto nonnegative = arith::CmpIOp::create( + opBuilder, loc, arith::CmpIPredicate::sge, exponent, zero); + cf::AssertOp::create(opBuilder, loc, nonnegative, + "integer power requires a nonnegative exponent"); + } + auto power = scf::WhileOp::create( + opBuilder, loc, + TypeRange{base.getType(), base.getType(), exponent.getType()}, + ValueRange{one, base, exponent}, + [&](OpBuilder& nested, Location nestedLoc, ValueRange arguments) { + auto active = arith::CmpIOp::create( + nested, nestedLoc, arith::CmpIPredicate::ne, arguments[2], zero); + scf::ConditionOp::create(nested, nestedLoc, active, arguments); + }, + [&](OpBuilder& nested, Location nestedLoc, ValueRange arguments) { + auto lowBit = + arith::AndIOp::create(nested, nestedLoc, arguments[2], one); + auto odd = arith::CmpIOp::create( + nested, nestedLoc, arith::CmpIPredicate::ne, lowBit, zero); + auto nextResult = conditionalIntegerMultiply( + nested, nestedLoc, odd, arguments[0], arguments[1], isUnsigned); + auto nextExponent = + arith::ShRUIOp::create(nested, nestedLoc, arguments[2], one); + auto squareBase = arith::CmpIOp::create( + nested, nestedLoc, arith::CmpIPredicate::ne, nextExponent, zero); + auto nextBase = conditionalIntegerMultiply(nested, nestedLoc, + squareBase, arguments[1], + arguments[1], isUnsigned); + scf::YieldOp::create(nested, nestedLoc, + ValueRange{nextResult, nextBase, nextExponent}); + }); + return power.getResult(0); + } + + Value emitExpression(OpBuilder& opBuilder, const frontend::ExpressionId id, + ValueRange gateParameters) { + const auto& expression = program.expressions.at(id); + auto loc = opBuilder.getInsertionPoint() == opBuilder.getBlock()->end() + ? opBuilder.getUnknownLoc() + : opBuilder.getInsertionPoint()->getLoc(); + switch (expression.kind) { + case frontend::ExpressionKind::Constant: + switch (expression.type) { + case frontend::ScalarType::Bool: + return arith::ConstantIntOp::create( + opBuilder, loc, std::get(expression.constant), 1); + case frontend::ScalarType::Int: + return arith::ConstantIntOp::create( + opBuilder, loc, std::get(expression.constant), 64); + case frontend::ScalarType::Uint: + return arith::ConstantOp::create( + opBuilder, loc, + IntegerAttr::get(opBuilder.getI64Type(), + APInt(64, + std::get(expression.constant), + /*isSigned=*/false))); + case frontend::ScalarType::Float: + return arith::ConstantFloatOp::create( + opBuilder, loc, opBuilder.getF64Type(), + APFloat(std::get(expression.constant))); + } + llvm_unreachable("unknown scalar type"); + case frontend::ExpressionKind::GateParameter: + return gateParameters[expression.parameter]; + case frontend::ExpressionKind::Variable: + return scalarValues.at(expression.variable); + case frontend::ExpressionKind::Negate: { + auto operand = emitExpression(opBuilder, expression.lhs, gateParameters); + if (isa(operand.getType())) { + return arith::NegFOp::create(opBuilder, loc, operand); + } + if (expression.type == frontend::ScalarType::Uint) { + auto zero = arith::ConstantIntOp::create(opBuilder, loc, 0, 64); + return arith::SubIOp::create(opBuilder, loc, zero, operand); + } + auto i128 = opBuilder.getIntegerType(128); + auto zero = arith::ConstantIntOp::create(opBuilder, loc, 0, 128); + auto operandWide = arith::ExtSIOp::create(opBuilder, loc, i128, operand); + auto negated = arith::SubIOp::create(opBuilder, loc, zero, operandWide); + return checkedSignedResult(opBuilder, loc, negated, + "integer negation overflows i64"); + } + case frontend::ExpressionKind::ArcCos: + case frontend::ExpressionKind::ArcSin: + case frontend::ExpressionKind::ArcTan: + case frontend::ExpressionKind::Sin: + case frontend::ExpressionKind::Cos: + case frontend::ExpressionKind::Tan: + case frontend::ExpressionKind::Exp: + case frontend::ExpressionKind::Ln: + case frontend::ExpressionKind::Sqrt: { + Value operand = emitExpression(opBuilder, expression.lhs, gateParameters); + if (isa(operand.getType())) { + const auto sourceType = program.expressions.at(expression.lhs).type; + if (sourceType == frontend::ScalarType::Uint) { + operand = arith::UIToFPOp::create(opBuilder, loc, + opBuilder.getF64Type(), operand); + } else { + operand = arith::SIToFPOp::create(opBuilder, loc, + opBuilder.getF64Type(), operand); + } + } + switch (expression.kind) { + case frontend::ExpressionKind::ArcCos: + return math::AcosOp::create(opBuilder, loc, operand); + case frontend::ExpressionKind::ArcSin: + return math::AsinOp::create(opBuilder, loc, operand); + case frontend::ExpressionKind::ArcTan: + return math::AtanOp::create(opBuilder, loc, operand); + case frontend::ExpressionKind::Sin: + return math::SinOp::create(opBuilder, loc, operand); + case frontend::ExpressionKind::Cos: + return math::CosOp::create(opBuilder, loc, operand); + case frontend::ExpressionKind::Tan: + return math::TanOp::create(opBuilder, loc, operand); + case frontend::ExpressionKind::Exp: + return math::ExpOp::create(opBuilder, loc, operand); + case frontend::ExpressionKind::Ln: + return math::LogOp::create(opBuilder, loc, operand); + case frontend::ExpressionKind::Sqrt: + return math::SqrtOp::create(opBuilder, loc, operand); + default: + llvm_unreachable("unknown scalar math function"); + } + } + case frontend::ExpressionKind::Add: + case frontend::ExpressionKind::Subtract: + case frontend::ExpressionKind::Multiply: + case frontend::ExpressionKind::Divide: + case frontend::ExpressionKind::Modulo: + case frontend::ExpressionKind::Power: { + auto lhs = emitExpression(opBuilder, expression.lhs, gateParameters); + auto rhs = emitExpression(opBuilder, expression.rhs, gateParameters); + if (expression.type != frontend::ScalarType::Float) { + const bool isUnsigned = expression.type == frontend::ScalarType::Uint; + auto zero = arith::ConstantIntOp::create(opBuilder, loc, 0, 64); + if (expression.kind == frontend::ExpressionKind::Divide || + expression.kind == frontend::ExpressionKind::Modulo) { + auto nonzero = arith::CmpIOp::create( + opBuilder, loc, arith::CmpIPredicate::ne, rhs, zero); + cf::AssertOp::create(opBuilder, loc, nonzero, + expression.kind == + frontend::ExpressionKind::Divide + ? "division by zero" + : "modulo by zero"); + if (!isUnsigned) { + auto minimum = arith::ConstantIntOp::create( + opBuilder, loc, std::numeric_limits::min(), 64); + auto minusOne = + arith::ConstantIntOp::create(opBuilder, loc, -1, 64); + auto lhsIsMinimum = arith::CmpIOp::create( + opBuilder, loc, arith::CmpIPredicate::eq, lhs, minimum); + auto rhsIsMinusOne = arith::CmpIOp::create( + opBuilder, loc, arith::CmpIPredicate::eq, rhs, minusOne); + auto overflows = arith::AndIOp::create(opBuilder, loc, lhsIsMinimum, + rhsIsMinusOne); + auto valid = arith::XOrIOp::create( + opBuilder, loc, overflows, + arith::ConstantIntOp::create(opBuilder, loc, 1, 1)); + cf::AssertOp::create(opBuilder, loc, valid, + "integer division overflows i64"); + } + if (expression.kind == frontend::ExpressionKind::Divide) { + return isUnsigned ? arith::DivUIOp::create(opBuilder, loc, lhs, rhs) + .getResult() + : arith::DivSIOp::create(opBuilder, loc, lhs, rhs) + .getResult(); + } + return isUnsigned ? arith::RemUIOp::create(opBuilder, loc, lhs, rhs) + .getResult() + : arith::RemSIOp::create(opBuilder, loc, lhs, rhs) + .getResult(); + } + if (expression.kind == frontend::ExpressionKind::Power) { + return emitIntegerPower(opBuilder, loc, lhs, rhs, isUnsigned); + } + if (isUnsigned) { + switch (expression.kind) { + case frontend::ExpressionKind::Add: + return arith::AddIOp::create(opBuilder, loc, lhs, rhs); + case frontend::ExpressionKind::Subtract: + return arith::SubIOp::create(opBuilder, loc, lhs, rhs); + case frontend::ExpressionKind::Multiply: + return arith::MulIOp::create(opBuilder, loc, lhs, rhs); + default: + llvm_unreachable("not an unsigned integer binary expression"); + } + } + auto i128 = opBuilder.getIntegerType(128); + auto lhsWide = arith::ExtSIOp::create(opBuilder, loc, i128, lhs); + auto rhsWide = arith::ExtSIOp::create(opBuilder, loc, i128, rhs); + Value result; + switch (expression.kind) { + case frontend::ExpressionKind::Add: + result = arith::AddIOp::create(opBuilder, loc, lhsWide, rhsWide); + break; + case frontend::ExpressionKind::Subtract: + result = arith::SubIOp::create(opBuilder, loc, lhsWide, rhsWide); + break; + case frontend::ExpressionKind::Multiply: + result = arith::MulIOp::create(opBuilder, loc, lhsWide, rhsWide); + break; + default: + llvm_unreachable("not a signed integer binary expression"); + } + return checkedSignedResult(opBuilder, loc, result, + "integer arithmetic overflows i64"); + } + const auto toFloat = [&](Value value, + const frontend::ScalarType sourceType) { + if (isa(value.getType())) { + return value; + } + if (sourceType == frontend::ScalarType::Uint) { + return arith::UIToFPOp::create(opBuilder, loc, opBuilder.getF64Type(), + value) + .getResult(); + } + return arith::SIToFPOp::create(opBuilder, loc, opBuilder.getF64Type(), + value) + .getResult(); + }; + auto floatLhs = toFloat(lhs, program.expressions.at(expression.lhs).type); + auto floatRhs = toFloat(rhs, program.expressions.at(expression.rhs).type); + switch (expression.kind) { + case frontend::ExpressionKind::Add: + return arith::AddFOp::create(opBuilder, loc, floatLhs, floatRhs); + case frontend::ExpressionKind::Subtract: + return arith::SubFOp::create(opBuilder, loc, floatLhs, floatRhs); + case frontend::ExpressionKind::Multiply: + return arith::MulFOp::create(opBuilder, loc, floatLhs, floatRhs); + case frontend::ExpressionKind::Divide: + return arith::DivFOp::create(opBuilder, loc, floatLhs, floatRhs); + case frontend::ExpressionKind::Modulo: + return arith::RemFOp::create(opBuilder, loc, floatLhs, floatRhs); + case frontend::ExpressionKind::Power: + return math::PowFOp::create(opBuilder, loc, floatLhs, floatRhs); + default: + llvm_unreachable("not a floating-point binary expression"); + } + } + } + llvm_unreachable("unknown scalar expression kind"); + } + + [[nodiscard]] Value emitCheckedIndex(const frontend::ExpressionId expression, + const std::int64_t width, + const llvm::StringRef message) { + auto index = emitExpression(builder, expression, {}); + auto zero = builder.intConstant(0); + auto upper = builder.intConstant(width); + Value inBounds; + if (program.expressions.at(expression).type == frontend::ScalarType::Uint) { + inBounds = arith::CmpIOp::create(builder, arith::CmpIPredicate::ult, + index, upper); + } else { + auto negative = arith::CmpIOp::create(builder, arith::CmpIPredicate::slt, + index, zero); + auto wrapped = arith::AddIOp::create(builder, index, upper); + index = arith::SelectOp::create(builder, negative, wrapped, index); + auto nonnegative = arith::CmpIOp::create( + builder, arith::CmpIPredicate::sge, index, zero); + auto belowWidth = arith::CmpIOp::create( + builder, arith::CmpIPredicate::slt, index, upper); + inBounds = arith::AndIOp::create(builder, nonnegative, belowWidth); + } + cf::AssertOp::create(builder, inBounds, message); + return index; + } + + Value resolveQubit(const frontend::QubitReference& reference, + ValueRange gateQubits) { + switch (reference.kind) { + case frontend::QubitReferenceKind::Register: { + assert(!reference.dynamicIndex && + "dynamic qubit references require structured dispatch"); + return registerValues.at(reference.symbol)[reference.index]; + } + case frontend::QubitReferenceKind::GateArgument: + return gateQubits[reference.symbol]; + case frontend::QubitReferenceKind::Hardware: + return builder.staticQubit(reference.index); + } + llvm_unreachable("unknown qubit reference kind"); + } + + [[nodiscard]] SmallVector + emitDynamicQubitIndices(ArrayRef references) { + SmallVector indices(references.size()); + for (const auto [position, reference] : llvm::enumerate(references)) { + if (!reference.dynamicIndex) { + continue; + } + const auto width = static_cast( + program.registers.at(reference.symbol).width); + indices[position] = emitCheckedIndex(*reference.dynamicIndex, width, + "dynamic qubit index out of bounds"); + } + return indices; + } + + void + dispatchQubits(ArrayRef references, + ValueRange gateQubits, ValueRange dynamicIndices, + llvm::function_ref emitResolvedOperation) { + SmallVector resolved(references.size()); + std::function resolveAt; + resolveAt = [&](const std::size_t position) { + if (position == references.size()) { + emitResolvedOperation(resolved); + return; + } + + const auto& reference = references[position]; + if (!reference.dynamicIndex) { + resolved[position] = resolveQubit(reference, gateQubits); + resolveAt(position + 1); + return; + } + + const auto& qubits = registerValues.at(reference.symbol); + std::function emitCase; + emitCase = [&](const std::size_t candidate) { + if (candidate + 1 == qubits.size()) { + resolved[position] = qubits[candidate]; + resolveAt(position + 1); + return; + } + auto matches = arith::CmpIOp::create( + builder, arith::CmpIPredicate::eq, dynamicIndices[position], + builder.intConstant(static_cast(candidate))); + auto ifOp = scf::IfOp::create(builder, TypeRange{}, matches, true); + OpBuilder::InsertionGuard guard(builder); + const auto emitBranch = [&](Block& block, + llvm::function_ref emitBody) { + if (!block.empty()) { + block.back().erase(); + } + builder.setInsertionPointToEnd(&block); + emitBody(); + scf::YieldOp::create(builder); + }; + emitBranch(ifOp.getThenRegion().front(), [&] { + resolved[position] = qubits[candidate]; + resolveAt(position + 1); + }); + emitBranch(ifOp.getElseRegion().front(), + [&] { emitCase(candidate + 1); }); + }; + emitCase(0); + }; + resolveAt(0); + } + + [[nodiscard]] Value + emitQubitOperation(const frontend::QubitReference& reference, + ValueRange gateQubits, + llvm::function_ref emitResolvedOperation) { + if (!reference.dynamicIndex) { + return emitResolvedOperation(resolveQubit(reference, gateQubits)); + } + + const auto dynamicIndex = emitDynamicQubitIndices({reference}).front(); + const auto& qubits = registerValues.at(reference.symbol); + std::function emitCase; + emitCase = [&](const std::size_t candidate) -> Value { + if (candidate + 1 == qubits.size()) { + return emitResolvedOperation(qubits[candidate]); + } + auto matches = arith::CmpIOp::create( + builder, arith::CmpIPredicate::eq, dynamicIndex, + builder.intConstant(static_cast(candidate))); + auto ifOp = + scf::IfOp::create(builder, builder.getI1Type(), matches, true); + OpBuilder::InsertionGuard guard(builder); + const auto emitBranch = [&](Block& block, + llvm::function_ref emitValue) { + if (!block.empty()) { + block.back().erase(); + } + builder.setInsertionPointToEnd(&block); + scf::YieldOp::create(builder, emitValue()); + }; + emitBranch(ifOp.getThenRegion().front(), + [&] { return emitResolvedOperation(qubits[candidate]); }); + emitBranch(ifOp.getElseRegion().front(), + [&] { return emitCase(candidate + 1); }); + return ifOp.getResult(0); + }; + return emitCase(0); + } + + static LogicalResult emitPrimitive(OpBuilder& opBuilder, const Location loc, + const StringRef name, + const ValueRange parameters, + const ValueRange qubits) { + const auto operationName = + llvm::StringSwitch(name) + .Case("gphase", qc::GPhaseOp::getOperationName()) + .Case("id", qc::IdOp::getOperationName()) + .Case("x", qc::XOp::getOperationName()) + .Case("y", qc::YOp::getOperationName()) + .Case("z", qc::ZOp::getOperationName()) + .Case("h", qc::HOp::getOperationName()) + .Case("s", qc::SOp::getOperationName()) + .Case("sdg", qc::SdgOp::getOperationName()) + .Case("t", qc::TOp::getOperationName()) + .Case("tdg", qc::TdgOp::getOperationName()) + .Case("sx", qc::SXOp::getOperationName()) + .Case("sxdg", qc::SXdgOp::getOperationName()) + .Case("p", qc::POp::getOperationName()) + .Case("rx", qc::RXOp::getOperationName()) + .Case("ry", qc::RYOp::getOperationName()) + .Case("rz", qc::RZOp::getOperationName()) + .Case("r", qc::ROp::getOperationName()) + .Case("u2", qc::U2Op::getOperationName()) + .Case("U", qc::UOp::getOperationName()) + .Case("swap", qc::SWAPOp::getOperationName()) + .Case("iswap", qc::iSWAPOp::getOperationName()) + .Case("dcx", qc::DCXOp::getOperationName()) + .Case("ecr", qc::ECROp::getOperationName()) + .Case("rxx", qc::RXXOp::getOperationName()) + .Case("ryy", qc::RYYOp::getOperationName()) + .Case("rzx", qc::RZXOp::getOperationName()) + .Case("rzz", qc::RZZOp::getOperationName()) + .Case("xx_plus_yy", qc::XXPlusYYOp::getOperationName()) + .Case("xx_minus_yy", qc::XXMinusYYOp::getOperationName()) + .Default({}); + if (operationName.empty()) { + return failure(); + } + OperationState state(loc, operationName); + if (name == "gphase") { + state.addOperands(parameters); + } else { + state.addOperands(qubits); + state.addOperands(parameters); + } + opBuilder.create(state); + return success(); + } + + LogicalResult emitResolvedGate(OpBuilder& opBuilder, + const frontend::GateApplication& application, + const Location loc, ValueRange parameters, + ValueRange qubits) { + if (const auto* custom = findCustomGate(application.callee)) { + if (parameters.size() != custom->parameterCount || + qubits.size() != custom->qubitCount) { + llvm::errs() << "OpenQASM QC emission error: custom-gate operands do " + "not match its verified declaration.\n"; + return failure(); + } + OpBuilder::InsertionGuard guard(builder); + builder.setInsertionPoint(opBuilder.getInsertionBlock(), + opBuilder.getInsertionPoint()); + for (const auto statement : custom->body) { + emitStatement(statement, parameters, qubits); + } + return emissionFailed ? failure() : success(); + } + + const GateCatalogEntry* catalog = + oq3::frontend::lookupGate(application.callee); + if (catalog == nullptr || qubits.size() < catalog->targetCount) { + return failure(); + } + const std::size_t controls = catalog->variadicControls + ? qubits.size() - catalog->targetCount + : catalog->controlCount; + if (qubits.size() < controls + catalog->targetCount) { + return failure(); + } + auto primitiveParameters = parameters; + auto controlValues = qubits.take_front(controls); + auto targets = qubits.drop_front(controls); + if (application.callee == "cu") { + if (controls != 1 || parameters.size() != 4 || targets.size() != 1) { + return failure(); + } + qc::POp::create(opBuilder, loc, controlValues.front(), parameters.back()); + primitiveParameters = parameters.drop_back(); + } + const auto emitCatalogPrimitive = [&](ValueRange primitiveQubits) { + if (!catalog->inverse) { + return emitPrimitive(opBuilder, loc, catalog->primitive, + primitiveParameters, primitiveQubits); + } + LogicalResult result = success(); + qc::InvOp::create( + opBuilder, loc, primitiveQubits, [&](ValueRange aliases) { + result = emitPrimitive(opBuilder, loc, catalog->primitive, + primitiveParameters, aliases); + }); + return result; + }; + if (controls == 0) { + return emitCatalogPrimitive(qubits); + } + LogicalResult result = success(); + qc::CtrlOp::create( + opBuilder, loc, controlValues, targets, + [&](ValueRange aliases) { result = emitCatalogPrimitive(aliases); }); + return result; + } + + LogicalResult emitModifiers(OpBuilder& opBuilder, + const frontend::GateApplication& application, + const Location loc, ValueRange parameters, + ArrayRef controlCounts, + const std::size_t position, ValueRange qubits) { + if (position == application.modifiers.size()) { + return emitResolvedGate(opBuilder, application, loc, parameters, qubits); + } + const auto kind = application.modifiers[position].kind; + if (kind == frontend::ModifierKind::Inv) { + LogicalResult result = success(); + qc::InvOp::create(opBuilder, loc, qubits, [&](ValueRange aliases) { + result = emitModifiers(opBuilder, application, loc, parameters, + controlCounts, position + 1, aliases); + }); + return result; + } + return emitControls(opBuilder, application, loc, parameters, controlCounts, + position + 1, controlCounts[position], qubits); + } + + LogicalResult emitControls(OpBuilder& opBuilder, + const frontend::GateApplication& application, + const Location loc, ValueRange parameters, + ArrayRef controlCounts, + const std::size_t nextPosition, + const std::size_t remainingControls, + ValueRange qubits) { + if (remainingControls == 0) { + return emitModifiers(opBuilder, application, loc, parameters, + controlCounts, nextPosition, qubits); + } + LogicalResult result = success(); + qc::CtrlOp::create(opBuilder, loc, qubits.take_front(1), + qubits.drop_front(1), [&](ValueRange aliases) { + result = emitControls(opBuilder, application, loc, + parameters, controlCounts, + nextPosition, + remainingControls - 1, aliases); + }); + return result; + } + + void emitGateApplication(OpBuilder& opBuilder, + const frontend::GateApplication& application, + const Location loc, ValueRange gateParameters, + ValueRange gateQubits) { + SmallVector parameters; + parameters.reserve(application.parameters.size()); + for (const auto expression : application.parameters) { + Value parameter = emitExpression(opBuilder, expression, gateParameters); + if (isa(parameter.getType())) { + if (program.expressions.at(expression).type == + frontend::ScalarType::Uint) { + parameter = arith::UIToFPOp::create( + opBuilder, loc, opBuilder.getF64Type(), parameter); + } else { + parameter = arith::SIToFPOp::create( + opBuilder, loc, opBuilder.getF64Type(), parameter); + } + } + parameters.push_back(parameter); + } + const auto dynamicIndices = emitDynamicQubitIndices(application.qubits); + for (const auto [position, reference] : + llvm::enumerate(application.qubits)) { + if (reference.kind != frontend::QubitReferenceKind::Register) { + continue; + } + for (const auto [previousPosition, previous] : + llvm::enumerate(ArrayRef(application.qubits).take_front(position))) { + if (previous.kind != frontend::QubitReferenceKind::Register || + previous.symbol != reference.symbol || + (!previous.dynamicIndex && !reference.dynamicIndex)) { + continue; + } + auto previousIndex = + previous.dynamicIndex + ? dynamicIndices[previousPosition] + : builder.intConstant( + static_cast(previous.index)); + auto currentIndex = reference.dynamicIndex + ? dynamicIndices[position] + : builder.intConstant(static_cast( + reference.index)); + auto distinct = arith::CmpIOp::create(builder, arith::CmpIPredicate::ne, + previousIndex, currentIndex); + cf::AssertOp::create(builder, distinct, + "gate operands must not reference the same qubit"); + } + } + SmallVector controlCounts(application.modifiers.size(), 0); + for (const auto [position, modifier] : + llvm::enumerate(application.modifiers)) { + if (modifier.kind != frontend::ModifierKind::Ctrl && + modifier.kind != frontend::ModifierKind::NegCtrl) { + continue; + } + std::int64_t count = 1; + if (modifier.operand) { + auto countValue = + emitExpression(opBuilder, *modifier.operand, gateParameters); + auto constant = countValue.getDefiningOp(); + if (!constant || constant.value() <= 0) { + emissionFailed = true; + llvm::errs() << "OpenQASM QC emission error: gate control count " + "must be a positive constant integer.\n"; + return; + } + count = constant.value(); + } + controlCounts[position] = count; + } + + dispatchQubits( + application.qubits, gateQubits, dynamicIndices, [&](ValueRange qubits) { + llvm::DenseSet distinctQubits(qubits.begin(), qubits.end()); + if (distinctQubits.size() != qubits.size()) { + return; + } + std::size_t negativeOffset = 0; + for (const auto [position, modifier] : + llvm::enumerate(application.modifiers)) { + if (modifier.kind == frontend::ModifierKind::Ctrl || + modifier.kind == frontend::ModifierKind::NegCtrl) { + if (modifier.kind == frontend::ModifierKind::NegCtrl) { + for (auto control : + qubits.slice(negativeOffset, controlCounts[position])) { + qc::XOp::create(opBuilder, loc, control); + } + } + negativeOffset += + static_cast(controlCounts[position]); + } + } + const auto result = + emitModifiers(opBuilder, application, loc, parameters, + controlCounts, 0, qubits); + negativeOffset = 0; + for (const auto [position, modifier] : + llvm::enumerate(application.modifiers)) { + if (modifier.kind == frontend::ModifierKind::Ctrl || + modifier.kind == frontend::ModifierKind::NegCtrl) { + if (modifier.kind == frontend::ModifierKind::NegCtrl) { + for (auto control : + qubits.slice(negativeOffset, controlCounts[position])) { + qc::XOp::create(opBuilder, loc, control); + } + } + negativeOffset += + static_cast(controlCounts[position]); + } + } + if (failed(result)) { + emissionFailed = true; + llvm::errs() << "OpenQASM QC emission error: gate '" + << application.callee + << "' has no lowering to the QC dialect.\n"; + } + }); + } + + [[nodiscard]] Value coerceScalar(Value value, + const frontend::ScalarType source, + const frontend::ScalarType target) { + if (source == target || + (source == frontend::ScalarType::Int && + target == frontend::ScalarType::Uint) || + (source == frontend::ScalarType::Uint && + target == frontend::ScalarType::Int)) { + return value; + } + if (target == frontend::ScalarType::Float) { + if (source == frontend::ScalarType::Bool || + source == frontend::ScalarType::Uint) { + return arith::UIToFPOp::create(builder, builder.getF64Type(), value); + } + return arith::SIToFPOp::create(builder, builder.getF64Type(), value); + } + if (source == frontend::ScalarType::Bool) { + return arith::ExtUIOp::create(builder, builder.getI64Type(), value); + } + if (source == frontend::ScalarType::Float && + target == frontend::ScalarType::Uint) { + return arith::FPToUIOp::create(builder, builder.getI64Type(), value); + } + if (source == frontend::ScalarType::Float) { + return arith::FPToSIOp::create(builder, builder.getI64Type(), value); + } + llvm_unreachable("unsupported standard scalar conversion"); + } + + [[nodiscard]] Value readBit(const frontend::BitReference& reference) { + auto& values = bitValues.at(reference.reg); + if (!reference.dynamicIndex) { + return values[reference.index]; + } + + const auto width = + static_cast(program.registers.at(reference.reg).width); + auto index = emitCheckedIndex(*reference.dynamicIndex, width, + "dynamic classical index out of bounds"); + + Value selected = values.front(); + for (std::int64_t i = 1; i < width; ++i) { + auto isIndex = arith::CmpIOp::create(builder, arith::CmpIPredicate::eq, + index, builder.intConstant(i)); + selected = arith::SelectOp::create(builder, isIndex, values[i], selected); + } + return selected; + } + + [[nodiscard]] Value + emitComparison(const frontend::ConditionExpression& condition, + ValueRange gateParameters) { + auto lhs = emitExpression(builder, condition.comparisonLhs, gateParameters); + auto rhs = emitExpression(builder, condition.comparisonRhs, gateParameters); + const auto lhsType = program.expressions.at(condition.comparisonLhs).type; + const auto rhsType = program.expressions.at(condition.comparisonRhs).type; + if (lhsType == frontend::ScalarType::Float || + rhsType == frontend::ScalarType::Float) { + lhs = coerceScalar(lhs, lhsType, frontend::ScalarType::Float); + rhs = coerceScalar(rhs, rhsType, frontend::ScalarType::Float); + const auto predicate = [&] { + switch (condition.comparison) { + case frontend::ComparisonKind::Equal: + return arith::CmpFPredicate::OEQ; + case frontend::ComparisonKind::NotEqual: + return arith::CmpFPredicate::UNE; + case frontend::ComparisonKind::Less: + return arith::CmpFPredicate::OLT; + case frontend::ComparisonKind::LessEqual: + return arith::CmpFPredicate::OLE; + case frontend::ComparisonKind::Greater: + return arith::CmpFPredicate::OGT; + case frontend::ComparisonKind::GreaterEqual: + return arith::CmpFPredicate::OGE; + } + llvm_unreachable("unknown floating-point comparison"); + }(); + return arith::CmpFOp::create(builder, predicate, lhs, rhs); + } + + const bool isUnsigned = lhsType == frontend::ScalarType::Uint || + rhsType == frontend::ScalarType::Uint; + const auto predicate = [&] { + switch (condition.comparison) { + case frontend::ComparisonKind::Equal: + return arith::CmpIPredicate::eq; + case frontend::ComparisonKind::NotEqual: + return arith::CmpIPredicate::ne; + case frontend::ComparisonKind::Less: + return isUnsigned ? arith::CmpIPredicate::ult + : arith::CmpIPredicate::slt; + case frontend::ComparisonKind::LessEqual: + return isUnsigned ? arith::CmpIPredicate::ule + : arith::CmpIPredicate::sle; + case frontend::ComparisonKind::Greater: + return isUnsigned ? arith::CmpIPredicate::ugt + : arith::CmpIPredicate::sgt; + case frontend::ComparisonKind::GreaterEqual: + return isUnsigned ? arith::CmpIPredicate::uge + : arith::CmpIPredicate::sge; + } + llvm_unreachable("unknown integer comparison"); + }(); + return arith::CmpIOp::create(builder, predicate, lhs, rhs); + } + + [[nodiscard]] Value emitCondition(const frontend::ConditionId id, + ValueRange gateParameters, + ValueRange gateQubits) { + const auto& condition = program.conditions.at(id); + switch (condition.kind) { + case frontend::ConditionKind::Literal: + return builder.boolConstant(condition.literal); + case frontend::ConditionKind::Scalar: + return scalarValues.at(condition.scalar); + case frontend::ConditionKind::Bit: + return readBit(condition.bit); + case frontend::ConditionKind::Measurement: + return emitQubitOperation( + condition.measurement, gateQubits, + [&](Value qubit) { return builder.measure(qubit); }); + case frontend::ConditionKind::Not: + return arith::XOrIOp::create( + builder, emitCondition(condition.lhs, gateParameters, gateQubits), + builder.boolConstant(true)); + case frontend::ConditionKind::And: { + auto lhs = emitCondition(condition.lhs, gateParameters, gateQubits); + auto rhs = emitCondition(condition.rhs, gateParameters, gateQubits); + return arith::AndIOp::create(builder, lhs, rhs); + } + case frontend::ConditionKind::Or: { + auto lhs = emitCondition(condition.lhs, gateParameters, gateQubits); + auto rhs = emitCondition(condition.rhs, gateParameters, gateQubits); + return arith::OrIOp::create(builder, lhs, rhs); + } + case frontend::ConditionKind::Comparison: + return emitComparison(condition, gateParameters); + } + llvm_unreachable("unknown condition kind"); + } + + static constexpr std::uint64_t scalarStateMask = std::uint64_t{1} << 63U; + + static std::uint64_t scalarStateKey(const frontend::ScalarId scalar) { + return scalarStateMask | scalar; + } + + static std::uint64_t bitStateKey(const frontend::RegisterId reg, + const std::uint64_t bit) { + return (static_cast(reg) << 32U) | bit; + } + + void collectMutations(const frontend::StatementId id, + llvm::DenseSet& mutations) const { + const auto& statement = program.statements.at(id); + std::visit( + [&](const auto& data) { + using T = std::decay_t; + if constexpr (std::is_same_v || + std::is_same_v) { + mutations.insert(scalarStateKey(data.scalar)); + } else if constexpr (std::is_same_v) { + for (const auto& target : data.targets) { + if (!target.dynamicIndex) { + mutations.insert(bitStateKey(target.reg, target.index)); + continue; + } + for (std::uint64_t bit = 0; + bit < program.registers.at(target.reg).width; ++bit) { + mutations.insert(bitStateKey(target.reg, bit)); + } + } + } else if constexpr (std::is_same_v< + T, frontend::BitAssignmentStatement>) { + if (!data.target.dynamicIndex) { + mutations.insert(bitStateKey(data.target.reg, data.target.index)); + } else { + for (std::uint64_t bit = 0; + bit < program.registers.at(data.target.reg).width; ++bit) { + mutations.insert(bitStateKey(data.target.reg, bit)); + } + } + } else if constexpr (std::is_same_v) { + for (const auto nested : data.thenStatements) { + collectMutations(nested, mutations); + } + for (const auto nested : data.elseStatements) { + collectMutations(nested, mutations); + } + } else if constexpr (std::is_same_v || + std::is_same_v) { + for (const auto nested : data.body) { + collectMutations(nested, mutations); + } + } + }, + statement.data); + } + + [[nodiscard]] SmallVector + mutatedState(ArrayRef statements) const { + llvm::DenseSet mutations; + for (const auto statement : statements) { + collectMutations(statement, mutations); + } + SmallVector slots; + for (const auto [scalar, value] : llvm::enumerate(scalarValues)) { + if (value && mutations.contains(scalarStateKey( + static_cast(scalar)))) { + slots.push_back({.kind = StateKind::Scalar, + .first = static_cast(scalar)}); + } + } + for (const auto [reg, values] : llvm::enumerate(bitValues)) { + for (const auto [bit, value] : llvm::enumerate(values)) { + if (value && mutations.contains(bitStateKey( + static_cast(reg), bit))) { + slots.push_back({.kind = StateKind::Bit, + .first = static_cast(reg), + .second = static_cast(bit)}); + } + } + } + return slots; + } + + [[nodiscard]] SmallVector + stateValues(ArrayRef slots) const { + SmallVector values; + values.reserve(slots.size()); + for (const auto& slot : slots) { + values.push_back(slot.kind == StateKind::Scalar + ? scalarValues.at(slot.first) + : bitValues.at(slot.first)[slot.second]); + } + return values; + } + + void assignState(ArrayRef slots, ValueRange values) { + for (const auto [slot, value] : llvm::zip_equal(slots, values)) { + if (slot.kind == StateKind::Scalar) { + scalarValues.at(slot.first) = value; + } else { + bitValues.at(slot.first)[slot.second] = value; + } + } + } + + void emitStatement(const frontend::StatementId id, ValueRange gateParameters, + ValueRange gateQubits) { + const auto& statement = program.statements.at(id); + const auto loc = getLocation(statement.location); + builder.setLoc(loc); + std::visit( + [&](const auto& data) { + using T = std::decay_t; + if constexpr (std::is_same_v) { + emitDeclaration(data); + } else if constexpr (std::is_same_v< + T, frontend::ScalarDeclarationStatement>) { + emitScalarDeclaration(data, gateQubits); + } else if constexpr (std::is_same_v< + T, frontend::ScalarAssignmentStatement>) { + emitScalarAssignment(data, gateQubits); + } else if constexpr (std::is_same_v< + T, frontend::BitAssignmentStatement>) { + emitBitAssignment(data, gateQubits); + } else if constexpr (std::is_same_v) { + emitGateApplication(builder, data, loc, gateParameters, gateQubits); + } else if constexpr (std::is_same_v) { + emitMeasurement(data, gateQubits); + } else if constexpr (std::is_same_v) { + for (const auto& qubit : data.qubits) { + const auto indices = emitDynamicQubitIndices({qubit}); + dispatchQubits({qubit}, gateQubits, indices, + [&](ValueRange resolved) { + builder.reset(resolved.front()); + }); + } + } else if constexpr (std::is_same_v) { + const auto indices = emitDynamicQubitIndices(data.qubits); + dispatchQubits(data.qubits, gateQubits, indices, + [&](ValueRange qubits) { builder.barrier(qubits); }); + } else if constexpr (std::is_same_v) { + emitIf(data, gateParameters, gateQubits); + } else if constexpr (std::is_same_v) { + emitFor(data, gateParameters, gateQubits); + } else if constexpr (std::is_same_v) { + emitWhile(data, gateParameters, gateQubits); + } + }, + statement.data); + } + + [[nodiscard]] Type scalarType(const frontend::ScalarType type) { + switch (type) { + case frontend::ScalarType::Bool: + return builder.getI1Type(); + case frontend::ScalarType::Int: + case frontend::ScalarType::Uint: + return builder.getI64Type(); + case frontend::ScalarType::Float: + return builder.getF64Type(); + } + llvm_unreachable("unknown scalar type"); + } + + void + emitScalarDeclaration(const frontend::ScalarDeclarationStatement& statement, + ValueRange gateQubits) { + const auto type = program.scalars.at(statement.scalar).type; + Value value = ub::PoisonOp::create(builder, scalarType(type)).getResult(); + if (statement.initializer) { + const auto source = program.expressions.at(*statement.initializer).type; + value = coerceScalar(emitExpression(builder, *statement.initializer, {}), + source, type); + } else if (statement.conditionInitializer) { + value = emitCondition(*statement.conditionInitializer, {}, gateQubits); + } + scalarValues.at(statement.scalar) = value; + } + + void + emitScalarAssignment(const frontend::ScalarAssignmentStatement& statement, + ValueRange gateQubits) { + const auto type = program.scalars.at(statement.scalar).type; + if (statement.value) { + const auto source = program.expressions.at(*statement.value).type; + scalarValues.at(statement.scalar) = coerceScalar( + emitExpression(builder, *statement.value, {}), source, type); + return; + } + scalarValues.at(statement.scalar) = + emitCondition(*statement.condition, {}, gateQubits); + } + + void emitDeclaration(const frontend::DeclarationStatement& statement) { + const auto& declaration = program.registers.at(statement.reg); + if (declaration.kind == frontend::RegisterKind::Qubit) { + auto allocation = builder.allocQubitRegister( + static_cast(declaration.width)); + registerValues[statement.reg] = std::move(allocation.qubits); + return; + } + classicalRegisters[statement.reg] = builder.allocClassicalBitRegister( + static_cast(declaration.width), declaration.name); + bitValues[statement.reg].resize(declaration.width); + auto poison = + ub::PoisonOp::create(builder, builder.getI1Type()).getResult(); + llvm::fill(bitValues[statement.reg], poison); + } + + void assignBit(const frontend::BitReference& target, Value value) { + if (!target.dynamicIndex) { + bitValues[target.reg][target.index] = value; + return; + } + const auto width = + static_cast(program.registers.at(target.reg).width); + auto index = emitCheckedIndex(*target.dynamicIndex, width, + "dynamic classical index out of bounds"); + for (std::int64_t bit = 0; bit < width; ++bit) { + auto selected = arith::CmpIOp::create(builder, arith::CmpIPredicate::eq, + index, builder.intConstant(bit)); + bitValues[target.reg][bit] = arith::SelectOp::create( + builder, selected, value, bitValues[target.reg][bit]); + } + } + + void emitBitAssignment(const frontend::BitAssignmentStatement& assignment, + ValueRange gateQubits) { + assignBit(assignment.target, + emitCondition(assignment.value, {}, gateQubits)); + } + + void emitMeasurement(const frontend::MeasurementStatement& measurement, + ValueRange gateQubits) { + if (measurement.targets.empty()) { + for (const auto& qubit : measurement.qubits) { + const auto indices = emitDynamicQubitIndices({qubit}); + dispatchQubits({qubit}, gateQubits, indices, [&](ValueRange resolved) { + (void)builder.measure(resolved.front()); + }); + } + return; + } + for (const auto [target, qubit] : + llvm::zip_equal(measurement.targets, measurement.qubits)) { + const auto& reg = classicalRegisters[target.reg]; + if (!reg) { + llvm::errs() << "OpenQASM emission error: measurement target has no " + "classical storage.\n"; + return; + } + const auto emitMeasurement = [&](Value resolved) { + if (target.dynamicIndex) { + return builder.measure(resolved); + } + return builder.measure(resolved, + (*reg)[static_cast(target.index)]); + }; + auto measured = emitQubitOperation(qubit, gateQubits, emitMeasurement); + if (!measured) { + return; + } + if (!target.dynamicIndex) { + bitValues[target.reg][target.index] = measured; + continue; + } + assignBit(target, measured); + } + } + + void emitIf(const frontend::IfStatement& conditional, + ValueRange gateParameters, ValueRange gateQubits) { + const auto& typedCondition = program.conditions.at(conditional.condition); + if (typedCondition.kind == frontend::ConditionKind::Literal) { + const auto& selected = typedCondition.literal + ? conditional.thenStatements + : conditional.elseStatements; + for (const auto statement : selected) { + emitStatement(statement, gateParameters, gateQubits); + } + return; + } + auto condition = + emitCondition(conditional.condition, gateParameters, gateQubits); + SmallVector nestedStatements( + conditional.thenStatements.begin(), conditional.thenStatements.end()); + nestedStatements.append(conditional.elseStatements.begin(), + conditional.elseStatements.end()); + const auto slots = mutatedState(nestedStatements); + const auto initialValues = stateValues(slots); + const auto savedScalars = scalarValues; + const auto savedBits = bitValues; + const auto* thenStatements = &conditional.thenStatements; + const auto* elseStatements = &conditional.elseStatements; + if (slots.empty() && thenStatements->empty() && !elseStatements->empty()) { + condition = + arith::XOrIOp::create(builder, condition, builder.boolConstant(true)); + std::swap(thenStatements, elseStatements); + } + const bool withElseRegion = !elseStatements->empty() || !slots.empty(); + auto ifOp = scf::IfOp::create(builder, ValueRange(initialValues).getTypes(), + condition, withElseRegion); + OpBuilder::InsertionGuard guard(builder); + const auto emitBranch = [&](Block& block, + ArrayRef statements) { + scalarValues = savedScalars; + bitValues = savedBits; + if (!block.empty()) { + block.back().erase(); + } + builder.setInsertionPointToEnd(&block); + for (const auto statement : statements) { + emitStatement(statement, gateParameters, gateQubits); + } + scf::YieldOp::create(builder, stateValues(slots)); + }; + emitBranch(ifOp.getThenRegion().front(), *thenStatements); + if (withElseRegion) { + emitBranch(ifOp.getElseRegion().front(), *elseStatements); + } + scalarValues = savedScalars; + bitValues = savedBits; + assignState(slots, ifOp.getResults()); + } + + [[nodiscard]] Value extendRangeValue(Value value, Type targetType, + const bool isUnsigned) { + if (isUnsigned) { + return arith::ExtUIOp::create(builder, targetType, value); + } + return arith::ExtSIOp::create(builder, targetType, value); + } + + [[nodiscard]] std::optional + constantRangeTripCount(const frontend::ForStatement& loop) const { + const auto& startExpression = program.expressions.at(loop.start); + const auto& stepExpression = program.expressions.at(loop.step); + const auto& stopExpression = program.expressions.at(loop.stop); + if (startExpression.kind != frontend::ExpressionKind::Constant || + stepExpression.kind != frontend::ExpressionKind::Constant || + stopExpression.kind != frontend::ExpressionKind::Constant) { + return std::nullopt; + } + const bool unsignedEndpoints = + startExpression.type == frontend::ScalarType::Uint || + stopExpression.type == frontend::ScalarType::Uint; + const auto extendConstant = [](const frontend::ScalarExpression& expression, + const bool asUnsigned) { + const auto bits = expression.type == frontend::ScalarType::Uint + ? std::get(expression.constant) + : static_cast( + std::get(expression.constant)); + const APInt value(64, bits); + return asUnsigned ? value.zext(128) : value.sext(128); + }; + const auto start = extendConstant(startExpression, unsignedEndpoints); + const auto stop = extendConstant(stopExpression, unsignedEndpoints); + const bool unsignedStep = stepExpression.type == frontend::ScalarType::Uint; + const auto step = extendConstant(stepExpression, unsignedStep); + if (step.isZero()) { + return std::nullopt; + } + const bool positive = unsignedStep || !step.isNegative(); + const bool nonempty = + positive ? (unsignedEndpoints ? start.ule(stop) : start.sle(stop)) + : (unsignedEndpoints ? start.uge(stop) : start.sge(stop)); + if (!nonempty) { + return 0; + } + const auto distance = positive ? stop - start : start - stop; + const auto absoluteStep = positive ? step : -step; + const auto count = distance.udiv(absoluteStep) + 1; + const APInt maximum(128, static_cast( + std::numeric_limits::max())); + if (count.ugt(maximum)) { + return std::nullopt; + } + return static_cast(count.getZExtValue()); + } + + void emitFor(const frontend::ForStatement& loop, ValueRange gateParameters, + ValueRange gateQubits) { + const auto slots = mutatedState(loop.body); + const auto initialValues = stateValues(slots); + const auto savedScalars = scalarValues; + const auto savedBits = bitValues; + + auto start = emitExpression(builder, loop.start, {}); + auto step = emitExpression(builder, loop.step, {}); + auto stop = emitExpression(builder, loop.stop, {}); + auto i128 = IntegerType::get(&context, 128); + const bool unsignedEndpoints = + program.expressions.at(loop.start).type == frontend::ScalarType::Uint || + program.expressions.at(loop.stop).type == frontend::ScalarType::Uint; + auto startWide = extendRangeValue(start, i128, unsignedEndpoints); + auto stepWide = extendRangeValue(step, i128, + program.expressions.at(loop.step).type == + frontend::ScalarType::Uint); + auto stopWide = extendRangeValue(stop, i128, unsignedEndpoints); + auto zero = arith::ConstantIntOp::create(builder, 0, 128); + if (const auto tripCount = constantRangeTripCount(loop)) { + auto lowerBound = arith::ConstantIndexOp::create(builder, 0); + auto upperBound = arith::ConstantIndexOp::create(builder, *tripCount); + auto indexStep = arith::ConstantIndexOp::create(builder, 1); + auto forOp = scf::ForOp::create(builder, lowerBound, upperBound, + indexStep, initialValues); + { + OpBuilder::InsertionGuard guard(builder); + auto* body = forOp.getBody(); + if (!body->empty()) { + body->back().erase(); + } + builder.setInsertionPointToEnd(body); + scalarValues = savedScalars; + bitValues = savedBits; + assignState(slots, forOp.getRegionIterArgs()); + auto counter = arith::IndexCastOp::create(builder, builder.getI64Type(), + forOp.getInductionVar()); + auto counterWide = arith::ExtUIOp::create(builder, i128, counter); + auto offset = arith::MulIOp::create(builder, counterWide, stepWide); + auto inductionWide = arith::AddIOp::create(builder, startWide, offset); + scalarValues.at(loop.inductionVariable) = arith::TruncIOp::create( + builder, builder.getI64Type(), inductionWide); + for (const auto statement : loop.body) { + emitStatement(statement, gateParameters, gateQubits); + } + scf::YieldOp::create(builder, stateValues(slots)); + } + scalarValues = savedScalars; + bitValues = savedBits; + assignState(slots, forOp.getResults()); + return; + } + + if (program.expressions.at(loop.step).kind != + frontend::ExpressionKind::Constant) { + auto nonzero = arith::CmpIOp::create(builder, arith::CmpIPredicate::ne, + stepWide, zero); + cf::AssertOp::create(builder, nonzero, + "for-loop range step must not be zero"); + } + SmallVector resultTypes{i128}; + llvm::append_range(resultTypes, ValueRange(initialValues).getTypes()); + SmallVector operands{startWide}; + llvm::append_range(operands, initialValues); + auto whileOp = scf::WhileOp::create( + builder, resultTypes, operands, + [&](OpBuilder& nested, Location loc, ValueRange arguments) { + auto positive = arith::CmpIOp::create( + nested, loc, arith::CmpIPredicate::sgt, stepWide, zero); + auto ascending = + arith::CmpIOp::create(nested, loc, arith::CmpIPredicate::sle, + arguments.front(), stopWide); + auto descending = + arith::CmpIOp::create(nested, loc, arith::CmpIPredicate::sge, + arguments.front(), stopWide); + auto active = arith::SelectOp::create(nested, loc, positive, + ascending, descending); + scf::ConditionOp::create(nested, loc, active, arguments); + }, + [&](OpBuilder& nested, Location, ValueRange arguments) { + OpBuilder::InsertionGuard guard(builder); + builder.setInsertionPoint(nested.getInsertionBlock(), + nested.getInsertionPoint()); + scalarValues = savedScalars; + bitValues = savedBits; + assignState(slots, arguments.drop_front()); + scalarValues.at(loop.inductionVariable) = arith::TruncIOp::create( + builder, builder.getI64Type(), arguments.front()); + for (const auto statement : loop.body) { + emitStatement(statement, gateParameters, gateQubits); + } + SmallVector yielded{ + arith::AddIOp::create(builder, arguments.front(), stepWide)}; + llvm::append_range(yielded, stateValues(slots)); + scf::YieldOp::create(builder, yielded); + }); + scalarValues = savedScalars; + bitValues = savedBits; + assignState(slots, whileOp.getResults().drop_front()); + } + + void emitWhile(const frontend::WhileStatement& loop, + ValueRange gateParameters, ValueRange gateQubits) { + const auto slots = mutatedState(loop.body); + const auto initialValues = stateValues(slots); + const auto savedScalars = scalarValues; + const auto savedBits = bitValues; + auto whileOp = scf::WhileOp::create( + builder, ValueRange(initialValues).getTypes(), initialValues, + [&](OpBuilder& nested, Location, ValueRange arguments) { + OpBuilder::InsertionGuard guard(builder); + builder.setInsertionPoint(nested.getInsertionBlock(), + nested.getInsertionPoint()); + scalarValues = savedScalars; + bitValues = savedBits; + assignState(slots, arguments); + auto condition = + emitCondition(loop.condition, gateParameters, gateQubits); + scf::ConditionOp::create(builder, condition, stateValues(slots)); + }, + [&](OpBuilder& nested, Location, ValueRange arguments) { + OpBuilder::InsertionGuard guard(builder); + builder.setInsertionPoint(nested.getInsertionBlock(), + nested.getInsertionPoint()); + scalarValues = savedScalars; + bitValues = savedBits; + assignState(slots, arguments); + for (const auto statement : loop.body) { + emitStatement(statement, gateParameters, gateQubits); + } + scf::YieldOp::create(builder, stateValues(slots)); + }); + scalarValues = savedScalars; + bitValues = savedBits; + assignState(slots, whileOp.getResults()); + } +}; + +} // namespace + +OwningOpRef emitOpenQASMToQC(const frontend::TypedProgram& program, + MLIRContext& context) { + return OpenQASMToQCEmitter(program, context).emit(); +} + +} // namespace mlir::qc::detail diff --git a/mlir/lib/Dialect/QC/Translation/OpenQASMToQCEmitter.h b/mlir/lib/Dialect/QC/Translation/OpenQASMToQCEmitter.h new file mode 100644 index 0000000000..f929856303 --- /dev/null +++ b/mlir/lib/Dialect/QC/Translation/OpenQASMToQCEmitter.h @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#pragma once + +#include "mlir/Target/OpenQASM/Frontend.h" + +#include + +namespace mlir { +class MLIRContext; +class ModuleOp; + +namespace qc::detail { + +[[nodiscard]] OwningOpRef +emitOpenQASMToQC(const oq3::frontend::TypedProgram& program, + MLIRContext& context); + +} // namespace qc::detail +} // namespace mlir diff --git a/mlir/lib/Dialect/QC/Translation/TranslateQASM3ToQC.cpp b/mlir/lib/Dialect/QC/Translation/TranslateQASM3ToQC.cpp index eab7843753..0356249978 100644 --- a/mlir/lib/Dialect/QC/Translation/TranslateQASM3ToQC.cpp +++ b/mlir/lib/Dialect/QC/Translation/TranslateQASM3ToQC.cpp @@ -10,1104 +10,56 @@ #include "mlir/Dialect/QC/Translation/TranslateQASM3ToQC.h" -#include "ir/Definitions.hpp" -#include "ir/operations/OpType.hpp" -#include "mlir/Dialect/QC/Builder/QCProgramBuilder.h" -#include "mlir/Dialect/QC/IR/QCOps.h" -#include "qasm3/Exception.hpp" -#include "qasm3/Gate.hpp" -#include "qasm3/InstVisitor.hpp" -#include "qasm3/NestedEnvironment.hpp" -#include "qasm3/Parser.hpp" -#include "qasm3/Statement.hpp" -#include "qasm3/StdGates.hpp" -#include "qasm3/Types.hpp" -#include "qasm3/passes/ConstEvalPass.hpp" -#include "qasm3/passes/TypeCheckPass.hpp" +#include "OpenQASMToQCEmitter.h" +#include "mlir/Target/OpenQASM/Frontend.h" -#include -#include -#include -#include +#include #include #include -#include -#include -#include #include #include -#include -#include -#include +#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include #include namespace mlir::qc { - namespace { -/// Signature: (builder, gate operands, evaluated parameters). -/// For gates with implicit controls (cx, ccx, ...), all qubits including -/// the controls are part of the range, matching OpenQASM 3 operand order. -using GateFn = - std::function)>; - -} // namespace - -/** - * @brief Build the table mapping OpenQASM 3 gate identifiers to - * QCProgramBuilder emitters. - * - * @details - * Each entry maps an OpenQASM 3 gate identifier to a lambda that emits the - * corresponding QC operation via the QCProgramBuilder. - */ -static llvm::StringMap buildGateDispatch() { - llvm::StringMap d; - - // ZeroTargetOneParameter - d["gphase"] = [](auto& b, auto /*q*/, auto p) { b.gphase(p[0]); }; - - // OneTargetZeroParameter - d["id"] = [](auto& b, auto q, auto) { b.id(q[0]); }; - d["x"] = [](auto& b, auto q, auto) { b.x(q[0]); }; - d["y"] = [](auto& b, auto q, auto) { b.y(q[0]); }; - d["z"] = [](auto& b, auto q, auto) { b.z(q[0]); }; - d["h"] = [](auto& b, auto q, auto) { b.h(q[0]); }; - d["s"] = [](auto& b, auto q, auto) { b.s(q[0]); }; - d["sdg"] = [](auto& b, auto q, auto) { b.sdg(q[0]); }; - d["t"] = [](auto& b, auto q, auto) { b.t(q[0]); }; - d["tdg"] = [](auto& b, auto q, auto) { b.tdg(q[0]); }; - d["sx"] = [](auto& b, auto q, auto) { b.sx(q[0]); }; - d["sxdg"] = [](auto& b, auto q, auto) { b.sxdg(q[0]); }; - - // OneTargetOneParameter - d["rx"] = [](auto& b, auto q, auto p) { b.rx(p[0], q[0]); }; - d["ry"] = [](auto& b, auto q, auto p) { b.ry(p[0], q[0]); }; - d["rz"] = [](auto& b, auto q, auto p) { b.rz(p[0], q[0]); }; - d["p"] = [](auto& b, auto q, auto p) { b.p(p[0], q[0]); }; - d["u1"] = [](auto& b, auto q, auto p) { b.p(p[0], q[0]); }; // alias - d["phase"] = [](auto& b, auto q, auto p) { b.p(p[0], q[0]); }; // alias - - // OneTargetTwoParameter - d["r"] = [](auto& b, auto q, auto p) { b.r(p[0], p[1], q[0]); }; - d["u2"] = [](auto& b, auto q, auto p) { b.u2(p[0], p[1], q[0]); }; - - // OneTargetThreeParameter - auto uFn = [](auto& b, auto q, auto p) { b.u(p[0], p[1], p[2], q[0]); }; - d["U"] = uFn; - d["u3"] = uFn; // alias - d["u"] = uFn; // alias - - // TwoTargetZeroParameter - d["swap"] = [](auto& b, auto q, auto) { b.swap(q[0], q[1]); }; - d["iswap"] = [](auto& b, auto q, auto) { b.iswap(q[0], q[1]); }; - d["dcx"] = [](auto& b, auto q, auto) { b.dcx(q[0], q[1]); }; - d["ecr"] = [](auto& b, auto q, auto) { b.ecr(q[0], q[1]); }; - - // TwoTargetOneParameter - d["rxx"] = [](auto& b, auto q, auto p) { b.rxx(p[0], q[0], q[1]); }; - d["ryy"] = [](auto& b, auto q, auto p) { b.ryy(p[0], q[0], q[1]); }; - d["rzx"] = [](auto& b, auto q, auto p) { b.rzx(p[0], q[0], q[1]); }; - d["rzz"] = [](auto& b, auto q, auto p) { b.rzz(p[0], q[0], q[1]); }; - - // TwoTargetTwoParameter - d["xx_plus_yy"] = [](auto& b, auto q, auto p) { - b.xx_plus_yy(p[0], p[1], q[0], q[1]); - }; - d["xx_minus_yy"] = [](auto& b, auto q, auto p) { - b.xx_minus_yy(p[0], p[1], q[0], q[1]); - }; - - // Controlled OneTargetZeroParameter - d["cx"] = [](auto& b, auto q, auto) { b.cx(q[0], q[1]); }; - d["cnot"] = [](auto& b, auto q, auto) { b.cx(q[0], q[1]); }; // alias - d["cy"] = [](auto& b, auto q, auto) { b.cy(q[0], q[1]); }; - d["cz"] = [](auto& b, auto q, auto) { b.cz(q[0], q[1]); }; - d["ch"] = [](auto& b, auto q, auto) { b.ch(q[0], q[1]); }; - d["csx"] = [](auto& b, auto q, auto) { b.csx(q[0], q[1]); }; - - // Controlled OneTargetOneParameter - d["crx"] = [](auto& b, auto q, auto p) { b.crx(p[0], q[0], q[1]); }; - d["cry"] = [](auto& b, auto q, auto p) { b.cry(p[0], q[0], q[1]); }; - d["crz"] = [](auto& b, auto q, auto p) { b.crz(p[0], q[0], q[1]); }; - d["cp"] = [](auto& b, auto q, auto p) { b.cp(p[0], q[0], q[1]); }; - d["cphase"] = [](auto& b, auto q, auto p) { - b.cp(p[0], q[0], q[1]); - }; // alias - - // Controlled TwoTargetZeroParameter - d["cswap"] = [](auto& b, auto q, auto) { b.cswap(q[0], q[1], q[2]); }; - d["fredkin"] = [](auto& b, auto q, auto) { - b.cswap(q[0], q[1], q[2]); - }; // alias - - // Multi-controlled gates - auto mcxFn = [](auto& b, auto q, auto) { b.mcx(q.drop_back(1), q.back()); }; - d["mcx"] = mcxFn; - d["mcx_gray"] = mcxFn; - - d["mcx_vchain"] = [](auto& b, auto q, auto) { - const size_t n = q.size() - ((q.size() + 1) / 2) + 2; - b.mcx(q.slice(0, n - 1), q[n - 1]); - }; - - d["mcx_recursive"] = [](auto& b, auto q, auto) { - const size_t n = (q.size() > 5) ? q.size() - 1 : q.size(); - b.mcx(q.slice(0, n - 1), q[n - 1]); - }; - - d["mcphase"] = [](auto& b, auto q, auto p) { - b.mcp(p[0], q.drop_back(1), q.back()); - }; - - return d; -} - -static llvm::StringMap> convertToStringMap( - const std::map>& sourceMap) { - llvm::StringMap> targetMap; - for (const auto& [key, value] : sourceMap) { - targetMap.insert(std::make_pair(key, value)); +void printDiagnostics( + const std::vector& diagnostics) { + for (const auto& diagnostic : diagnostics) { + llvm::errs() << diagnostic.location.filename << ':' + << diagnostic.location.line << ':' + << diagnostic.location.column + << ": OpenQASM frontend error: " << diagnostic.message << '\n'; } - return targetMap; } -namespace { - -/// Map from OpenQASM 3 gate identifier to QCProgramBuilder emitter. -const llvm::StringMap GATE_DISPATCH = buildGateDispatch(); - -/// Map of qubits in the current scope. -using QubitScope = llvm::StringMap>; - -/** - * @brief AST visitor that translates an OpenQASM 3 program to a QC program. - * - * @details - * Implements qasm3::InstVisitor to walk the AST produced by qasm3::Parser and - * emit QC operations via the QCProgramBuilder. - */ -class MLIRQasmImporter final : public qasm3::InstVisitor { -public: - explicit MLIRQasmImporter(MLIRContext* ctx) - : builder(ctx), typeCheckPass(constEvalPass), - gates(convertToStringMap(qasm3::STANDARD_GATES)) { - initBuiltins(); - builder.initialize(); - } - - void - visitProgram(const std::vector>& program) { - for (const auto& stmt : program) { - constEvalPass.processStatement(*stmt); - typeCheckPass.processStatement(*stmt); - stmt->accept(this); - } - } - - OwningOpRef finalize() { - if (outputRegisters.empty()) { - outputRegisters = allBitRegisters; - } - - if (outputRegisters.empty()) { - return builder.finalize(); - } - - // Collect measurement results for all output bit registers - SmallVector returnValues; - for (const auto& regName : outputRegisters) { - auto it = bitValues.find(regName); - if (it == bitValues.end()) { - llvm::errs() << "Output register '" << regName - << "' was never measured.\n"; - return nullptr; - } - - auto expectedSize = classicalRegisters[regName].size; - if (it->second.size() < expectedSize) { - llvm::errs() << "Not all bits of output register '" << regName - << "' have been measured.\n"; - return nullptr; - } - for (auto bit : it->second) { - if (!bit) { - llvm::errs() << "Not all bits of output register '" << regName - << "' have been measured.\n"; - return nullptr; - } - returnValues.push_back(bit); - } - } - - builder.retype(ValueRange(returnValues).getTypes()); - return builder.finalize(returnValues); - } - -private: - QCProgramBuilder builder; - qasm3::const_eval::ConstEvalPass constEvalPass; - qasm3::type_checking::TypeCheckPass typeCheckPass; - qasm3::NestedEnvironment> - declarations; - - /// Map from qubit-register name to allocated qubit values. - QubitScope qubitRegisters; - - /// Map from classical-register name to ClassicalRegister. - llvm::StringMap classicalRegisters; - - /// Map from classical-register name to measurement results. - llvm::StringMap> bitValues; - - /// Names of all bit registers, in declaration order. - SmallVector allBitRegisters; - - /// Names of classical registers declared as output, in declaration order. - SmallVector outputRegisters; - - /// Map from gate identifier to OpenQASM 3 definition. - llvm::StringMap> gates; - - bool openQASM2CompatMode{false}; - - //===--- Initialization -----------------------------------------------===// - - void initBuiltins() { - using namespace qasm3::const_eval; - using namespace qasm3::type_checking; - - auto floatType = - InferredType{std::dynamic_pointer_cast( - std::make_shared>(qasm3::Float, - 64))}; - - auto addConstant = [&](const std::string& name, double value) { - constEvalPass.addConst(name, ConstEvalValue(value)); - typeCheckPass.addBuiltin(name, floatType); - }; - - addConstant("pi", ::qc::PI); - addConstant("π", ::qc::PI); - addConstant("tau", ::qc::TAU); - addConstant("τ", ::qc::TAU); - addConstant("euler", ::qc::E); - addConstant("ℇ", ::qc::E); - - const qasm3::GateInfo mcxInfo{.nControls = 0, - .nTargets = 0, - .nParameters = 0, - .type = ::qc::OpType::X}; - gates["mcx"] = std::make_shared(mcxInfo); - gates["mcx_gray"] = std::make_shared(mcxInfo); - gates["mcx_vchain"] = std::make_shared(mcxInfo); - gates["mcx_recursive"] = std::make_shared(mcxInfo); - - const qasm3::GateInfo mcphaseInfo{.nControls = 0, - .nTargets = 0, - .nParameters = 1, - .type = ::qc::OpType::P}; - gates["mcphase"] = std::make_shared(mcphaseInfo); - } - -public: - //===--- InstVisitor overrides ----------------------------------------===// - - void - visitGateStatement(std::shared_ptr stmt) override { - const auto& id = stmt->identifier; - if (stmt->isOpaque) { - if (!gates.contains(id)) { - throw qasm3::CompilerError("Unsupported opaque gate '" + id + "'.", - stmt->debugInfo); - } - return; - } - if (gates.contains(id)) { - throw qasm3::CompilerError("Gate '" + id + "' already declared.", - stmt->debugInfo); - } - std::vector params; - for (const auto& p : stmt->parameters->identifiers) { - const auto& param = p->identifier; - if (std::ranges::find(params, param) != params.end()) { - throw qasm3::CompilerError( - "Parameter is already declared in compound gate.", stmt->debugInfo); - } - params.push_back(param); - } - std::vector targets; - for (const auto& t : stmt->qubits->identifiers) { - const auto& target = t->identifier; - if (std::ranges::find(targets, target) != targets.end()) { - throw qasm3::CompilerError( - "Target is already declared in compound gate.", stmt->debugInfo); - } - targets.push_back(target); - } - gates[id] = std::make_shared( - std::move(params), std::move(targets), stmt->statements); - } - - void visitVersionDeclaration(const std::shared_ptr - versionDeclaration) override { - if (versionDeclaration->version < 3) { - openQASM2CompatMode = true; - } - } - - void visitDeclarationStatement( - std::shared_ptr stmt) override { - const auto& id = stmt->identifier; - if (declarations.find(id).has_value()) { - throw qasm3::CompilerError("Identifier '" + id + "' already declared.", - stmt->debugInfo); - } - declarations.emplace(id, stmt); - - if (stmt->isConst) { - // Nothing to emit - return; - } - - const auto sizedType = - std::dynamic_pointer_cast>( - std::get<1>(stmt->type)); - if (!sizedType) { - throw qasm3::CompilerError("Only sized types are supported.", - stmt->debugInfo); - } - const auto size = static_cast(sizedType->getDesignator()); - - switch (sizedType->type) { - case qasm3::Qubit: { - const auto& reg = builder.allocQubitRegister(size); - qubitRegisters[id] = reg.qubits; - break; - } - case qasm3::Bit: - case qasm3::Int: - case qasm3::Uint: { - classicalRegisters[id] = builder.allocClassicalBitRegister(size, id); - if (sizedType->type == qasm3::Bit) { - allBitRegisters.push_back(id); - if (stmt->isOutput || openQASM2CompatMode) { - // We return `output` bits in QASM3, or all named bits in QASM2. - outputRegisters.push_back(id); - } - } - break; - } - default: - throw qasm3::CompilerError("Unsupported declaration type.", - stmt->debugInfo); - } - - // Handle declarations through measure expressions - if (stmt->expression) { - const auto& innerExpr = stmt->expression->expression; - if (const auto measureExpr = - std::dynamic_pointer_cast(innerExpr)) { - auto target = std::make_shared(id); - visitMeasureAssignment(target, measureExpr, stmt->debugInfo); - return; - } - throw qasm3::CompilerError( - "Only measure expressions can declare variables.", stmt->debugInfo); - } - } - - void visitInitialLayout( - std::shared_ptr /*initialLayout*/) override {} - - void visitOutputPermutation( - std::shared_ptr /*outputPermutation*/) - override {} - - void visitGateCallStatement( - std::shared_ptr stmt) override { - applyGateCallStatement(stmt, qubitRegisters); - } - - void visitAssignmentStatement( - std::shared_ptr stmt) override { - const auto& innerId = stmt->identifier->identifier; - assert(declarations.find(innerId).has_value()); - assert(!declarations.find(innerId)->get()->isConst); - - const auto& innerExpr = stmt->expression->expression; - if (const auto measureExpr = - std::dynamic_pointer_cast(innerExpr)) { - visitMeasureAssignment(stmt->identifier, measureExpr, stmt->debugInfo); - return; - } - - throw qasm3::CompilerError("Classical computations are not supported.", - stmt->debugInfo); - } - - void visitMeasureAssignment( - const std::shared_ptr& target, - const std::shared_ptr& measureExpr, - const std::shared_ptr& debugInfo) { - const auto& bits = resolveClassicalBits(target, debugInfo); - const auto& operand = resolveGateOperand(measureExpr->gate, debugInfo); - SmallVector qubits; - if (std::holds_alternative(operand)) { - qubits.push_back(std::get(operand)); - } else { - qubits = std::get>(operand); - } - if (bits.size() != qubits.size()) { - throw qasm3::CompilerError("The classical register and the quantum " - "register must have the same width.", - debugInfo); - } - for (const auto& [bit, qubit] : llvm::zip_equal(bits, qubits)) { - auto result = MeasureOp::create( - builder, qubit, builder.getStringAttr(bit.registerName), - builder.getI64IntegerAttr(bit.registerSize), - builder.getI64IntegerAttr(bit.registerIndex)) - .getResult(); - auto& regBits = bitValues[bit.registerName]; - const auto index = static_cast(bit.registerIndex); - if (regBits.size() <= index) { - regBits.resize(index + 1); - } - regBits[index] = result; - } - } - - void visitBarrierStatement( - std::shared_ptr stmt) override { - SmallVector qubits; - for (const auto& gate : stmt->gates) { - const auto& operand = resolveGateOperand(gate, stmt->debugInfo); - if (std::holds_alternative(operand)) { - qubits.push_back(std::get(operand)); - } else { - llvm::append_range(qubits, std::get>(operand)); - } - } - builder.barrier(qubits); - } - - void - visitResetStatement(std::shared_ptr stmt) override { - const auto& operand = resolveGateOperand(stmt->gate, stmt->debugInfo); - if (std::holds_alternative(operand)) { - builder.reset(std::get(operand)); - } else { - for (auto qubit : std::get>(operand)) { - builder.reset(qubit); - } - } - } - - void visitIfStatement(std::shared_ptr stmt) override { - if (stmt->thenStatements.empty() && stmt->elseStatements.empty()) { - throw qasm3::CompilerError( - "If statements with empty then and else blocks are not supported.", - stmt->debugInfo); - } - - auto condition = translateCondition(stmt->condition, stmt->debugInfo); - auto hasElse = !stmt->elseStatements.empty(); - - std::vector> thenStatements; - if (stmt->thenStatements.empty()) { - thenStatements = stmt->elseStatements; - hasElse = false; - auto trueValue = builder.boolConstant(true); - condition = - arith::XOrIOp::create(builder, condition, trueValue).getResult(); - } else { - thenStatements = stmt->thenStatements; - } - - auto ifOp = - scf::IfOp::create(builder, condition, /*withElseRegion=*/hasElse); - - // Save current insertion point - OpBuilder::InsertionGuard guard(builder); - - // Then block - builder.setInsertionPointToStart(&ifOp.getThenRegion().front()); - emitBlockStatements(thenStatements, stmt->debugInfo); - - // Else block - if (hasElse) { - builder.setInsertionPointToStart(&ifOp.getElseRegion().front()); - emitBlockStatements(stmt->elseStatements, stmt->debugInfo); - } - } - - //===--- Core gate application ----------------------------------------===// - - /** - * @brief Apply a GateCallStatement by emitting the corresponding QC - * operations. - * - * @param stmt The GateCallStatement to apply. - * @param scope The current qubit scope for resolving operands. If called from - * the main visitor, this is the top-level qubitRegisters map. If called - * recursively for a compound gate, this is the local scope of the - * CompoundGate. - */ - void - applyGateCallStatement(const std::shared_ptr& stmt, - const QubitScope& scope) { - const auto& id = stmt->identifier; - auto it = gates.find(id); - - // OpenQASM 2 compatibility: - // Strip leading c characters and treat them as implicit control modifiers - auto resolvedId = id; - size_t numCompatControls = 0; - if (openQASM2CompatMode && it == gates.end()) { - while (!resolvedId.empty() && resolvedId.front() == 'c') { - resolvedId = resolvedId.substr(1); - ++numCompatControls; - } - if (numCompatControls > 0) { - it = gates.find(resolvedId); - } - } - - if (it == gates.end()) { - throw qasm3::CompilerError("No OpenQASM definition found for gate '" + - id + "'.", - stmt->debugInfo); - } - - // Evaluate parameters to doubles - SmallVector params; - params.reserve(stmt->arguments.size()); - for (const auto& arg : stmt->arguments) { - auto result = constEvalPass.visit(arg); - if (!result.has_value()) { - throw qasm3::CompilerError("Gate parameter could not be evaluated.", - stmt->debugInfo); - } - params.push_back(result->toExpr()->asFP()); - } - - // Expand operands to MLIR values - SmallVector operands; - SmallVector> operandsBroadcasting; - auto broadcasting = false; - for (const auto& operand : stmt->operands) { - const auto& resolvedOperand = - resolveGateOperandInScope(operand, scope, stmt->debugInfo); - if (const auto* operand = std::get_if(&resolvedOperand)) { - operands.push_back(*operand); - } else if (const auto* operand = - std::get_if>(&resolvedOperand)) { - operandsBroadcasting.push_back(*operand); - broadcasting = true; - } - } - - if (broadcasting && !operands.empty()) { - throw qasm3::CompilerError("Gate operands must be single qubits or " - "quantum registers and not a mix of both.", - stmt->debugInfo); - } - - if (broadcasting && numCompatControls != 0) { - throw qasm3::CompilerError("OpenQASM 2 gates cannot be broadcasted.", - stmt->debugInfo); - } - - size_t broadcastWidth = 0; - if (broadcasting) { - for (const auto& operand : operandsBroadcasting) { - if (broadcastWidth == 0) { - broadcastWidth = operand.size(); - } else if (broadcastWidth != operand.size()) { - throw qasm3::CompilerError( - "All broadcasting operands must have the same width.", - stmt->debugInfo); - } - } - } - - auto invert = false; - size_t numControls = 0; - SmallVector posControls; - SmallVector negControls; - SmallVector> posControlsBroadcasting; - SmallVector> negControlsBroadcasting; - - // Parse modifiers - for (const auto& mod : stmt->modifiers) { - if (std::dynamic_pointer_cast(mod)) { - invert = !invert; - } else if (const auto* ctrlMod = - dynamic_cast(mod.get())) { - const auto n = - evaluatePositiveConstant(ctrlMod->expression, stmt->debugInfo, 1); - for (size_t i = 0; i < n; ++i, ++numControls) { - const auto positive = ctrlMod->ctrlType; - if (!broadcasting) { - if (numControls >= operands.size()) { - throw qasm3::CompilerError("Control index out of bounds.", - stmt->debugInfo); - } - auto operand = operands[numControls]; - if (positive) { - posControls.push_back(operand); - } else { - negControls.push_back(operand); - } - } else { - if (numControls >= operandsBroadcasting.size()) { - throw qasm3::CompilerError("Control index out of bounds.", - stmt->debugInfo); - } - const auto& operand = operandsBroadcasting[numControls]; - if (positive) { - posControlsBroadcasting.push_back(operand); - } else { - negControlsBroadcasting.push_back(operand); - } - } - } - } else { - throw qasm3::CompilerError( - "Only ctrl, negctrl, and inv modifiers are supported.", - stmt->debugInfo); - } - } - - // OpenQASM 2 compatibility: - // Append implicit control qubits - for (size_t i = 0; i < numCompatControls; ++i, ++numControls) { - if (numControls >= operands.size()) { - throw qasm3::CompilerError("Control index out of bounds.", - stmt->debugInfo); - } - posControls.push_back(operands[numControls]); - } - - // Remaining operands are target qubits - SmallVector targets; - SmallVector> targetsBroadcasting; - if (!broadcasting) { - targets = llvm::to_vector(llvm::drop_begin(operands, numControls)); - } else { - targetsBroadcasting = - llvm::to_vector(llvm::drop_begin(operandsBroadcasting, numControls)); - } - - // Inline compound gate - if (const auto* compound = - dynamic_cast(it->second.get())) { - if (broadcasting) { - throw qasm3::CompilerError( - "Broadcasted compound gates are not supported.", stmt->debugInfo); - } - applyCompoundGate(*compound, params, targets, posControls, negControls, - invert, stmt->debugInfo); - return; - } - - // Emit standard gate - const auto dispIt = GATE_DISPATCH.find(resolvedId); - if (dispIt == GATE_DISPATCH.end()) { - throw qasm3::CompilerError( - "No MLIR definition found for gate '" + id + "'.", stmt->debugInfo); - } - - if (it->second->getNParameters() != params.size()) { - throw qasm3::CompilerError("Invalid number of parameters for gate '" + - id + "'.", - stmt->debugInfo); - } - - if (!broadcasting) { - emitGate(dispIt->second, params, targets, posControls, negControls, - invert); - } else { - for (size_t b = 0; b < broadcastWidth; ++b) { - SmallVector bTargets; - bTargets.reserve(targetsBroadcasting.size()); - for (const auto& target : targetsBroadcasting) { - bTargets.push_back(target[b]); - } - SmallVector bPosControls; - bPosControls.reserve(posControlsBroadcasting.size()); - for (const auto& ctrl : posControlsBroadcasting) { - bPosControls.push_back(ctrl[b]); - } - SmallVector bNegControls; - bNegControls.reserve(negControlsBroadcasting.size()); - for (const auto& ctrl : negControlsBroadcasting) { - bNegControls.push_back(ctrl[b]); - } - emitGate(dispIt->second, params, bTargets, bPosControls, bNegControls, - invert); - } - } - } - - /// Helper function to build a gate with potential modifiers. - void buildModifiedGate(function_ref bodyFn, - ValueRange targets, ValueRange posControls, - ValueRange negControls, bool invert) { - auto wrappedBodyFn = [&](ValueRange qubits) { - if (invert) { - builder.inv(qubits, function_ref(bodyFn)); - } else { - bodyFn(qubits); - } - }; - - if (posControls.empty() && negControls.empty()) { - wrappedBodyFn(targets); - return; - } - - SmallVector controls; - controls.append(posControls.begin(), posControls.end()); - controls.append(negControls.begin(), negControls.end()); - - for (auto control : negControls) { - builder.x(control); - } - builder.ctrl(controls, targets, - function_ref(wrappedBodyFn)); - for (auto control : negControls) { - builder.x(control); - } - } - - /// Emit a standard gate. - void emitGate(const GateFn& gateFn, const SmallVector& params, - ValueRange targets, ValueRange posControls, - ValueRange negControls, bool invert) { - auto bodyFn = [&](ValueRange qubits) { gateFn(builder, qubits, params); }; - buildModifiedGate(bodyFn, targets, posControls, negControls, invert); - } - - /// Inline a compound gate. - void applyCompoundGate(const qasm3::CompoundGate& gate, - const SmallVector& params, ValueRange targets, - ValueRange posControls, ValueRange negControls, - bool invert, - const std::shared_ptr& debugInfo) { - assert(gate.parameterNames.size() == params.size()); - assert(gate.targetNames.size() == targets.size()); - - // Map from internal target name to index in targets list. This map is - // needed because the qubits may be aliased if the CompoundGate is inlined - // within a modifier region. - llvm::StringMap> targetsMap; - - for (const auto& [targetName, target] : - llvm::zip_equal(gate.targetNames, targets)) { - auto it = llvm::find(targets, target); - if (it == targets.end()) { - throw qasm3::CompilerError( - "Target '" + targetName + "' not found in operands.", debugInfo); - } - const auto index = - static_cast(std::distance(targets.begin(), it)); - targetsMap[targetName].push_back(index); - } - - // Bind parameters as constants - constEvalPass.pushEnv(); - for (size_t i = 0; i < gate.parameterNames.size(); ++i) { - constEvalPass.addConst(gate.parameterNames[i], - qasm3::const_eval::ConstEvalValue(params[i])); - } - - auto bodyFn = [&](ValueRange qubits) { - QubitScope localScope; - for (const auto& [name, indices] : targetsMap) { - SmallVector args; - for (auto index : indices) { - args.push_back(qubits[index]); - } - localScope[name] = std::move(args); - } - for (const auto& stmt : gate.body) { - if (const auto gateCall = - std::dynamic_pointer_cast(stmt)) { - applyGateCallStatement(gateCall, localScope); - continue; - } - throw qasm3::CompilerError("Compound operations with non-quantum " - "statements are not supported.", - debugInfo); - } - }; - - buildModifiedGate(bodyFn, targets, posControls, negControls, invert); - - constEvalPass.popEnv(); - } - - //===--- IfStatement helpers ------------------------------------------===// - - /// Helper function to emit quantum statements within an IfOp's then/else - /// regions. - void emitBlockStatements( - const std::vector>& statements, - const std::shared_ptr& debugInfo) { - for (const auto& stmt : statements) { - if (const auto gateCall = - std::dynamic_pointer_cast(stmt)) { - applyGateCallStatement(gateCall, qubitRegisters); - continue; - } - throw qasm3::CompilerError( - "If statements with non-quantum statements are not supported.", - debugInfo); - } - } - - /// Translate an OpenQASM 3 condition to MLIR. - [[nodiscard]] Value - translateCondition(const std::shared_ptr& condition, - const std::shared_ptr& debugInfo) { - // Single bit (c[0]) - if (const auto& id = - std::dynamic_pointer_cast(condition)) { - return lookupBitValue(id, debugInfo); - } - - // Unary negation (!c[0] or ~c[0]) - if (const auto unaryExpr = - std::dynamic_pointer_cast(condition)) { - if (unaryExpr->op != qasm3::UnaryExpression::LogicalNot && - unaryExpr->op != qasm3::UnaryExpression::BitwiseNot) { - throw qasm3::CompilerError( - "Only ! and ~ are supported in if statements.", debugInfo); - } - const auto& id = std::dynamic_pointer_cast( - unaryExpr->operand); - if (!id) { - throw qasm3::CompilerError("Unary expression has unsupported operand.", - debugInfo); - } - auto value = lookupBitValue(id, debugInfo); - auto trueValue = builder.boolConstant(true); - return arith::XOrIOp::create(builder, value, trueValue).getResult(); - } - - // Register comparison (creg == N, creg != N, etc.) - if (const auto binaryExpr = - std::dynamic_pointer_cast(condition)) { - throw qasm3::CompilerError("Register comparisons are not supported.", - debugInfo); - } - - throw qasm3::CompilerError( - "Unsupported condition expression in if statement.", debugInfo); - } - - /// Look up the most recent measurement result for a classical bit. - [[nodiscard]] Value - lookupBitValue(const std::shared_ptr& id, - const std::shared_ptr& debugInfo) const { - const auto& regName = id->identifier; - auto it = bitValues.find(regName); - if (it == bitValues.end()) { - throw qasm3::CompilerError("No classical bit of register '" + regName + - "' has been measured yet.", - debugInfo); - } - const auto& regBits = it->second; - - if (id->indices.empty()) { - assert(regBits.size() == 1); - return regBits[0]; - } - - if (id->indices.size() != 1 || - id->indices[0]->indexExpressions.size() != 1) { - throw qasm3::CompilerError("Only single-index expressions are supported.", - debugInfo); - } - const auto& indexExpression = id->indices[0]->indexExpressions[0]; - const auto index = evaluatePositiveConstant(indexExpression, debugInfo); - if (index >= regBits.size() || !regBits[index]) { - throw qasm3::CompilerError("Bit " + std::to_string(index) + - " of register '" + regName + - "' has been not measured yet.", - debugInfo); - } - return regBits[index]; - } - - //===--- Operand resolution helpers ------------------------------------===// - - /** - * @brief Resolve a qubit operand against the top-level qubitRegisters map. - * - * @return A variant containing - * - a `Value` if the operand is, e.g., `q[0]`, - * - a `Value` if the operand `q` is a single-qubit register, or - * - a `SmallVector` if the operand `q` is a multi-qubit register. - */ - [[nodiscard]] std::variant> - resolveGateOperand(const std::shared_ptr& operand, - const std::shared_ptr& debugInfo) { - return resolveGateOperandInScope(operand, qubitRegisters, debugInfo); - } - - /** - * @brief Resolve a qubit operand against @p scope. - * - * @return A variant containing - * - a `Value` if the operand is, e.g., `q[0]`, - * - a `Value` if the operand `q` is a single-qubit register, or - * - a `SmallVector` if the operand `q` is a multi-qubit register. - */ - [[nodiscard]] std::variant> - resolveGateOperandInScope( - const std::shared_ptr& operand, - const QubitScope& scope, - const std::shared_ptr& debugInfo) { - if (operand->isHardwareQubit()) { - return builder.staticQubit(operand->getHardwareQubit()); - } - - const auto& id = operand->getIdentifier(); - const auto& name = id->identifier; - auto it = scope.find(name); - if (it == scope.end()) { - throw qasm3::CompilerError("Unknown qubit register '" + name + "'.", - debugInfo); - } - - const auto& qubits = it->second; - - if (id->indices.empty()) { - if (qubits.size() == 1) { - return qubits[0]; - } - // Return full register - return qubits; - } - - if (id->indices.size() != 1 || - id->indices[0]->indexExpressions.size() != 1) { - throw qasm3::CompilerError("Only single-index expressions are supported.", - debugInfo); - } - const auto& indexExpression = id->indices[0]->indexExpressions[0]; - const auto index = evaluatePositiveConstant(indexExpression, debugInfo); - if (index >= qubits.size()) { - throw qasm3::CompilerError("Qubit index out of bounds.", debugInfo); - } - return qubits[index]; - } - - /// Resolve a classical bit operand. - [[nodiscard]] SmallVector resolveClassicalBits( - const std::shared_ptr& operand, - const std::shared_ptr& debugInfo) const { - const auto& name = operand->identifier; - auto it = classicalRegisters.find(name); - if (it == classicalRegisters.end()) { - throw qasm3::CompilerError("Unknown classical register '" + name + "'.", - debugInfo); - } - - const auto& creg = it->second; - SmallVector bits; - - if (operand->indices.empty()) { - for (int64_t i = 0; i < creg.size; ++i) { - bits.push_back(creg[i]); - } - return bits; - } - - if (operand->indices.size() != 1 || - operand->indices[0]->indexExpressions.size() != 1) { - throw qasm3::CompilerError("Only single-index expressions are supported.", - debugInfo); - } - const auto& indexExpression = operand->indices[0]->indexExpressions[0]; - const auto index = evaluatePositiveConstant(indexExpression, debugInfo); - if (std::cmp_greater_equal(index, creg.size)) { - throw qasm3::CompilerError("Classical bit index out of bounds.", - debugInfo); - } - bits.push_back(creg[static_cast(index)]); - return bits; - } - - /// Evaluate a constant expression to a positive integer. - static size_t - evaluatePositiveConstant(const std::shared_ptr& expr, - const std::shared_ptr& debugInfo, - size_t defaultValue = 0) { - if (!expr) { - return defaultValue; - } - const auto constVal = std::dynamic_pointer_cast(expr); - if (!constVal) { - throw qasm3::CompilerError("Expected a constant integer expression.", - debugInfo); - } - return static_cast(constVal->getUInt()); - } -}; - } // namespace -//===----------------------------------------------------------------------===// -// Public API -//===----------------------------------------------------------------------===// - OwningOpRef translateQASM3ToQC(llvm::SourceMgr& sourceMgr, MLIRContext* context) { - try { - auto buffer = - sourceMgr.getMemoryBuffer(sourceMgr.getMainFileID())->getBuffer(); - std::string_view view(buffer.data(), buffer.size()); - std::istringstream input((std::string(view))); - - qasm3::Parser parser(input); - const auto program = parser.parseProgram(); - - MLIRQasmImporter importer(context); - importer.visitProgram(program); - return importer.finalize(); - } catch (const qasm3::CompilerError& e) { - llvm::errs() << "Import error: " << e.what() << "\n"; + auto analyzed = oq3::frontend::analyzeOpenQASM(sourceMgr); + if (!analyzed) { + printDiagnostics(analyzed.diagnostics); + return nullptr; + } + auto module = detail::emitOpenQASMToQC(*analyzed.program, *context); + if (!module) { return nullptr; - } catch (const std::exception& e) { - llvm::errs() << "Import error: " << e.what() << "\n"; + } + if (failed(verify(*module))) { + llvm::errs() << "OpenQASM emission produced invalid QC IR.\n"; return nullptr; } + return module; } -OwningOpRef translateQASM3ToQC(StringRef source, +OwningOpRef translateQASM3ToQC(const StringRef source, MLIRContext* context) { llvm::SourceMgr sourceMgr; - auto buffer = llvm::MemoryBuffer::getMemBufferCopy(source); - sourceMgr.AddNewSourceBuffer(std::move(buffer), SMLoc()); + sourceMgr.AddNewSourceBuffer( + llvm::MemoryBuffer::getMemBufferCopy(source, ""), llvm::SMLoc()); return translateQASM3ToQC(sourceMgr, context); } diff --git a/mlir/lib/Target/CMakeLists.txt b/mlir/lib/Target/CMakeLists.txt new file mode 100644 index 0000000000..7cecfeee4b --- /dev/null +++ b/mlir/lib/Target/CMakeLists.txt @@ -0,0 +1,9 @@ +# Copyright (c) 2023 - 2026 Chair for Design Automation, TUM +# Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH +# All rights reserved. +# +# SPDX-License-Identifier: MIT +# +# Licensed under the MIT License + +add_subdirectory(OpenQASM) diff --git a/mlir/lib/Target/OpenQASM/CMakeLists.txt b/mlir/lib/Target/OpenQASM/CMakeLists.txt new file mode 100644 index 0000000000..9eaaf06312 --- /dev/null +++ b/mlir/lib/Target/OpenQASM/CMakeLists.txt @@ -0,0 +1,29 @@ +# Copyright (c) 2023 - 2026 Chair for Design Automation, TUM +# Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH +# All rights reserved. +# +# SPDX-License-Identifier: MIT +# +# Licensed under the MIT License + +add_mlir_library( + MLIROpenQASMFrontend + Frontend.cpp + GateCatalog.cpp + OpenQASMLexer.cpp + OpenQASMSemantics.cpp + OpenQASMSyntax.cpp + LINK_LIBS + LLVMSupport) + +mqt_mlir_target_use_project_options(MLIROpenQASMFrontend) + +file(GLOB OPENQASM_HEADERS_SOURCE ${MQT_MLIR_SOURCE_INCLUDE_DIR}/mlir/Target/OpenQASM/*.h) +file(GLOB_RECURSE OPENQASM_DETAIL_HEADERS_SOURCE + ${MQT_MLIR_SOURCE_INCLUDE_DIR}/mlir/Target/OpenQASM/Detail/*.h) + +target_sources( + MLIROpenQASMFrontend PUBLIC FILE_SET HEADERS BASE_DIRS ${MQT_MLIR_SOURCE_INCLUDE_DIR} FILES + ${OPENQASM_HEADERS_SOURCE}) + +target_sources(MLIROpenQASMFrontend PRIVATE ${OPENQASM_DETAIL_HEADERS_SOURCE}) diff --git a/mlir/lib/Target/OpenQASM/Frontend.cpp b/mlir/lib/Target/OpenQASM/Frontend.cpp new file mode 100644 index 0000000000..a55ae9017c --- /dev/null +++ b/mlir/lib/Target/OpenQASM/Frontend.cpp @@ -0,0 +1,317 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#include "mlir/Target/OpenQASM/Frontend.h" + +#include "mlir/Target/OpenQASM/Detail/OpenQASMLexer.h" +#include "mlir/Target/OpenQASM/Detail/OpenQASMParser.h" +#include "mlir/Target/OpenQASM/Detail/OpenQASMSemantics.h" +#include "mlir/Target/OpenQASM/Detail/OpenQASMSyntax.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace mlir::oq3::frontend { + +struct ParsedProgram::Impl { + std::unique_ptr sources; + detail::SyntaxProgram syntax; +}; + +ParsedProgram::ParsedProgram(std::unique_ptr implementation) + : impl(std::move(implementation)) {} +ParsedProgram::ParsedProgram(ParsedProgram&&) noexcept = default; +ParsedProgram& ParsedProgram::operator=(ParsedProgram&&) noexcept = default; +ParsedProgram::~ParsedProgram() = default; + +namespace { + +struct ParseArtifacts { + std::unique_ptr sources; + detail::SyntaxProgram syntax; + std::vector diagnostics; +}; + +constexpr std::size_t includeNestingLimit = 64; +constexpr std::size_t expandedStatementLimit = 100'000; + +[[nodiscard]] bool isStandardLibrary(const llvm::StringRef filename) { + return filename == "stdgates.inc" || filename == "qelib1.inc"; +} + +ParseArtifacts parseBuffer(std::unique_ptr buffer, + const llvm::SourceMgr* providedSources = nullptr) { + ParseArtifacts result; + auto sources = std::make_unique(); + if (providedSources != nullptr) { + sources->setVirtualFileSystem(providedSources->getVirtualFileSystem()); + sources->setIncludeDirs( + std::vector(providedSources->getIncludeDirs())); + } + const auto mainBufferId = + sources->AddNewSourceBuffer(std::move(buffer), llvm::SMLoc()); + if (providedSources != nullptr) { + for (unsigned id = 2; id <= providedSources->getNumBuffers(); ++id) { + const auto* included = providedSources->getMemoryBuffer(id); + sources->AddNewSourceBuffer( + llvm::MemoryBuffer::getMemBufferCopy(included->getBuffer(), + included->getBufferIdentifier()), + llvm::SMLoc()); + } + } + + llvm::BumpPtrAllocator allocator; + detail::SyntaxBuilder builder; + bool failedParsing = false; + const auto reportIncludeNestingLimit = [&](const llvm::SMLoc location) { + (void)builder.error( + location, llvm::Twine("include nesting exceeds the limit of ") + + llvm::Twine(static_cast(includeNestingLimit))); + failedParsing = true; + }; + const auto reportStatementLimit = [&](const llvm::SMLoc location) { + (void)builder.error( + location, + llvm::Twine( + "expanded OpenQASM program exceeds the statement limit of ") + + llvm::Twine(static_cast(expandedStatementLimit))); + failedParsing = true; + }; + struct ParsedSource { + std::size_t bodyBegin = 0; + std::size_t bodyEnd = 0; + std::size_t includeBegin = 0; + std::size_t includeEnd = 0; + }; + llvm::DenseMap parsedSources; + llvm::StringMap includeBuffers; + for (unsigned id = 2; id <= sources->getNumBuffers(); ++id) { + includeBuffers.try_emplace( + sources->getMemoryBuffer(id)->getBufferIdentifier(), id); + } + const auto parseSource = [&](const unsigned bufferId) { + const auto bodyBegin = builder.getBody().size(); + const auto includeBegin = builder.getIncludes().size(); + detail::Lexer lexer(sources->getMemoryBuffer(bufferId)->getBuffer()); + detail::Parser parser(lexer, builder, allocator); + if (failed(parser.parseProgram())) { + failedParsing = true; + } + parsedSources.try_emplace( + bufferId, ParsedSource{.bodyBegin = bodyBegin, + .bodyEnd = builder.getBody().size(), + .includeBegin = includeBegin, + .includeEnd = builder.getIncludes().size()}); + }; + parseSource(mainBufferId); + + llvm::SmallSet parsedBuffers; + llvm::SmallVector includeTargets; + llvm::SmallVector includeDepths(builder.getIncludes().size(), 1); + parsedBuffers.insert(mainBufferId); + for (std::size_t includeIndex = 0; + includeIndex < builder.getIncludes().size(); ++includeIndex) { + includeTargets.resize(builder.getIncludes().size()); + const auto include = builder.getIncludes()[includeIndex]; + if (includeDepths[includeIndex] > includeNestingLimit) { + reportIncludeNestingLimit(include.location); + continue; + } + if (isStandardLibrary(include.filename)) { + continue; + } + auto bufferId = includeBuffers.lookup(include.filename); + if (bufferId == 0) { + std::string includedPath; + auto included = + sources->OpenIncludeFile(include.filename.str(), includedPath); + if (included) { + bufferId = includeBuffers.lookup(includedPath); + if (bufferId == 0) { + bufferId = sources->AddNewSourceBuffer(std::move(*included), + include.location); + includeBuffers[includedPath] = bufferId; + } + } + } + if (bufferId == 0) { + (void)builder.error(include.location, + llvm::Twine("could not open included file '") + + include.filename + "'"); + failedParsing = true; + continue; + } + if (parsedBuffers.insert(bufferId).second) { + parseSource(bufferId); + includeDepths.resize(builder.getIncludes().size(), + includeDepths[includeIndex] + 1); + } + includeTargets[includeIndex] = bufferId; + } + + std::vector> includeMarkers( + builder.getIncludes().size()); + for (const auto [index, include] : llvm::enumerate(builder.getIncludes())) { + if (isStandardLibrary(include.filename)) { + includeMarkers[index] = builder.standardLibraryInclude(include.location); + } + } + + std::vector expandedBody; + llvm::SmallSet activeBuffers; + const auto appendBodyRange = [&](const std::size_t begin, + const std::size_t end, + const llvm::SMLoc location) { + const auto count = end - begin; + if (count > expandedStatementLimit - expandedBody.size()) { + reportStatementLimit(location); + return false; + } + expandedBody.insert(expandedBody.end(), builder.getBody().begin() + begin, + builder.getBody().begin() + end); + return true; + }; + const auto appendSource = [&](auto&& self, const unsigned bufferId, + const std::size_t depth) -> bool { + activeBuffers.insert(bufferId); + const auto parsed = parsedSources.lookup(bufferId); + auto cursor = parsed.bodyBegin; + for (auto includeIndex = parsed.includeBegin; + includeIndex < parsed.includeEnd; ++includeIndex) { + const auto offset = builder.getIncludes()[includeIndex].bodyOffset; + const auto includeLocation = builder.getIncludes()[includeIndex].location; + if (!appendBodyRange(cursor, offset, includeLocation)) { + activeBuffers.erase(bufferId); + return false; + } + if (includeMarkers[includeIndex]) { + if (expandedBody.size() >= expandedStatementLimit) { + reportStatementLimit(includeLocation); + activeBuffers.erase(bufferId); + return false; + } + expandedBody.push_back(*includeMarkers[includeIndex]); + } else if (includeTargets[includeIndex] != 0) { + const auto target = includeTargets[includeIndex]; + if (activeBuffers.contains(target)) { + (void)builder.error(includeLocation, + "recursive include is not allowed"); + failedParsing = true; + } else if (depth >= includeNestingLimit) { + reportIncludeNestingLimit(includeLocation); + activeBuffers.erase(bufferId); + return false; + } else { + if (!self(self, target, depth + 1)) { + activeBuffers.erase(bufferId); + return false; + } + } + } + cursor = offset; + } + const auto* source = sources->getMemoryBuffer(bufferId); + if (!appendBodyRange( + cursor, parsed.bodyEnd, + llvm::SMLoc::getFromPointer(source->getBufferStart()))) { + activeBuffers.erase(bufferId); + return false; + } + activeBuffers.erase(bufferId); + return true; + }; + (void)appendSource(appendSource, mainBufferId, 0); + builder.replaceBody(std::move(expandedBody)); + + if (failedParsing) { + for (const auto& diagnostic : builder.getDiagnostics()) { + result.diagnostics.push_back( + {.location = detail::sourceLocation(*sources, diagnostic.location), + .message = diagnostic.message}); + } + if (result.diagnostics.empty()) { + result.diagnostics.push_back({.message = "OpenQASM parsing failed"}); + } + return result; + } + + result.sources = std::move(sources); + result.syntax = builder.takeProgram(); + return result; +} + +} // namespace + +ParseResult parseOpenQASM(llvm::SourceMgr& sourceMgr) { + const auto* source = sourceMgr.getMemoryBuffer(sourceMgr.getMainFileID()); + auto parsed = + parseBuffer(llvm::MemoryBuffer::getMemBufferCopy( + source->getBuffer(), source->getBufferIdentifier()), + &sourceMgr); + if (!parsed.sources) { + return {.diagnostics = std::move(parsed.diagnostics)}; + } + auto implementation = std::make_unique(); + implementation->sources = std::move(parsed.sources); + implementation->syntax = std::move(parsed.syntax); + return {.program = std::unique_ptr( + new ParsedProgram(std::move(implementation)))}; +} + +ParseResult parseOpenQASM(const llvm::StringRef source) { + auto parsed = + parseBuffer(llvm::MemoryBuffer::getMemBufferCopy(source, "")); + if (!parsed.sources) { + return {.diagnostics = std::move(parsed.diagnostics)}; + } + auto implementation = std::make_unique(); + implementation->sources = std::move(parsed.sources); + implementation->syntax = std::move(parsed.syntax); + return {.program = std::unique_ptr( + new ParsedProgram(std::move(implementation)))}; +} + +AnalysisResult analyzeOpenQASM(const ParsedProgram& parsedProgram, + const FrontendOptions& options) { + return detail::analyzeSyntaxProgram(parsedProgram.impl->syntax, + *parsedProgram.impl->sources, options); +} + +AnalysisResult analyzeOpenQASM(llvm::SourceMgr& sourceMgr, + const FrontendOptions& options) { + auto parsed = parseOpenQASM(sourceMgr); + if (!parsed) { + return {.diagnostics = std::move(parsed.diagnostics)}; + } + return analyzeOpenQASM(*parsed.program, options); +} + +AnalysisResult analyzeOpenQASM(const llvm::StringRef source, + const FrontendOptions& options) { + auto parsed = parseOpenQASM(source); + if (!parsed) { + return {.diagnostics = std::move(parsed.diagnostics)}; + } + return analyzeOpenQASM(*parsed.program, options); +} + +} // namespace mlir::oq3::frontend diff --git a/mlir/lib/Target/OpenQASM/GateCatalog.cpp b/mlir/lib/Target/OpenQASM/GateCatalog.cpp new file mode 100644 index 0000000000..dc146ca775 --- /dev/null +++ b/mlir/lib/Target/OpenQASM/GateCatalog.cpp @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#include "mlir/Target/OpenQASM/GateCatalog.h" + +#include + +#include + +namespace mlir::oq3::frontend { +namespace { + +using Availability = GateAvailability; + +constexpr std::array CATALOG{ + GateCatalogEntry{"gphase", "gphase", 1, 0, 0, Availability::Language}, + GateCatalogEntry{"U", "U", 3, 0, 1, Availability::Language}, + GateCatalogEntry{"id", "id", 0, 0, 1, Availability::StandardLibrary}, + GateCatalogEntry{"x", "x", 0, 0, 1, Availability::StandardLibrary}, + GateCatalogEntry{"y", "y", 0, 0, 1, Availability::StandardLibrary}, + GateCatalogEntry{"z", "z", 0, 0, 1, Availability::StandardLibrary}, + GateCatalogEntry{"h", "h", 0, 0, 1, Availability::StandardLibrary}, + GateCatalogEntry{"s", "s", 0, 0, 1, Availability::StandardLibrary}, + GateCatalogEntry{"sdg", "sdg", 0, 0, 1, Availability::StandardLibrary}, + GateCatalogEntry{"t", "t", 0, 0, 1, Availability::StandardLibrary}, + GateCatalogEntry{"tdg", "tdg", 0, 0, 1, Availability::StandardLibrary}, + GateCatalogEntry{"sx", "sx", 0, 0, 1, Availability::StandardLibrary}, + GateCatalogEntry{"p", "p", 1, 0, 1, Availability::StandardLibrary}, + GateCatalogEntry{"rx", "rx", 1, 0, 1, Availability::StandardLibrary}, + GateCatalogEntry{"ry", "ry", 1, 0, 1, Availability::StandardLibrary}, + GateCatalogEntry{"rz", "rz", 1, 0, 1, Availability::StandardLibrary}, + GateCatalogEntry{"r", "r", 2, 0, 1, Availability::StandardLibrary}, + GateCatalogEntry{"swap", "swap", 0, 0, 2, Availability::StandardLibrary}, + GateCatalogEntry{"cx", "x", 0, 1, 1, Availability::StandardLibrary}, + GateCatalogEntry{"cy", "y", 0, 1, 1, Availability::StandardLibrary}, + GateCatalogEntry{"cz", "z", 0, 1, 1, Availability::StandardLibrary}, + GateCatalogEntry{"ch", "h", 0, 1, 1, Availability::StandardLibrary}, + GateCatalogEntry{"cp", "p", 1, 1, 1, Availability::StandardLibrary}, + GateCatalogEntry{"crx", "rx", 1, 1, 1, Availability::StandardLibrary}, + GateCatalogEntry{"cry", "ry", 1, 1, 1, Availability::StandardLibrary}, + GateCatalogEntry{"crz", "rz", 1, 1, 1, Availability::StandardLibrary}, + GateCatalogEntry{"ccx", "x", 0, 2, 1, Availability::StandardLibrary}, + GateCatalogEntry{"cswap", "swap", 0, 1, 2, Availability::StandardLibrary}, + GateCatalogEntry{"cu", "U", 4, 1, 1, Availability::StandardLibrary}, + GateCatalogEntry{"u1", "p", 1, 0, 1, Availability::Compatibility}, + GateCatalogEntry{"cu1", "p", 1, 1, 1, Availability::Compatibility}, + GateCatalogEntry{"phase", "p", 1, 0, 1, Availability::Compatibility}, + GateCatalogEntry{"cphase", "p", 1, 1, 1, Availability::Compatibility}, + GateCatalogEntry{"u2", "u2", 2, 0, 1, Availability::Compatibility}, + GateCatalogEntry{"u3", "U", 3, 0, 1, Availability::Compatibility}, + GateCatalogEntry{"u", "U", 3, 0, 1, Availability::Compatibility}, + GateCatalogEntry{"cu3", "U", 3, 1, 1, Availability::Compatibility}, + GateCatalogEntry{"CX", "x", 0, 1, 1, Availability::Compatibility}, + GateCatalogEntry{"cnot", "x", 0, 1, 1, Availability::Compatibility}, + GateCatalogEntry{"c3x", "x", 0, 3, 1, Availability::Compatibility}, + GateCatalogEntry{"c4x", "x", 0, 4, 1, Availability::Compatibility}, + GateCatalogEntry{"csx", "sx", 0, 1, 1, Availability::Compatibility}, + GateCatalogEntry{"sxdg", "sxdg", 0, 0, 1, Availability::Compatibility}, + GateCatalogEntry{"c3sqrtx", "sxdg", 0, 3, 1, Availability::Compatibility}, + GateCatalogEntry{"prx", "r", 2, 0, 1, Availability::Compatibility}, + GateCatalogEntry{"cr", "r", 2, 1, 1, Availability::Compatibility}, + GateCatalogEntry{"fredkin", "swap", 0, 1, 2, Availability::Compatibility}, + GateCatalogEntry{"iswap", "iswap", 0, 0, 2, Availability::Compatibility}, + GateCatalogEntry{"iswapdg", "iswap", 0, 0, 2, Availability::Compatibility, + false, true}, + GateCatalogEntry{"dcx", "dcx", 0, 0, 2, Availability::Compatibility}, + GateCatalogEntry{"ecr", "ecr", 0, 0, 2, Availability::Compatibility}, + GateCatalogEntry{"rxx", "rxx", 1, 0, 2, Availability::Compatibility}, + GateCatalogEntry{"ryy", "ryy", 1, 0, 2, Availability::Compatibility}, + GateCatalogEntry{"rzx", "rzx", 1, 0, 2, Availability::Compatibility}, + GateCatalogEntry{"rzz", "rzz", 1, 0, 2, Availability::Compatibility}, + GateCatalogEntry{"xx_plus_yy", "xx_plus_yy", 2, 0, 2, + Availability::Compatibility}, + GateCatalogEntry{"xx_minus_yy", "xx_minus_yy", 2, 0, 2, + Availability::Compatibility}, + GateCatalogEntry{"mcx", "x", 0, 1, 1, Availability::Compatibility, true}, + GateCatalogEntry{"mcx_gray", "x", 0, 1, 1, Availability::Compatibility, + true}, + GateCatalogEntry{"mcx_vchain", "x", 0, 1, 1, Availability::Compatibility, + true}, + GateCatalogEntry{"mcx_recursive", "x", 0, 1, 1, Availability::Compatibility, + true}, + GateCatalogEntry{"mcphase", "p", 1, 1, 1, Availability::Compatibility, + true}, +}; + +} // namespace + +llvm::ArrayRef getGateCatalog() { return CATALOG; } + +const GateCatalogEntry* lookupGate(const llvm::StringRef name) { + const auto iterator = llvm::find_if( + CATALOG, [&](const GateCatalogEntry& gate) { return gate.name == name; }); + return iterator == CATALOG.end() ? nullptr : &*iterator; +} + +} // namespace mlir::oq3::frontend diff --git a/mlir/lib/Target/OpenQASM/OpenQASMLexer.cpp b/mlir/lib/Target/OpenQASM/OpenQASMLexer.cpp new file mode 100644 index 0000000000..4bb641481a --- /dev/null +++ b/mlir/lib/Target/OpenQASM/OpenQASMLexer.cpp @@ -0,0 +1,541 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#include "mlir/Target/OpenQASM/Detail/OpenQASMLexer.h" + +#include "mlir/Target/OpenQASM/Detail/OpenQASMUnicode.h" + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +// NOLINTBEGIN(cppcoreguidelines-pro-bounds-pointer-arithmetic) +namespace mlir::oq3::frontend::detail { + +[[nodiscard]] static bool canStartIdentifier(char c) { + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_'; +} + +[[nodiscard]] static bool canContinueIdentifier(char c) { + return canStartIdentifier(c) || (c >= '0' && c <= '9'); +} + +[[nodiscard]] static bool isDigit(char c) { return c >= '0' && c <= '9'; } + +[[nodiscard]] static bool isDigitForRadix(const char c, const unsigned radix) { + if (c >= '0' && c <= '9') { + return static_cast(c - '0') < radix; + } + if (c >= 'a' && c <= 'f') { + return static_cast(c - 'a' + 10) < radix; + } + if (c >= 'A' && c <= 'F') { + return static_cast(c - 'A' + 10) < radix; + } + return false; +} + +template +[[nodiscard]] static bool hasValidSeparators(const StringRef text, + IsDigit&& isValidDigit) { + for (std::size_t index = 0; index < text.size(); ++index) { + if (text[index] != '_') { + continue; + } + if (index == 0 || index + 1 == text.size() || + !isValidDigit(text[index - 1]) || !isValidDigit(text[index + 1])) { + return false; + } + } + return true; +} + +struct DecodedCodePoint { + std::uint32_t value = 0; + std::size_t width = 0; +}; + +[[nodiscard]] static std::optional +decodeCodePoint(const char* position, const char* end) { + auto* source = reinterpret_cast(position); + const auto* const begin = source; + const auto* const sourceEnd = reinterpret_cast(end); + llvm::UTF32 codePoint = 0; + if (llvm::convertUTF8Sequence(&source, sourceEnd, &codePoint, + llvm::strictConversion) != llvm::conversionOK) { + return std::nullopt; + } + return DecodedCodePoint{.value = codePoint, + .width = static_cast(source - begin)}; +} + +[[nodiscard]] static TokenKind keywordKind(StringRef text) { + return llvm::StringSwitch(text) + .Case("OPENQASM", TokenKind::OpenQASM) + .Case("include", TokenKind::Include) + .Case("const", TokenKind::Const) + .Case("qubit", TokenKind::Qubit) + .Case("qreg", TokenKind::Qreg) + .Case("bit", TokenKind::Bit) + .Case("creg", TokenKind::CReg) + .Case("gate", TokenKind::Gate) + .Case("opaque", TokenKind::Opaque) + .Case("output", TokenKind::Output) + .Case("barrier", TokenKind::Barrier) + .Case("reset", TokenKind::Reset) + .Case("measure", TokenKind::Measure) + .Case("if", TokenKind::If) + .Case("else", TokenKind::Else) + .Case("for", TokenKind::For) + .Case("while", TokenKind::While) + .Case("in", TokenKind::In) + .Case("gphase", TokenKind::Gphase) + .Case("inv", TokenKind::Inv) + .Case("pow", TokenKind::Pow) + .Case("ctrl", TokenKind::Ctrl) + .Case("negctrl", TokenKind::NegCtrl) + .Case("int", TokenKind::Int) + .Case("uint", TokenKind::Uint) + .Case("bool", TokenKind::Bool) + .Case("float", TokenKind::Float) + .Case("angle", TokenKind::Angle) + .Case("duration", TokenKind::Duration) + .Case("true", TokenKind::True) + .Case("false", TokenKind::False) + .Cases("defcalgrammar", "def", "cal", "defcal", + TokenKind::UnsupportedKeyword) + .Cases("extern", "box", "let", "break", "continue", + TokenKind::UnsupportedKeyword) + .Cases("end", "return", "switch", "case", "default", + TokenKind::UnsupportedKeyword) + .Cases("pragma", "input", "readonly", "mutable", + TokenKind::UnsupportedKeyword) + .Cases("complex", "array", "void", "stretch", + TokenKind::UnsupportedKeyword) + .Cases("durationof", "delay", "im", TokenKind::UnsupportedKeyword) + .Default(TokenKind::Identifier); +} + +StringRef describe(const TokenKind kind) { + switch (kind) { + case TokenKind::Eof: + return "end of input"; + case TokenKind::Error: + return "invalid token"; + case TokenKind::UnterminatedComment: + return "unterminated block comment"; + case TokenKind::UnsupportedKeyword: + return "unsupported reserved keyword"; + case TokenKind::Identifier: + return "identifier"; + case TokenKind::HardwareQubit: + return "hardware qubit"; + case TokenKind::StringLiteral: + return "string literal"; + case TokenKind::IntegerLiteral: + return "integer literal"; + case TokenKind::FloatLiteral: + return "float literal"; + case TokenKind::LParen: + return "'('"; + case TokenKind::RParen: + return "')'"; + case TokenKind::LBracket: + return "'['"; + case TokenKind::RBracket: + return "']'"; + case TokenKind::LBrace: + return "'{'"; + case TokenKind::RBrace: + return "'}'"; + case TokenKind::Comma: + return "','"; + case TokenKind::Semicolon: + return "';'"; + case TokenKind::Colon: + return "':'"; + case TokenKind::Arrow: + return "'->'"; + case TokenKind::At: + return "'@'"; + case TokenKind::Equals: + return "'='"; + default: + return "token"; + } +} + +const char* Lexer::skipTrivia() { + while (!atEnd()) { + const char c = *cur; + if (c == ' ' || c == '\t' || c == '\r' || c == '\n') { + ++cur; + continue; + } + if (c == '/' && peek() == '/') { + // Line comment + cur += 2; + while (!atEnd() && *cur != '\n') { + ++cur; + } + continue; + } + if (c == '/' && peek() == '*') { + // Block comment + const char* start = cur; + cur += 2; + while (!atEnd() && (*cur != '*' || peek() != '/')) { + ++cur; + } + if (atEnd()) { + // Report unterminated block comment + return start; + } + // Consume the closing */ + cur += 2; + continue; + } + break; + } + return nullptr; +} + +Token Lexer::lexIdentifierOrKeyword(const char* start) { + while (!atEnd()) { + if (canContinueIdentifier(*cur)) { + ++cur; + continue; + } + if (static_cast(*cur) < 0x80) { + break; + } + const auto codePoint = decodeCodePoint(cur, end); + if (!codePoint || !isOpenQASMIdentifierCodePoint(codePoint->value)) { + break; + } + cur += codePoint->width; + } + const StringRef text(start, static_cast(cur - start)); + Token token; + token.loc = SMLoc::getFromPointer(start); + token.spelling = text; + token.kind = keywordKind(text); + if (token.kind == TokenKind::Identifier) { + token.identifier = text; + } + return token; +} + +Token Lexer::lexNumber(const char* start) { + const auto isSeparator = [](const char value) { return value == '_'; }; + if (*start == '0' && (peek() == 'b' || peek() == 'B' || peek() == 'o' || + peek() == 'O' || peek() == 'x' || peek() == 'X')) { + const char prefix = peek(); + cur += 2; + const unsigned radix = prefix == 'b' || prefix == 'B' ? 2 + : prefix == 'o' || prefix == 'O' ? 8 + : 16; + const char* digits = cur; + while (!atEnd() && (std::isalnum(static_cast(*cur)) || + isSeparator(*cur))) { + ++cur; + } + const StringRef text(start, static_cast(cur - start)); + const StringRef digitText(digits, static_cast(cur - digits)); + Token token{.kind = TokenKind::IntegerLiteral, + .loc = SMLoc::getFromPointer(start), + .spelling = text}; + llvm::SmallString<32> normalized; + for (const char value : digitText) { + if (!isSeparator(value)) { + normalized.push_back(value); + } + } + if (digitText.empty() || + !hasValidSeparators(digitText, + [radix](const char value) { + return isDigitForRadix(value, radix); + }) || + StringRef(normalized).getAsInteger(radix, token.intValue)) { + token.kind = TokenKind::Error; + } + return token; + } + + bool isFloat = false; + while (!atEnd() && (isDigit(*cur) || isSeparator(*cur))) { + ++cur; + } + if (!atEnd() && *cur == '.') { + isFloat = true; + ++cur; + while (!atEnd() && (isDigit(*cur) || isSeparator(*cur))) { + ++cur; + } + } + if (!atEnd() && (*cur == 'e' || *cur == 'E')) { + isFloat = true; + ++cur; + if (!atEnd() && (*cur == '+' || *cur == '-')) { + ++cur; + } + while (!atEnd() && (isDigit(*cur) || isSeparator(*cur))) { + ++cur; + } + } + const StringRef text(start, static_cast(cur - start)); + Token token; + token.loc = SMLoc::getFromPointer(start); + token.spelling = text; + llvm::SmallString<32> normalized; + for (const char value : text) { + if (!isSeparator(value)) { + normalized.push_back(value); + } + } + const bool invalidSeparators = !hasValidSeparators( + text, [](const char value) { return isDigit(value); }); + if (isFloat) { + token.kind = TokenKind::FloatLiteral; + if (invalidSeparators || + StringRef(normalized).getAsDouble(token.floatValue) || + !std::isfinite(token.floatValue)) { + token.kind = TokenKind::Error; + } + } else { + token.kind = TokenKind::IntegerLiteral; + if (invalidSeparators || + StringRef(normalized).getAsInteger(10, token.intValue)) { + token.kind = TokenKind::Error; + } + } + return token; +} + +Token Lexer::lexString(const char* start, const char quote) { + ++cur; // consume opening quote + const char* contentStart = cur; + while (!atEnd() && *cur != quote) { + ++cur; + } + Token token; + token.loc = SMLoc::getFromPointer(start); + if (atEnd()) { + token.kind = TokenKind::Error; + return token; + } + token.kind = TokenKind::StringLiteral; + token.stringValue = + StringRef(contentStart, static_cast(cur - contentStart)); + ++cur; // consume closing quote + return token; +} + +Token Lexer::lexHardwareQubit(const char* start) { + ++cur; // consume '$' + const char* digitsStart = cur; + while (!atEnd() && isDigit(*cur)) { + ++cur; + } + Token token; + token.loc = SMLoc::getFromPointer(start); + const StringRef digits(digitsStart, static_cast(cur - digitsStart)); + if (digits.empty() || digits.getAsInteger(10, token.intValue)) { + token.kind = TokenKind::Error; + return token; + } + token.kind = TokenKind::HardwareQubit; + return token; +} + +Token Lexer::next() { + const char* unterminatedComment = skipTrivia(); + + Token token; + if (unterminatedComment != nullptr) { + token.loc = SMLoc::getFromPointer(unterminatedComment); + token.kind = TokenKind::UnterminatedComment; + return token; + } + + token.loc = SMLoc::getFromPointer(cur); + if (atEnd()) { + token.kind = TokenKind::Eof; + return token; + } + + const char* start = cur; + const char c = *cur; + + if (canStartIdentifier(c)) { + return lexIdentifierOrKeyword(start); + } + const auto remaining = static_cast(end - cur); + const std::size_t unsupportedHashKeywordWidth = + c == '#' && remaining >= 4 && StringRef(cur, 4) == "#dim" && + (remaining == 4 || !canContinueIdentifier(cur[4])) + ? 4 + : c == '#' && remaining >= 7 && StringRef(cur, 7) == "#pragma" && + (remaining == 7 || !canContinueIdentifier(cur[7])) + ? 7 + : 0; + if (unsupportedHashKeywordWidth != 0) { + cur += unsupportedHashKeywordWidth; + token.kind = TokenKind::UnsupportedKeyword; + token.spelling = StringRef(start, unsupportedHashKeywordWidth); + return token; + } + if (static_cast(c) >= 0x80) { + const auto codePoint = decodeCodePoint(cur, end); + if (codePoint && isOpenQASMIdentifierCodePoint(codePoint->value)) { + return lexIdentifierOrKeyword(start); + } + cur += codePoint ? codePoint->width : 1; + token.kind = TokenKind::Error; + token.spelling = StringRef(start, static_cast(cur - start)); + return token; + } + // A float literal may lead with a dot + if (isDigit(c) || (c == '.' && isDigit(peek()))) { + return lexNumber(start); + } + if (c == '"' || c == '\'') { + return lexString(start, c); + } + if (c == '$') { + return lexHardwareQubit(start); + } + + // Punctuation and operators + const auto single = [&](const TokenKind kind) { + ++cur; + token.kind = kind; + token.spelling = StringRef(start, 1); + return token; + }; + const auto twoChar = [&](const TokenKind kind) { + cur += 2; + token.kind = kind; + token.spelling = StringRef(start, 2); + return token; + }; + const auto threeChar = [&](const TokenKind kind) { + cur += 3; + token.kind = kind; + token.spelling = StringRef(start, 3); + return token; + }; + + switch (c) { + case '(': + return single(TokenKind::LParen); + case ')': + return single(TokenKind::RParen); + case '[': + return single(TokenKind::LBracket); + case ']': + return single(TokenKind::RBracket); + case '{': + return single(TokenKind::LBrace); + case '}': + return single(TokenKind::RBrace); + case ',': + return single(TokenKind::Comma); + case ';': + return single(TokenKind::Semicolon); + case ':': + return single(TokenKind::Colon); + case '@': + return single(TokenKind::At); + case '~': + return peek() == '=' ? twoChar(TokenKind::CompoundAssign) + : single(TokenKind::Tilde); + case '=': + return peek() == '=' ? twoChar(TokenKind::EqualsEquals) + : single(TokenKind::Equals); + case '!': + return peek() == '=' ? twoChar(TokenKind::NotEquals) + : single(TokenKind::ExclamationPoint); + case '<': + if (peek() == '<') { + return cur + 2 < end && cur[2] == '=' + ? threeChar(TokenKind::CompoundAssign) + : twoChar(TokenKind::ShiftLeft); + } + return peek() == '=' ? twoChar(TokenKind::LessEquals) + : single(TokenKind::Less); + case '>': + if (peek() == '>') { + return cur + 2 < end && cur[2] == '=' + ? threeChar(TokenKind::CompoundAssign) + : twoChar(TokenKind::ShiftRight); + } + return peek() == '=' ? twoChar(TokenKind::GreaterEquals) + : single(TokenKind::Greater); + case '+': + return peek() == '=' ? twoChar(TokenKind::CompoundAssign) + : single(TokenKind::Plus); + case '-': + if (peek() == '>') { + return twoChar(TokenKind::Arrow); + } + return peek() == '=' ? twoChar(TokenKind::CompoundAssign) + : single(TokenKind::Minus); + case '*': + if (peek() == '*') { + return cur + 2 < end && cur[2] == '=' + ? threeChar(TokenKind::CompoundAssign) + : twoChar(TokenKind::DoubleAsterisk); + } + return peek() == '=' ? twoChar(TokenKind::CompoundAssign) + : single(TokenKind::Asterisk); + case '/': + return peek() == '=' ? twoChar(TokenKind::CompoundAssign) + : single(TokenKind::Slash); + case '&': + if (peek() == '&') { + return twoChar(TokenKind::AmpAmp); + } + if (peek() == '=') { + return twoChar(TokenKind::CompoundAssign); + } + return single(TokenKind::Amp); + case '|': + if (peek() == '|') { + return twoChar(TokenKind::PipePipe); + } + if (peek() == '=') { + return twoChar(TokenKind::CompoundAssign); + } + return single(TokenKind::Pipe); + case '%': + return peek() == '=' ? twoChar(TokenKind::CompoundAssign) + : single(TokenKind::Percent); + case '^': + if (peek() == '=') { + return twoChar(TokenKind::CompoundAssign); + } + return single(TokenKind::Caret); + default: + break; + } + + return single(TokenKind::Error); +} + +} // namespace mlir::oq3::frontend::detail +// NOLINTEND(cppcoreguidelines-pro-bounds-pointer-arithmetic) diff --git a/mlir/lib/Target/OpenQASM/OpenQASMSemantics.cpp b/mlir/lib/Target/OpenQASM/OpenQASMSemantics.cpp new file mode 100644 index 0000000000..ba3b0bd188 --- /dev/null +++ b/mlir/lib/Target/OpenQASM/OpenQASMSemantics.cpp @@ -0,0 +1,2244 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#include "mlir/Target/OpenQASM/Detail/OpenQASMSemantics.h" + +#include "mlir/Target/OpenQASM/GateCatalog.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace mlir::oq3::frontend::detail { +namespace { + +class SemanticError final : public std::runtime_error { +public: + Diagnostic diagnostic; + + explicit SemanticError(Diagnostic value) + : std::runtime_error(value.message), diagnostic(std::move(value)) {} +}; + +struct Constant { + ScalarType type = ScalarType::Int; + std::variant value = + std::int64_t{0}; +}; + +struct GateSignature { + std::size_t parameterCount = 0; + std::size_t qubitCount = 0; + bool variadicControls = false; +}; + +enum class SymbolKind : std::uint8_t { + Scalar, + GateLocalScalar, + Constant, + Register, + GateParameter, + GateQubit, +}; + +struct Symbol { + SymbolKind kind = SymbolKind::Scalar; + ScalarType type = ScalarType::Int; + std::uint32_t id = 0; + std::optional constant; +}; + +[[nodiscard]] ScalarType scalarType(const ScalarKind kind) { + switch (kind) { + case ScalarKind::Bool: + return ScalarType::Bool; + case ScalarKind::Int: + return ScalarType::Int; + case ScalarKind::Uint: + return ScalarType::Uint; + case ScalarKind::Float: + return ScalarType::Float; + } + llvm_unreachable("unknown syntax scalar kind"); +} + +[[nodiscard]] bool isInteger(const ScalarType type) { + return type == ScalarType::Int || type == ScalarType::Uint; +} + +[[nodiscard]] double asDouble(const Constant& constant) { + return std::visit([](const auto value) { return static_cast(value); }, + constant.value); +} + +[[nodiscard]] std::int64_t asSigned(const Constant& constant) { + if (constant.type == ScalarType::Uint) { + const auto value = std::get(constant.value); + if (value > + static_cast(std::numeric_limits::max())) { + throw std::overflow_error("unsigned value does not fit in signed i64"); + } + return static_cast(value); + } + return std::get(constant.value); +} + +[[nodiscard]] int compareNumericConstants(const Constant& lhs, + const Constant& rhs) { + if (lhs.type == ScalarType::Float || rhs.type == ScalarType::Float) { + const auto left = asDouble(lhs); + const auto right = asDouble(rhs); + return left < right ? -1 : left > right ? 1 : 0; + } + if (lhs.type == ScalarType::Uint || rhs.type == ScalarType::Uint) { + const auto asUnsigned = [](const Constant& constant) { + return constant.type == ScalarType::Uint + ? std::get(constant.value) + : static_cast( + std::get(constant.value)); + }; + const auto left = asUnsigned(lhs); + const auto right = asUnsigned(rhs); + return left < right ? -1 : left > right ? 1 : 0; + } + const auto left = std::get(lhs.value); + const auto right = std::get(rhs.value); + return left < right ? -1 : left > right ? 1 : 0; +} + +class SemanticAnalyzer { +public: + SemanticAnalyzer(const SyntaxProgram& syntaxProgram, + const llvm::SourceMgr& sourceManager, + const FrontendOptions& frontendOptions) + : syntax(syntaxProgram), sources(sourceManager), + options(frontendOptions) { + scopes.emplace_back(); + } + + [[nodiscard]] AnalysisResult run() { + try { + analyzeVersion(); + analyzeBody(syntax.body, program.body, /*global=*/true); + validateGateCallGraph(); + finalizeOutputs(); + return {.program = std::make_unique(std::move(program))}; + } catch (const SemanticError& error) { + return {.diagnostics = {error.diagnostic}}; + } catch (const std::exception& error) { + return {.diagnostics = {{.message = error.what()}}}; + } + } + +private: + struct DynamicBitFact { + ExpressionId expression = 0; + std::vector> dependencies; + }; + + const SyntaxProgram& syntax; + const llvm::SourceMgr& sources; + FrontendOptions options; + TypedProgram program; + SmallVector> scopes; + llvm::StringMap customGates; + std::vector> initializedBits; + std::vector> dynamicBitFacts; + std::vector initializedScalars; + std::vector scalarGenerations; + std::vector bitRegisters; + std::vector explicitOutputs; + bool insideGate = false; + bool hasVirtualQubits = false; + bool hasHardwareQubits = false; + std::set hardwareQubits; + + [[noreturn]] void fail(SMLoc location, const Twine& message) const { + throw SemanticError({.location = sourceLocation(sources, location), + .message = message.str()}); + } + + void restoreStatePrefix(const std::vector>& bitsInitialized, + const std::vector& scalarsInitialized, + const std::vector& generations) { + for (std::size_t reg = 0; reg < bitsInitialized.size(); ++reg) { + initializedBits[reg] = bitsInitialized[reg]; + } + for (std::size_t scalar = 0; scalar < scalarsInitialized.size(); ++scalar) { + initializedScalars[scalar] = scalarsInitialized[scalar]; + scalarGenerations[scalar] = generations[scalar]; + } + } + + void restoreDynamicFactsPrefix( + const std::vector>& facts) { + for (std::size_t reg = 0; reg < facts.size(); ++reg) { + dynamicBitFacts[reg] = facts[reg]; + } + for (std::size_t reg = facts.size(); reg < dynamicBitFacts.size(); ++reg) { + dynamicBitFacts[reg].clear(); + } + } + + [[nodiscard]] bool sameExpression(const ExpressionId lhs, + const ExpressionId rhs) const { + const auto& left = program.expressions[lhs]; + const auto& right = program.expressions[rhs]; + if (left.kind != right.kind || left.type != right.type || + left.constant != right.constant || left.parameter != right.parameter || + left.variable != right.variable) { + return false; + } + switch (left.kind) { + case ExpressionKind::Constant: + case ExpressionKind::GateParameter: + case ExpressionKind::Variable: + return true; + case ExpressionKind::Negate: + case ExpressionKind::ArcCos: + case ExpressionKind::ArcSin: + case ExpressionKind::ArcTan: + case ExpressionKind::Sin: + case ExpressionKind::Cos: + case ExpressionKind::Tan: + case ExpressionKind::Exp: + case ExpressionKind::Ln: + case ExpressionKind::Sqrt: + return sameExpression(left.lhs, right.lhs); + default: + return sameExpression(left.lhs, right.lhs) && + sameExpression(left.rhs, right.rhs); + } + } + + void collectDependencies( + const ExpressionId expression, + std::vector>& dependencies) const { + const auto& value = program.expressions[expression]; + if (value.kind == ExpressionKind::Variable) { + dependencies.emplace_back(value.variable, + scalarGenerations[value.variable]); + return; + } + if (value.kind == ExpressionKind::Constant || + value.kind == ExpressionKind::GateParameter) { + return; + } + collectDependencies(value.lhs, dependencies); + if (value.kind != ExpressionKind::Negate && + value.kind != ExpressionKind::ArcCos && + value.kind != ExpressionKind::ArcSin && + value.kind != ExpressionKind::ArcTan && + value.kind != ExpressionKind::Sin && + value.kind != ExpressionKind::Cos && + value.kind != ExpressionKind::Tan && + value.kind != ExpressionKind::Exp && value.kind != ExpressionKind::Ln && + value.kind != ExpressionKind::Sqrt) { + collectDependencies(value.rhs, dependencies); + } + } + + [[nodiscard]] std::optional + constantCondition(const SyntaxExpressionId expression) const { + if (!isConstantExpression(expression)) { + return std::nullopt; + } + const auto value = evaluateConstant(expression); + if (value.type != ScalarType::Bool) { + return std::nullopt; + } + return std::get(value.value); + } + + [[nodiscard]] const Symbol* lookup(StringRef name) const { + for (const auto& scope : llvm::reverse(scopes)) { + if (const auto found = scope.find(name); found != scope.end()) { + return &found->second; + } + } + return nullptr; + } + + void declare(SMLoc location, StringRef name, Symbol symbol) { + const auto* catalog = lookupGate(name); + const bool catalogNameReserved = + catalog != nullptr && + (catalog->availability == GateAvailability::Language || + (catalog->availability == GateAvailability::StandardLibrary && + program.standardLibraryIncluded)); + if (builtinConstant(name) || + (scopes.size() == 1 && + (customGates.contains(name) || catalogNameReserved))) { + fail(location, "identifier '" + name + "' is already declared"); + } + if (!scopes.back().insert({name, symbol}).second) { + fail(location, "identifier '" + name + "' is already declared"); + } + } + + [[nodiscard]] bool isGateAvailable(const GateCatalogEntry& gate) const { + if (gate.availability == GateAvailability::Language) { + return true; + } + if (options.gatePolicy == GatePolicy::MQTCompatibility) { + return true; + } + return gate.availability == GateAvailability::StandardLibrary && + program.standardLibraryIncluded; + } + + void analyzeVersion() { + if (!syntax.version) { + return; + } + const auto version = *syntax.version; + if (version.major == 2 && version.minor == 0) { + program.openQASM2 = true; + return; + } + if (version.major == 3 && (version.minor == 0 || version.minor == 1)) { + return; + } + fail(syntax.versionLocation, "Unsupported OpenQASM version " + + std::to_string(version.major) + "." + + std::to_string(version.minor)); + } + + void validateGateCallGraph() const { + llvm::StringMap gateIndices; + for (const auto [index, gate] : llvm::enumerate(program.gates)) { + gateIndices[gate.name] = index; + } + enum class VisitState : std::uint8_t { Unvisited, Active, Complete }; + std::vector states(program.gates.size(), VisitState::Unvisited); + const auto visitApplications = [&](auto&& self, + ArrayRef statements, + const auto& callback) -> void { + for (const auto statementId : statements) { + const auto& statement = program.statements[statementId]; + std::visit( + [&](const auto& data) { + using T = std::decay_t; + if constexpr (std::is_same_v) { + callback(data, statement.location); + } else if constexpr (std::is_same_v) { + self(self, data.thenStatements, callback); + self(self, data.elseStatements, callback); + } else if constexpr (std::is_same_v || + std::is_same_v) { + self(self, data.body, callback); + } + }, + statement.data); + } + }; + const auto visit = [&](auto&& self, const std::size_t index) -> void { + if (states[index] == VisitState::Complete) { + return; + } + states[index] = VisitState::Active; + visitApplications( + visitApplications, program.gates[index].body, + [&](const GateApplication& application, + const SourceLocation& location) { + const auto callee = gateIndices.find(application.callee); + if (callee == gateIndices.end()) { + return; + } + if (states[callee->second] == VisitState::Active) { + throw SemanticError( + {.location = location, + .message = "recursive custom gate definition involving '" + + application.callee + "'"}); + } + self(self, callee->second); + }); + states[index] = VisitState::Complete; + }; + for (std::size_t index = 0; index < program.gates.size(); ++index) { + if (states[index] == VisitState::Unvisited) { + visit(visit, index); + } + } + } + + [[nodiscard]] StatementId addStatement(SMLoc location, StatementData data) { + const auto id = static_cast(program.statements.size()); + program.statements.push_back( + {.data = std::move(data), + .location = sourceLocation(sources, location)}); + return id; + } + + [[nodiscard]] ExpressionId addExpression(ScalarExpression expression) { + const auto id = static_cast(program.expressions.size()); + program.expressions.push_back(expression); + return id; + } + + [[nodiscard]] ConditionId addCondition(ConditionExpression condition) { + const auto id = static_cast(program.conditions.size()); + program.conditions.push_back(std::move(condition)); + return id; + } + + [[nodiscard]] ExpressionId addConstant(const Constant& constant) { + return addExpression({.kind = ExpressionKind::Constant, + .type = constant.type, + .constant = constant.value}); + } + + [[nodiscard]] Constant coerceConstant(Constant constant, + const ScalarType type, + const SMLoc location) const { + if (type == ScalarType::Bool) { + if (constant.type == ScalarType::Bool) { + return constant; + } + return {.type = ScalarType::Bool, .value = asDouble(constant) != 0.0}; + } + if (type == ScalarType::Float) { + return {.type = ScalarType::Float, .value = asDouble(constant)}; + } + if (constant.type == ScalarType::Bool) { + constant = {.type = ScalarType::Int, + .value = std::int64_t{std::get(constant.value)}}; + } else if (constant.type == ScalarType::Float) { + const auto value = std::get(constant.value); + const auto withinTargetRange = + type == ScalarType::Int + ? value >= static_cast( + std::numeric_limits::min()) && + value < std::ldexp(1.0, 63) + : value > -1.0 && value < std::ldexp(1.0, 64); + if (!std::isfinite(value) || !withinTargetRange) { + fail(location, "floating-point value is outside the integer range"); + } + if (type == ScalarType::Int) { + constant = {.type = ScalarType::Int, + .value = static_cast(value)}; + } else { + constant = {.type = ScalarType::Uint, + .value = static_cast(value)}; + } + } + if (type == ScalarType::Int) { + if (constant.type == ScalarType::Uint) { + return {.type = ScalarType::Int, + .value = std::bit_cast( + std::get(constant.value))}; + } + return constant; + } + if (constant.type == ScalarType::Int) { + return {.type = ScalarType::Uint, + .value = std::bit_cast( + std::get(constant.value))}; + } + return constant; + } + + [[nodiscard]] bool expressionProducesBool(const SyntaxExpressionId id) const { + const auto& expression = syntax.expressions[id]; + switch (expression.kind) { + case Expr::Kind::Bool: + case Expr::Kind::Measurement: + case Expr::Kind::Not: + case Expr::Kind::And: + case Expr::Kind::Or: + case Expr::Kind::Equal: + case Expr::Kind::NotEqual: + case Expr::Kind::Less: + case Expr::Kind::LessEqual: + case Expr::Kind::Greater: + case Expr::Kind::GreaterEqual: + return true; + case Expr::Kind::Identifier: { + const auto* symbol = lookup(expression.identifier); + return symbol != nullptr && + ((symbol->kind == SymbolKind::Scalar && + symbol->type == ScalarType::Bool) || + (symbol->kind == SymbolKind::Constant && symbol->constant && + symbol->constant->type == ScalarType::Bool) || + (symbol->kind == SymbolKind::Register && + program.registers[symbol->id].kind == RegisterKind::Bit)); + } + case Expr::Kind::Index: + return true; + default: + return false; + } + } + + [[nodiscard]] ConditionId + analyzeBoolValue(const SyntaxExpressionId syntaxId) { + if (expressionProducesBool(syntaxId)) { + return analyzeCondition(syntaxId); + } + if (isConstantExpression(syntaxId)) { + const auto constant = evaluateConstant(syntaxId); + return addCondition({.kind = ConditionKind::Literal, + .location = sourceLocation( + sources, syntax.expressions[syntaxId].location), + .literal = asDouble(constant) != 0.0}); + } + const auto value = analyzeExpression(syntaxId); + const auto type = program.expressions[value].type; + const auto zero = addConstant( + type == ScalarType::Float + ? Constant{.type = ScalarType::Float, .value = 0.0} + : type == ScalarType::Uint + ? Constant{.type = ScalarType::Uint, .value = std::uint64_t{0}} + : Constant{.type = ScalarType::Int, .value = std::int64_t{0}}); + return addCondition({.kind = ConditionKind::Comparison, + .location = sourceLocation( + sources, syntax.expressions[syntaxId].location), + .comparisonLhs = value, + .comparisonRhs = zero, + .comparison = ComparisonKind::NotEqual}); + } + + [[nodiscard]] std::optional + builtinConstant(StringRef identifier) const { + if (identifier == "pi" || identifier == "π") { + return Constant{.type = ScalarType::Float, .value = std::numbers::pi}; + } + if (identifier == "tau" || identifier == "τ") { + return Constant{.type = ScalarType::Float, + .value = 2.0 * std::numbers::pi}; + } + if (identifier == "euler" || identifier == "ℇ") { + return Constant{.type = ScalarType::Float, .value = std::numbers::e}; + } + return std::nullopt; + } + + [[nodiscard]] Constant evaluateConstant(const SyntaxExpressionId id) const { + const auto& expression = syntax.expressions[id]; + switch (expression.kind) { + case Expr::Kind::Int: + if (expression.integer <= static_cast( + std::numeric_limits::max())) { + return {.type = ScalarType::Int, + .value = static_cast(expression.integer)}; + } + return {.type = ScalarType::Uint, .value = expression.integer}; + case Expr::Kind::Float: + return {.type = ScalarType::Float, .value = expression.floatingPoint}; + case Expr::Kind::Bool: + return {.type = ScalarType::Bool, .value = expression.boolean}; + case Expr::Kind::Identifier: { + if (const auto builtin = builtinConstant(expression.identifier)) { + return *builtin; + } + const auto* symbol = lookup(expression.identifier); + if (symbol == nullptr || symbol->kind != SymbolKind::Constant || + !symbol->constant) { + fail(expression.location, "expression is not a compile-time constant"); + } + return *symbol->constant; + } + case Expr::Kind::Neg: { + auto operand = evaluateConstant(*expression.lhs); + if (operand.type == ScalarType::Bool) { + fail(expression.location, + "numeric negation requires a numeric operand"); + } + if (operand.type == ScalarType::Float) { + return {.type = ScalarType::Float, .value = -asDouble(operand)}; + } + if (operand.type == ScalarType::Uint) { + const auto value = std::get(operand.value); + if (syntax.expressions[*expression.lhs].kind == Expr::Kind::Int) { + if (value > (1ULL << 63)) { + fail(expression.location, "integer negation overflows i64"); + } + return {.type = ScalarType::Int, + .value = std::numeric_limits::min()}; + } + return {.type = ScalarType::Uint, .value = 0ULL - value}; + } + const auto value = std::get(operand.value); + if (value == std::numeric_limits::min()) { + fail(expression.location, "integer negation overflows i64"); + } + return {.type = ScalarType::Int, .value = -value}; + } + case Expr::Kind::Not: { + const auto operand = evaluateConstant(*expression.lhs); + if (operand.type != ScalarType::Bool) { + fail(expression.location, "logical negation requires a bool operand"); + } + return {.type = ScalarType::Bool, + .value = !std::get(operand.value)}; + } + case Expr::Kind::BitNot: { + fail(expression.location, + "bitwise operators require explicitly sized uint, bit, or angle " + "operands, which are not supported yet"); + } + case Expr::Kind::And: + case Expr::Kind::Or: { + const auto lhs = evaluateConstant(*expression.lhs); + const auto rhs = evaluateConstant(*expression.rhs); + if (lhs.type != ScalarType::Bool || rhs.type != ScalarType::Bool) { + fail(expression.location, "logical operators require bool operands"); + } + const auto left = std::get(lhs.value); + const auto right = std::get(rhs.value); + return {.type = ScalarType::Bool, + .value = expression.kind == Expr::Kind::And ? left && right + : left || right}; + } + case Expr::Kind::Equal: + case Expr::Kind::NotEqual: + case Expr::Kind::Less: + case Expr::Kind::LessEqual: + case Expr::Kind::Greater: + case Expr::Kind::GreaterEqual: { + const auto lhs = evaluateConstant(*expression.lhs); + const auto rhs = evaluateConstant(*expression.rhs); + bool result = false; + if (lhs.type == ScalarType::Bool || rhs.type == ScalarType::Bool) { + if (lhs.type != ScalarType::Bool || rhs.type != ScalarType::Bool || + (expression.kind != Expr::Kind::Equal && + expression.kind != Expr::Kind::NotEqual)) { + fail(expression.location, + "bool values only support equality comparisons with bool " + "values"); + } + const auto equal = + std::get(lhs.value) == std::get(rhs.value); + result = expression.kind == Expr::Kind::Equal ? equal : !equal; + } else { + const auto ordering = compareNumericConstants(lhs, rhs); + switch (expression.kind) { + case Expr::Kind::Equal: + result = ordering == 0; + break; + case Expr::Kind::NotEqual: + result = ordering != 0; + break; + case Expr::Kind::Less: + result = ordering < 0; + break; + case Expr::Kind::LessEqual: + result = ordering <= 0; + break; + case Expr::Kind::Greater: + result = ordering > 0; + break; + case Expr::Kind::GreaterEqual: + result = ordering >= 0; + break; + default: + llvm_unreachable("not a comparison expression"); + } + } + return {.type = ScalarType::Bool, .value = result}; + } + case Expr::Kind::ArcCos: + case Expr::Kind::ArcSin: + case Expr::Kind::ArcTan: + case Expr::Kind::Cos: + case Expr::Kind::Exp: + case Expr::Kind::Log: + case Expr::Kind::Sin: + case Expr::Kind::Sqrt: + case Expr::Kind::Tan: { + const auto value = asDouble(evaluateConstant(*expression.lhs)); + double result = 0.0; + switch (expression.kind) { + case Expr::Kind::ArcCos: + result = std::acos(value); + break; + case Expr::Kind::ArcSin: + result = std::asin(value); + break; + case Expr::Kind::ArcTan: + result = std::atan(value); + break; + case Expr::Kind::Cos: + result = std::cos(value); + break; + case Expr::Kind::Exp: + result = std::exp(value); + break; + case Expr::Kind::Log: + result = std::log(value); + break; + case Expr::Kind::Sin: + result = std::sin(value); + break; + case Expr::Kind::Sqrt: + result = std::sqrt(value); + break; + case Expr::Kind::Tan: + result = std::tan(value); + break; + default: + llvm_unreachable("not a unary math expression"); + } + if (!std::isfinite(result)) { + fail(expression.location, + "constant math expression has a non-finite result"); + } + return {.type = ScalarType::Float, .value = result}; + } + case Expr::Kind::Add: + case Expr::Kind::Sub: + case Expr::Kind::Mul: + case Expr::Kind::Div: + case Expr::Kind::Mod: + case Expr::Kind::BuiltinMod: + case Expr::Kind::BuiltinPow: + case Expr::Kind::Pow: + return evaluateConstantBinary(expression); + case Expr::Kind::BitAnd: + case Expr::Kind::BitOr: + case Expr::Kind::BitXor: + case Expr::Kind::ShiftLeft: + case Expr::Kind::ShiftRight: + fail(expression.location, + "bitwise operators require explicitly sized uint, bit, or angle " + "operands, which are not supported yet"); + case Expr::Kind::Index: + case Expr::Kind::Measurement: + fail(expression.location, "expression is not a compile-time constant"); + } + llvm_unreachable("unknown syntax expression kind"); + } + + [[nodiscard]] Constant + evaluateConstantBinary(const SyntaxExpression& expression) const { + const auto lhs = evaluateConstant(*expression.lhs); + const auto rhs = evaluateConstant(*expression.rhs); + if (lhs.type == ScalarType::Bool || rhs.type == ScalarType::Bool) { + fail(expression.location, + "arithmetic operators require numeric operands"); + } + const bool builtinFloatPower = expression.kind == Expr::Kind::BuiltinPow && + rhs.type == ScalarType::Int && + std::get(rhs.value) < 0; + if (lhs.type == ScalarType::Float || rhs.type == ScalarType::Float || + builtinFloatPower) { + if (expression.kind == Expr::Kind::Mod) { + fail(expression.location, + "the '%' operator requires integer operands; use mod() for " + "floating-point remainder"); + } + const auto left = asDouble(lhs); + const auto right = asDouble(rhs); + double result = 0.0; + switch (expression.kind) { + case Expr::Kind::Add: + result = left + right; + break; + case Expr::Kind::Sub: + result = left - right; + break; + case Expr::Kind::Mul: + result = left * right; + break; + case Expr::Kind::Div: + if (right == 0.0) { + fail(expression.location, "division by zero"); + } + result = left / right; + break; + case Expr::Kind::BuiltinMod: + if (right == 0.0) { + fail(expression.location, "modulo by zero"); + } + result = std::fmod(left, right); + break; + case Expr::Kind::Pow: + case Expr::Kind::BuiltinPow: + result = std::pow(left, right); + break; + default: + llvm_unreachable("not a binary expression"); + } + if (!std::isfinite(result)) { + fail(expression.location, + "constant arithmetic has a non-finite result"); + } + return {.type = ScalarType::Float, .value = result}; + } + + if (lhs.type == ScalarType::Uint || rhs.type == ScalarType::Uint) { + const auto asUnsigned = [](const Constant& constant) { + return constant.type == ScalarType::Uint + ? std::get(constant.value) + : static_cast( + std::get(constant.value)); + }; + const auto left = asUnsigned(lhs); + const auto right = asUnsigned(rhs); + std::uint64_t result = 0; + switch (expression.kind) { + case Expr::Kind::Add: + result = left + right; + break; + case Expr::Kind::Sub: + result = left - right; + break; + case Expr::Kind::Mul: + result = left * right; + break; + case Expr::Kind::Div: + if (right == 0) { + fail(expression.location, "division by zero"); + } + result = left / right; + break; + case Expr::Kind::Mod: + case Expr::Kind::BuiltinMod: + if (right == 0) { + fail(expression.location, "modulo by zero"); + } + result = left % right; + break; + case Expr::Kind::Pow: + case Expr::Kind::BuiltinPow: + result = 1; + for (auto base = left, exponent = right; exponent != 0; + exponent >>= 1U, base *= base) { + if ((exponent & 1U) != 0) { + result *= base; + } + } + break; + default: + llvm_unreachable("not a binary expression"); + } + return {.type = ScalarType::Uint, .value = result}; + } + + const auto left = std::get(lhs.value); + const auto right = std::get(rhs.value); + std::int64_t result = 0; + bool overflow = false; + switch (expression.kind) { + case Expr::Kind::Add: + overflow = llvm::AddOverflow(left, right, result); + break; + case Expr::Kind::Sub: + overflow = llvm::SubOverflow(left, right, result); + break; + case Expr::Kind::Mul: + overflow = llvm::MulOverflow(left, right, result); + break; + case Expr::Kind::Div: + if (right == 0) { + fail(expression.location, "division by zero"); + } + if (left == std::numeric_limits::min() && right == -1) { + overflow = true; + } else { + result = left / right; + } + break; + case Expr::Kind::Mod: + case Expr::Kind::BuiltinMod: + if (right == 0) { + fail(expression.location, "modulo by zero"); + } + if (left == std::numeric_limits::min() && right == -1) { + overflow = true; + } else { + result = left % right; + } + break; + case Expr::Kind::Pow: + case Expr::Kind::BuiltinPow: { + if (right < 0) { + assert(expression.kind == Expr::Kind::Pow && + "negative built-in powers use the floating overload"); + fail(expression.location, + "integer power requires a nonnegative exponent"); + } + result = 1; + auto base = left; + auto exponent = static_cast(right); + while (exponent != 0 && !overflow) { + if ((exponent & 1U) != 0) { + overflow = llvm::MulOverflow(result, base, result); + } + exponent >>= 1U; + if (exponent != 0 && !overflow) { + overflow = llvm::MulOverflow(base, base, base); + } + } + break; + } + default: + llvm_unreachable("not a binary expression"); + } + if (overflow) { + fail(expression.location, "constant integer arithmetic overflows i64"); + } + return {.type = ScalarType::Int, .value = result}; + } + + [[nodiscard]] bool isConstantExpression(const SyntaxExpressionId id) const { + const auto& expression = syntax.expressions[id]; + switch (expression.kind) { + case Expr::Kind::Identifier: { + if (builtinConstant(expression.identifier)) { + return true; + } + const auto* symbol = lookup(expression.identifier); + return symbol != nullptr && symbol->kind == SymbolKind::Constant; + } + case Expr::Kind::Int: + case Expr::Kind::Float: + case Expr::Kind::Bool: + return true; + case Expr::Kind::Index: + case Expr::Kind::Measurement: + return false; + default: + return (!expression.lhs || isConstantExpression(*expression.lhs)) && + (!expression.rhs || isConstantExpression(*expression.rhs)); + } + } + + void validateGateExpression(const SyntaxExpressionId id) const { + const auto& expression = syntax.expressions[id]; + if (expression.kind == Expr::Kind::Identifier && + !builtinConstant(expression.identifier)) { + const auto* symbol = lookup(expression.identifier); + if (symbol == nullptr || (symbol->kind != SymbolKind::GateParameter && + symbol->kind != SymbolKind::GateLocalScalar && + symbol->kind != SymbolKind::Constant)) { + fail(expression.location, + "gate definitions cannot capture outer scalar '" + + expression.identifier + "'"); + } + } + if (expression.lhs) { + validateGateExpression(*expression.lhs); + } + if (expression.rhs) { + validateGateExpression(*expression.rhs); + } + } + + [[nodiscard]] ExpressionId + analyzeExpression(const SyntaxExpressionId syntaxId) { + const auto& expression = syntax.expressions[syntaxId]; + if (insideGate) { + validateGateExpression(syntaxId); + } + if (isConstantExpression(syntaxId)) { + return addConstant(evaluateConstant(syntaxId)); + } + if (expression.kind == Expr::Kind::Identifier) { + const auto* symbol = lookup(expression.identifier); + if (symbol == nullptr) { + fail(expression.location, + "unknown scalar identifier '" + expression.identifier + "'"); + } + if (symbol->kind == SymbolKind::GateParameter) { + return addExpression({.kind = ExpressionKind::GateParameter, + .type = ScalarType::Float, + .parameter = symbol->id}); + } + if (symbol->kind != SymbolKind::Scalar && + symbol->kind != SymbolKind::GateLocalScalar) { + fail(expression.location, "identifier '" + expression.identifier + + "' is not a scalar value"); + } + if (!initializedScalars.at(symbol->id)) { + fail(expression.location, + "scalar '" + expression.identifier + "' is uninitialized"); + } + return addExpression({.kind = ExpressionKind::Variable, + .type = symbol->type, + .variable = symbol->id}); + } + + ExpressionKind kind; + switch (expression.kind) { + case Expr::Kind::Neg: + kind = ExpressionKind::Negate; + break; + case Expr::Kind::BitNot: + fail(expression.location, + "bitwise operators require explicitly sized uint, bit, or angle " + "operands, which are not supported yet"); + case Expr::Kind::Add: + kind = ExpressionKind::Add; + break; + case Expr::Kind::Sub: + kind = ExpressionKind::Subtract; + break; + case Expr::Kind::Mul: + kind = ExpressionKind::Multiply; + break; + case Expr::Kind::Div: + kind = ExpressionKind::Divide; + break; + case Expr::Kind::Mod: + case Expr::Kind::BuiltinMod: + kind = ExpressionKind::Modulo; + break; + case Expr::Kind::Pow: + case Expr::Kind::BuiltinPow: + kind = ExpressionKind::Power; + break; + case Expr::Kind::BitAnd: + case Expr::Kind::BitOr: + case Expr::Kind::BitXor: + case Expr::Kind::ShiftLeft: + case Expr::Kind::ShiftRight: + fail(expression.location, + "bitwise operators require explicitly sized uint, bit, or angle " + "operands, which are not supported yet"); + case Expr::Kind::ArcCos: + kind = ExpressionKind::ArcCos; + break; + case Expr::Kind::ArcSin: + kind = ExpressionKind::ArcSin; + break; + case Expr::Kind::ArcTan: + kind = ExpressionKind::ArcTan; + break; + case Expr::Kind::Cos: + kind = ExpressionKind::Cos; + break; + case Expr::Kind::Exp: + kind = ExpressionKind::Exp; + break; + case Expr::Kind::Log: + kind = ExpressionKind::Ln; + break; + case Expr::Kind::Sin: + kind = ExpressionKind::Sin; + break; + case Expr::Kind::Sqrt: + kind = ExpressionKind::Sqrt; + break; + case Expr::Kind::Tan: + kind = ExpressionKind::Tan; + break; + case Expr::Kind::Not: + case Expr::Kind::Equal: + case Expr::Kind::NotEqual: + case Expr::Kind::Less: + case Expr::Kind::LessEqual: + case Expr::Kind::Greater: + case Expr::Kind::GreaterEqual: + case Expr::Kind::And: + case Expr::Kind::Or: + case Expr::Kind::Index: + case Expr::Kind::Measurement: + fail(expression.location, "expected a scalar arithmetic expression"); + case Expr::Kind::Int: + case Expr::Kind::Float: + case Expr::Kind::Bool: + case Expr::Kind::Identifier: + llvm_unreachable("handled expression kind"); + } + const auto lhs = analyzeExpression(*expression.lhs); + const auto rhs = + expression.rhs + ? std::optional(analyzeExpression(*expression.rhs)) + : std::nullopt; + if (program.expressions[lhs].type == ScalarType::Bool || + (rhs && program.expressions[*rhs].type == ScalarType::Bool)) { + fail(expression.location, + "arithmetic operators require numeric operands"); + } + if (expression.kind == Expr::Kind::Mod && + (program.expressions[lhs].type == ScalarType::Float || + (rhs && program.expressions[*rhs].type == ScalarType::Float))) { + fail(expression.location, + "the '%' operator requires integer operands; use mod() for " + "floating-point remainder"); + } + auto type = program.expressions[lhs].type; + if (kind == ExpressionKind::ArcCos || kind == ExpressionKind::ArcSin || + kind == ExpressionKind::ArcTan || kind == ExpressionKind::Cos || + kind == ExpressionKind::Exp || kind == ExpressionKind::Ln || + kind == ExpressionKind::Sin || kind == ExpressionKind::Sqrt || + kind == ExpressionKind::Tan || type == ScalarType::Float || + (expression.kind == Expr::Kind::BuiltinPow && rhs && + program.expressions[*rhs].type == ScalarType::Int) || + (rhs && program.expressions[*rhs].type == ScalarType::Float)) { + type = ScalarType::Float; + } else if (rhs && (type == ScalarType::Uint || + program.expressions[*rhs].type == ScalarType::Uint)) { + type = ScalarType::Uint; + } + return addExpression( + {.kind = kind, .type = type, .lhs = lhs, .rhs = rhs.value_or(0)}); + } + + [[nodiscard]] std::uint64_t + constantWidth(const std::optional size, + SMLoc location) const { + if (!size) { + return 1; + } + if (!isConstantExpression(*size)) { + fail(location, "register width must be a constant integer expression"); + } + const auto constant = evaluateConstant(*size); + if (!isInteger(constant.type)) { + fail(location, "register width must be an integer expression"); + } + const auto value = asSigned(constant); + if (value <= 0) { + fail(location, "register width must be greater than zero"); + } + return static_cast(value); + } + + [[nodiscard]] std::optional + constantIndex(const SyntaxExpressionId id, const std::uint64_t width, + SMLoc location) const { + if (!isConstantExpression(id)) { + return std::nullopt; + } + const auto constant = evaluateConstant(id); + if (!isInteger(constant.type)) { + fail(location, "index must be an integer expression"); + } + auto value = asSigned(constant); + if (value < 0) { + value += static_cast(width); + } + if (value < 0) { + fail(location, "index is out of bounds"); + } + return static_cast(value); + } + + void analyzeBody(ArrayRef source, + std::vector& destination, const bool global) { + for (const auto id : source) { + analyzeStatement(syntax.statements[id], destination, global); + } + } + + void analyzeStatement(const SyntaxStatement& statement, + std::vector& destination, + const bool global) { + std::visit( + [&](const auto& data) { + using T = std::decay_t; + if constexpr (!std::is_same_v && + !std::is_same_v && + !std::is_same_v) { + if (insideGate) { + fail(statement.location, + "gate bodies may contain only gate calls and loops over " + "gate calls"); + } + } + if constexpr (std::is_same_v) { + activateStandardLibrary(statement.location); + } else if constexpr (std::is_same_v) { + analyzeScalarDeclaration(statement.location, data, destination); + } else if constexpr (std::is_same_v) { + analyzeAssignment(statement.location, data, destination); + } else if constexpr (std::is_same_v || + std::is_same_v) { + analyzeRegisterDeclaration(statement.location, data, destination, + global); + } else if constexpr (std::is_same_v) { + destination.push_back(analyzeMeasurement(statement.location, data)); + } else if constexpr (std::is_same_v) { + destination.push_back(analyzeReset(statement.location, data)); + } else if constexpr (std::is_same_v) { + destination.push_back(analyzeBarrier(statement.location, data)); + } else if constexpr (std::is_same_v) { + auto applications = analyzeGateApplication(data); + for (auto& application : applications) { + destination.push_back( + addStatement(statement.location, std::move(application))); + } + } else if constexpr (std::is_same_v) { + if (!global) { + fail(statement.location, + "gate definitions are only allowed at global scope"); + } + analyzeGateDefinition(statement.location, data); + } else if constexpr (std::is_same_v) { + destination.push_back(analyzeIf(statement.location, data)); + } else if constexpr (std::is_same_v) { + destination.push_back(analyzeFor(statement.location, data)); + } else if constexpr (std::is_same_v) { + destination.push_back(analyzeWhile(statement.location, data)); + } + }, + statement.data); + } + + void activateStandardLibrary(SMLoc location) { + if (program.standardLibraryIncluded) { + fail(location, "standard library is included more than once"); + } + for (const auto& gate : getGateCatalog()) { + if (gate.availability != GateAvailability::StandardLibrary) { + continue; + } + if (customGates.contains(gate.name) || lookup(gate.name) != nullptr) { + fail(location, + "standard-library gate '" + gate.name + "' is already declared"); + } + } + program.standardLibraryIncluded = true; + } + + void analyzeScalarDeclaration(SMLoc location, + const SyntaxScalarDeclaration& declaration, + std::vector& destination) { + const auto type = scalarType(declaration.kind); + if (declaration.isConst) { + if (!declaration.initializer || + !isConstantExpression(*declaration.initializer)) { + fail(location, "const declaration requires a constant initializer"); + } + auto constant = coerceConstant(evaluateConstant(*declaration.initializer), + type, location); + declare( + location, declaration.identifier, + {.kind = SymbolKind::Constant, .type = type, .constant = constant}); + return; + } + + const auto id = static_cast(program.scalars.size()); + program.scalars.push_back( + {.type = type, .name = declaration.identifier.str()}); + initializedScalars.push_back(false); + scalarGenerations.push_back(0); + declare(location, declaration.identifier, + {.kind = SymbolKind::Scalar, .type = type, .id = id}); + ScalarDeclarationStatement typed{.scalar = id}; + if (declaration.initializer) { + if (type == ScalarType::Bool) { + typed.conditionInitializer = analyzeBoolValue(*declaration.initializer); + } else { + typed.initializer = analyzeExpression(*declaration.initializer); + } + initializedScalars[id] = true; + } + destination.push_back(addStatement(location, typed)); + } + + void markBitInitialized(const frontend::BitReference& target) { + if (!target.dynamicIndex) { + initializedBits[target.reg][target.index] = true; + return; + } + DynamicBitFact fact{.expression = *target.dynamicIndex}; + collectDependencies(*target.dynamicIndex, fact.dependencies); + auto& facts = dynamicBitFacts[target.reg]; + if (llvm::none_of(facts, [&](const auto& existing) { + return existing.dependencies == fact.dependencies && + sameExpression(existing.expression, fact.expression); + })) { + facts.push_back(std::move(fact)); + } + } + + void analyzeAssignment(SMLoc location, const SyntaxAssignment& assignment, + std::vector& destination) { + const auto* symbol = lookup(assignment.target.identifier); + if (symbol != nullptr && symbol->kind == SymbolKind::Scalar) { + if (assignment.target.index) { + fail(location, "scalar assignments cannot have an index"); + } + ScalarAssignmentStatement typed{.scalar = symbol->id}; + if (symbol->type == ScalarType::Bool) { + typed.condition = analyzeBoolValue(assignment.value); + } else { + typed.value = analyzeExpression(assignment.value); + } + initializedScalars[symbol->id] = true; + ++scalarGenerations[symbol->id]; + destination.push_back(addStatement(location, typed)); + return; + } + if (symbol == nullptr || symbol->kind != SymbolKind::Register || + program.registers[symbol->id].kind != RegisterKind::Bit) { + fail(location, "cannot assign to '" + assignment.target.identifier + "'"); + } + auto targets = resolveBits(assignment.target); + if (targets.size() > 1) { + const auto& value = syntax.expressions[assignment.value]; + if (value.kind != Expr::Kind::Identifier) { + fail(location, + "whole-register bit assignment requires a bit-register value"); + } + auto sourceBits = resolveBits( + {.location = value.location, .identifier = value.identifier}); + if (sourceBits.size() != targets.size()) { + fail(location, "bit-register assignment widths must match"); + } + for (const auto& source : sourceBits) { + ensureBitInitialized(source, value.location); + } + for (const auto [target, source] : llvm::zip_equal(targets, sourceBits)) { + const auto condition = + addCondition({.kind = ConditionKind::Bit, + .location = sourceLocation(sources, value.location), + .bit = source}); + markBitInitialized(target); + destination.push_back( + addStatement(location, BitAssignmentStatement{.target = target, + .value = condition})); + } + return; + } + const auto value = analyzeBoolValue(assignment.value); + markBitInitialized(targets.front()); + destination.push_back( + addStatement(location, BitAssignmentStatement{.target = targets.front(), + .value = value})); + } + + template + void analyzeRegisterDeclaration(SMLoc location, + const Declaration& declaration, + std::vector& destination, + const bool global) { + constexpr bool isQubit = + std::is_same_v; + if constexpr (isQubit) { + if (!global) { + fail(location, "qubits must be declared at global scope"); + } + if (hasHardwareQubits) { + fail(location, + "mixing physical and declared qubits is not supported by the QC " + "target"); + } + hasVirtualQubits = true; + } else if (declaration.output && !global) { + fail(location, "outputs must be declared at global scope"); + } + const auto width = constantWidth(declaration.size, location); + const auto id = static_cast(program.registers.size()); + const bool output = [&] { + if constexpr (isQubit) { + return false; + } else { + return declaration.output; + } + }(); + program.registers.push_back( + {.kind = isQubit ? RegisterKind::Qubit : RegisterKind::Bit, + .name = declaration.identifier.str(), + .width = width, + .location = sourceLocation(sources, location)}); + initializedBits.emplace_back(width, false); + dynamicBitFacts.emplace_back(); + declare(location, declaration.identifier, + {.kind = SymbolKind::Register, .id = id}); + if (!isQubit && global) { + bitRegisters.push_back(id); + if (output || program.openQASM2) { + explicitOutputs.push_back(id); + } + } + destination.push_back( + addStatement(location, DeclarationStatement{.reg = id})); + if constexpr (!isQubit) { + if (declaration.initializer) { + if (width != 1) { + fail(location, + "bit expression initializers require a scalar bit declaration"); + } + analyzeAssignment( + location, + SyntaxAssignment{ + .target = + SyntaxBitReference{.location = location, + .identifier = declaration.identifier}, + .value = *declaration.initializer}, + destination); + } + } + } + + void analyzeGateDefinition(SMLoc location, + const SyntaxGateDefinition& declaration) { + if (customGates.contains(declaration.identifier) || + lookup(declaration.identifier) != nullptr) { + fail(location, + "gate '" + declaration.identifier + "' is already declared"); + } + if (const auto* catalog = lookupGate(declaration.identifier); + catalog != nullptr && isGateAvailable(*catalog)) { + fail(location, + "gate '" + declaration.identifier + "' is already declared"); + } + customGates[declaration.identifier] = { + .parameterCount = declaration.parameters.size(), + .qubitCount = declaration.qubits.size()}; + GateDefinition definition{.name = declaration.identifier.str(), + .parameterCount = declaration.parameters.size(), + .qubitCount = declaration.qubits.size(), + .location = sourceLocation(sources, location)}; + scopes.emplace_back(); + for (const auto [index, parameter] : + llvm::enumerate(declaration.parameters)) { + declare(location, parameter, + {.kind = SymbolKind::GateParameter, + .type = ScalarType::Float, + .id = static_cast(index)}); + } + for (const auto [index, qubit] : llvm::enumerate(declaration.qubits)) { + declare(location, qubit, + {.kind = SymbolKind::GateQubit, + .id = static_cast(index)}); + } + insideGate = true; + analyzeBody(declaration.body, definition.body, /*global=*/false); + insideGate = false; + scopes.pop_back(); + program.gates.push_back(std::move(definition)); + } + + [[nodiscard]] StatementId + analyzeMeasurement(SMLoc location, const SyntaxMeasurement& measurement) { + auto qubits = resolveQubitOperand(measurement.source); + if (!measurement.target) { + return addStatement(location, + MeasurementStatement{.qubits = std::move(qubits)}); + } + const auto* destination = lookup(measurement.target->identifier); + if (destination != nullptr && destination->kind == SymbolKind::Scalar) { + if (measurement.target->index || destination->type != ScalarType::Bool) { + fail(location, "measurement assignment requires a bool scalar or bit " + "register destination"); + } + if (qubits.size() != 1) { + fail(location, "bool measurement assignment requires one qubit"); + } + const auto condition = + addCondition({.kind = ConditionKind::Measurement, + .location = sourceLocation(sources, location), + .measurement = qubits.front()}); + initializedScalars[destination->id] = true; + ++scalarGenerations[destination->id]; + return addStatement(location, + ScalarAssignmentStatement{.scalar = destination->id, + .condition = condition}); + } + auto targets = resolveBits(*measurement.target); + if (targets.size() != qubits.size()) { + fail(location, + "measurement target and qubit operand must have the same width"); + } + for (const auto& target : targets) { + markBitInitialized(target); + } + return addStatement(location, + MeasurementStatement{.targets = std::move(targets), + .qubits = std::move(qubits)}); + } + + [[nodiscard]] StatementId analyzeReset(SMLoc location, + const SyntaxReset& reset) { + auto qubits = resolveQubitOperand(reset.operand); + return addStatement(location, ResetStatement{.qubits = std::move(qubits)}); + } + + [[nodiscard]] StatementId analyzeBarrier(SMLoc location, + const SyntaxBarrier& barrier) { + std::vector qubits; + if (barrier.operands.empty()) { + for (const auto [registerId, declaration] : + llvm::enumerate(program.registers)) { + if (declaration.kind != RegisterKind::Qubit) { + continue; + } + for (std::uint64_t index = 0; index < declaration.width; ++index) { + qubits.push_back({.kind = QubitReferenceKind::Register, + .symbol = static_cast(registerId), + .index = index}); + } + } + for (const auto index : hardwareQubits) { + qubits.push_back( + {.kind = QubitReferenceKind::Hardware, .index = index}); + } + } + for (const auto& operand : barrier.operands) { + auto selection = resolveQubitOperand(operand); + qubits.insert(qubits.end(), selection.begin(), selection.end()); + } + return addStatement(location, + BarrierStatement{.qubits = std::move(qubits)}); + } + + [[nodiscard]] StatementId analyzeIf(SMLoc location, + const SyntaxIf& conditional) { + IfStatement result{.condition = analyzeCondition(conditional.condition)}; + const auto beforeBitsInitialized = initializedBits; + const auto beforeInitialized = initializedScalars; + const auto beforeGenerations = scalarGenerations; + const auto beforeDynamicBitFacts = dynamicBitFacts; + scopes.emplace_back(); + analyzeBody(conditional.thenStatements, result.thenStatements, + /*global=*/false); + const auto afterThenBitsInitialized = initializedBits; + const auto afterThenInitialized = initializedScalars; + const auto afterThenGenerations = scalarGenerations; + const auto afterThenDynamicBitFacts = dynamicBitFacts; + scopes.pop_back(); + + restoreStatePrefix(beforeBitsInitialized, beforeInitialized, + beforeGenerations); + restoreDynamicFactsPrefix(beforeDynamicBitFacts); + scopes.emplace_back(); + analyzeBody(conditional.elseStatements, result.elseStatements, + /*global=*/false); + const auto afterElseBitsInitialized = initializedBits; + const auto afterElseInitialized = initializedScalars; + const auto afterElseGenerations = scalarGenerations; + const auto afterElseDynamicBitFacts = dynamicBitFacts; + scopes.pop_back(); + + restoreStatePrefix(beforeBitsInitialized, beforeInitialized, + beforeGenerations); + restoreDynamicFactsPrefix(beforeDynamicBitFacts); + const auto knownCondition = constantCondition(conditional.condition); + for (std::size_t reg = 0; reg < beforeBitsInitialized.size(); ++reg) { + for (std::size_t bit = 0; bit < beforeBitsInitialized[reg].size(); + ++bit) { + initializedBits[reg][bit] = + knownCondition + ? (*knownCondition ? afterThenBitsInitialized[reg][bit] + : afterElseBitsInitialized[reg][bit]) + : afterThenBitsInitialized[reg][bit] && + afterElseBitsInitialized[reg][bit]; + } + } + for (std::size_t reg = 0; reg < beforeDynamicBitFacts.size(); ++reg) { + if (knownCondition) { + dynamicBitFacts[reg] = *knownCondition ? afterThenDynamicBitFacts[reg] + : afterElseDynamicBitFacts[reg]; + continue; + } + dynamicBitFacts[reg].clear(); + for (const auto& thenFact : afterThenDynamicBitFacts[reg]) { + if (llvm::any_of( + afterElseDynamicBitFacts[reg], [&](const auto& elseFact) { + return thenFact.dependencies == elseFact.dependencies && + sameExpression(thenFact.expression, + elseFact.expression); + })) { + dynamicBitFacts[reg].push_back(thenFact); + } + } + } + for (std::size_t scalar = 0; scalar < beforeInitialized.size(); ++scalar) { + initializedScalars[scalar] = + knownCondition + ? (*knownCondition ? afterThenInitialized[scalar] + : afterElseInitialized[scalar]) + : afterThenInitialized[scalar] && afterElseInitialized[scalar]; + scalarGenerations[scalar] = + knownCondition ? (*knownCondition ? afterThenGenerations[scalar] + : afterElseGenerations[scalar]) + : std::max(afterThenGenerations[scalar], + afterElseGenerations[scalar]); + } + return addStatement(location, std::move(result)); + } + + [[nodiscard]] StatementId analyzeFor(SMLoc location, const SyntaxFor& loop) { + ForStatement result{.start = analyzeExpression(loop.start), + .step = analyzeExpression(loop.step), + .stop = analyzeExpression(loop.stop)}; + for (const auto expression : {result.start, result.step, result.stop}) { + if (!isInteger(program.expressions[expression].type)) { + fail(location, "for-loop ranges require integer expressions"); + } + } + const auto constantIsZero = [](const Constant& value) { + return value.type == ScalarType::Uint + ? std::get(value.value) == 0 + : std::get(value.value) == 0; + }; + if (isConstantExpression(loop.step) && + constantIsZero(evaluateConstant(loop.step))) { + fail(location, "for-loop range step must not be zero"); + } + + const auto beforeBitsInitialized = initializedBits; + const auto beforeInitialized = initializedScalars; + const auto beforeGenerations = scalarGenerations; + const auto beforeDynamicBitFacts = dynamicBitFacts; + scopes.emplace_back(); + const auto scalar = static_cast(program.scalars.size()); + const auto type = loop.isUnsigned ? ScalarType::Uint : ScalarType::Int; + program.scalars.push_back( + {.type = type, .name = loop.inductionVariable.str()}); + initializedScalars.push_back(true); + scalarGenerations.push_back(0); + declare( + location, loop.inductionVariable, + {.kind = insideGate ? SymbolKind::GateLocalScalar : SymbolKind::Scalar, + .type = type, + .id = scalar}); + result.inductionVariable = scalar; + analyzeBody(loop.body, result.body, /*global=*/false); + const auto afterBodyBitsInitialized = initializedBits; + const auto afterBodyInitialized = initializedScalars; + const auto afterBodyGenerations = scalarGenerations; + const auto afterBodyDynamicBitFacts = dynamicBitFacts; + scopes.pop_back(); + restoreStatePrefix(beforeBitsInitialized, beforeInitialized, + beforeGenerations); + restoreDynamicFactsPrefix(beforeDynamicBitFacts); + if (isConstantExpression(loop.start) && isConstantExpression(loop.step) && + isConstantExpression(loop.stop)) { + const auto startConstant = evaluateConstant(loop.start); + const auto stepConstant = evaluateConstant(loop.step); + const auto stopConstant = evaluateConstant(loop.stop); + const bool unsignedEndpoints = startConstant.type == ScalarType::Uint || + stopConstant.type == ScalarType::Uint; + const auto compareRangeValues = [&](const Constant& lhs, + const Constant& rhs) { + if (!unsignedEndpoints) { + const auto left = std::get(lhs.value); + const auto right = std::get(rhs.value); + return left < right ? -1 : left > right ? 1 : 0; + } + const auto asUnsigned = [](const Constant& value) { + return value.type == ScalarType::Uint + ? std::get(value.value) + : static_cast( + std::get(value.value)); + }; + const auto left = asUnsigned(lhs); + const auto right = asUnsigned(rhs); + return left < right ? -1 : left > right ? 1 : 0; + }; + const bool positiveStep = stepConstant.type == ScalarType::Uint || + std::get(stepConstant.value) > 0; + const auto endpointOrder = + compareRangeValues(startConstant, stopConstant); + const bool nonempty = + positiveStep ? endpointOrder <= 0 : endpointOrder >= 0; + if (nonempty) { + for (std::size_t reg = 0; reg < beforeBitsInitialized.size(); ++reg) { + initializedBits[reg] = afterBodyBitsInitialized[reg]; + } + for (std::size_t scalar = 0; scalar < beforeInitialized.size(); + ++scalar) { + initializedScalars[scalar] = afterBodyInitialized[scalar]; + scalarGenerations[scalar] = afterBodyGenerations[scalar]; + } + for (std::size_t reg = 0; reg < beforeDynamicBitFacts.size(); ++reg) { + dynamicBitFacts[reg] = afterBodyDynamicBitFacts[reg]; + } + } + } + return addStatement(location, std::move(result)); + } + + [[nodiscard]] StatementId analyzeWhile(SMLoc location, + const SyntaxWhile& loop) { + WhileStatement result{.condition = analyzeCondition(loop.condition)}; + const auto beforeBitsInitialized = initializedBits; + const auto beforeInitialized = initializedScalars; + const auto beforeGenerations = scalarGenerations; + const auto beforeDynamicBitFacts = dynamicBitFacts; + scopes.emplace_back(); + analyzeBody(loop.body, result.body, /*global=*/false); + scopes.pop_back(); + const auto afterBodyGenerations = scalarGenerations; + restoreStatePrefix(beforeBitsInitialized, beforeInitialized, + beforeGenerations); + restoreDynamicFactsPrefix(beforeDynamicBitFacts); + for (std::size_t scalar = 0; scalar < beforeGenerations.size(); ++scalar) { + scalarGenerations[scalar] = + std::max(beforeGenerations[scalar], afterBodyGenerations[scalar]); + } + return addStatement(location, std::move(result)); + } + + [[nodiscard]] ConditionId + analyzeCondition(const SyntaxExpressionId syntaxId) { + const auto& condition = syntax.expressions[syntaxId]; + ConditionExpression typed{.location = + sourceLocation(sources, condition.location)}; + if (isConstantExpression(syntaxId)) { + const auto constant = evaluateConstant(syntaxId); + if (constant.type != ScalarType::Bool) { + fail(condition.location, "condition must have bool type"); + } + typed.kind = ConditionKind::Literal; + typed.literal = std::get(constant.value); + return addCondition(std::move(typed)); + } + switch (condition.kind) { + case Expr::Kind::Identifier: { + const auto* symbol = lookup(condition.identifier); + if (symbol == nullptr) { + fail(condition.location, + "unknown condition identifier '" + condition.identifier + "'"); + } + if (symbol->kind == SymbolKind::Scalar && + symbol->type == ScalarType::Bool) { + if (!initializedScalars.at(symbol->id)) { + fail(condition.location, + "scalar '" + condition.identifier + "' is uninitialized"); + } + typed.kind = ConditionKind::Scalar; + typed.scalar = symbol->id; + break; + } + if (symbol->kind != SymbolKind::Register || + program.registers[symbol->id].kind != RegisterKind::Bit) { + fail(condition.location, "identifier '" + condition.identifier + + "' is not bool or a classical bit"); + } + auto bits = resolveBits( + {.location = condition.location, .identifier = condition.identifier}); + if (bits.size() != 1) { + fail(condition.location, + "condition must select exactly one classical bit"); + } + ensureBitInitialized(bits.front(), condition.location); + typed.kind = ConditionKind::Bit; + typed.bit = bits.front(); + break; + } + case Expr::Kind::Index: { + auto bits = resolveBits({.location = condition.location, + .identifier = condition.identifier, + .index = condition.lhs}); + if (bits.size() != 1) { + fail(condition.location, + "condition must select exactly one classical bit"); + } + ensureBitInitialized(bits.front(), condition.location); + typed.kind = ConditionKind::Bit; + typed.bit = bits.front(); + break; + } + case Expr::Kind::Measurement: { + auto qubits = + resolveQubitOperand({.location = condition.location, + .identifier = condition.identifier, + .index = condition.lhs, + .hardwareQubit = condition.hardwareQubit}); + if (qubits.size() != 1) { + fail(condition.location, + "measurement condition must select exactly one qubit"); + } + typed.kind = ConditionKind::Measurement; + typed.measurement = qubits.front(); + break; + } + case Expr::Kind::Not: + typed.kind = ConditionKind::Not; + typed.lhs = analyzeCondition(*condition.lhs); + break; + case Expr::Kind::And: + case Expr::Kind::Or: + typed.kind = condition.kind == Expr::Kind::And ? ConditionKind::And + : ConditionKind::Or; + typed.lhs = analyzeCondition(*condition.lhs); + typed.rhs = analyzeCondition(*condition.rhs); + break; + case Expr::Kind::Equal: + case Expr::Kind::NotEqual: + case Expr::Kind::Less: + case Expr::Kind::LessEqual: + case Expr::Kind::Greater: + case Expr::Kind::GreaterEqual: { + const auto& lhsSyntax = syntax.expressions[*condition.lhs]; + const auto* lhsSymbol = lhsSyntax.kind == Expr::Kind::Identifier + ? lookup(lhsSyntax.identifier) + : nullptr; + if (program.openQASM2 && condition.kind == Expr::Kind::Equal && + lhsSymbol != nullptr && lhsSymbol->kind == SymbolKind::Register && + program.registers[lhsSymbol->id].kind == RegisterKind::Bit && + isConstantExpression(*condition.rhs)) { + const auto expected = evaluateConstant(*condition.rhs); + if (!isInteger(expected.type) || + (expected.type == ScalarType::Int && + std::get(expected.value) < 0)) { + fail(condition.location, + "OpenQASM 2 register conditions require an unsigned integer"); + } + const auto expectedValue = + expected.type == ScalarType::Uint + ? std::get(expected.value) + : static_cast( + std::get(expected.value)); + auto bits = resolveBits({.location = lhsSyntax.location, + .identifier = lhsSyntax.identifier}); + for (const auto& bit : bits) { + ensureBitInitialized(bit, condition.location); + } + const bool fits = + bits.size() >= 64 || (expectedValue >> bits.size()) == 0; + auto result = addCondition( + {.kind = ConditionKind::Literal, + .location = sourceLocation(sources, condition.location), + .literal = fits}); + for (const auto [index, bit] : llvm::enumerate(bits)) { + auto bitCondition = addCondition( + {.kind = ConditionKind::Bit, + .location = sourceLocation(sources, condition.location), + .bit = bit}); + const bool expectedBit = + index < 64 && ((expectedValue >> index) & 1U) != 0; + if (!expectedBit) { + bitCondition = addCondition( + {.kind = ConditionKind::Not, + .location = sourceLocation(sources, condition.location), + .lhs = bitCondition}); + } + result = addCondition( + {.kind = ConditionKind::And, + .location = sourceLocation(sources, condition.location), + .lhs = result, + .rhs = bitCondition}); + } + return result; + } + typed.kind = ConditionKind::Comparison; + typed.comparisonLhs = analyzeExpression(*condition.lhs); + typed.comparisonRhs = analyzeExpression(*condition.rhs); + const auto lhsType = program.expressions[typed.comparisonLhs].type; + const auto rhsType = program.expressions[typed.comparisonRhs].type; + const bool boolComparison = + lhsType == ScalarType::Bool || rhsType == ScalarType::Bool; + if (boolComparison && + (lhsType != ScalarType::Bool || rhsType != ScalarType::Bool || + (condition.kind != Expr::Kind::Equal && + condition.kind != Expr::Kind::NotEqual))) { + fail(condition.location, + "bool values only support equality comparisons with bool values"); + } + switch (condition.kind) { + case Expr::Kind::Equal: + typed.comparison = ComparisonKind::Equal; + break; + case Expr::Kind::NotEqual: + typed.comparison = ComparisonKind::NotEqual; + break; + case Expr::Kind::Less: + typed.comparison = ComparisonKind::Less; + break; + case Expr::Kind::LessEqual: + typed.comparison = ComparisonKind::LessEqual; + break; + case Expr::Kind::Greater: + typed.comparison = ComparisonKind::Greater; + break; + case Expr::Kind::GreaterEqual: + typed.comparison = ComparisonKind::GreaterEqual; + break; + default: + llvm_unreachable("not a comparison expression"); + } + break; + } + case Expr::Kind::Int: + case Expr::Kind::Float: + case Expr::Kind::Bool: + case Expr::Kind::Neg: + case Expr::Kind::BitNot: + case Expr::Kind::Add: + case Expr::Kind::Sub: + case Expr::Kind::Mul: + case Expr::Kind::Div: + case Expr::Kind::ArcCos: + case Expr::Kind::ArcSin: + case Expr::Kind::ArcTan: + case Expr::Kind::Cos: + case Expr::Kind::Exp: + case Expr::Kind::Log: + case Expr::Kind::Mod: + case Expr::Kind::BuiltinMod: + case Expr::Kind::Pow: + case Expr::Kind::BuiltinPow: + case Expr::Kind::BitAnd: + case Expr::Kind::BitOr: + case Expr::Kind::BitXor: + case Expr::Kind::ShiftLeft: + case Expr::Kind::ShiftRight: + case Expr::Kind::Sin: + case Expr::Kind::Sqrt: + case Expr::Kind::Tan: + fail(condition.location, "condition must have bool type"); + } + return addCondition(std::move(typed)); + } + + [[nodiscard]] std::vector + analyzeGateApplication(const SyntaxGateCall& call) { + std::string callee = call.identifier.str(); + const GateCatalogEntry* standard = lookupGate(callee); + auto custom = customGates.find(callee); + std::uint64_t compatibilityControls = 0; + if (standard == nullptr && custom == customGates.end() && + program.openQASM2) { + auto stripped = callee; + while (!stripped.empty() && stripped.front() == 'c') { + stripped.erase(stripped.begin()); + ++compatibilityControls; + } + standard = lookupGate(stripped); + custom = customGates.find(stripped); + if (standard != nullptr || custom != customGates.end()) { + callee = std::move(stripped); + } + } + if (standard != nullptr && !isGateAvailable(*standard)) { + standard = nullptr; + } + if (standard == nullptr && custom == customGates.end()) { + fail(call.location, + "No OpenQASM definition found for gate '" + call.identifier + "'."); + } + + const auto signature = + standard != nullptr + ? GateSignature{.parameterCount = standard->parameterCount, + .qubitCount = standard->qubitCount(), + .variadicControls = standard->variadicControls} + : custom->second; + if (signature.parameterCount != call.parameters.size()) { + fail(call.location, + "Invalid number of parameters for gate '" + call.identifier + "'."); + } + std::vector parameters; + parameters.reserve(call.parameters.size()); + for (const auto expression : call.parameters) { + const auto parameter = analyzeExpression(expression); + if (program.expressions[parameter].type == ScalarType::Bool) { + fail(call.location, "gate parameters require numeric expressions"); + } + parameters.push_back(parameter); + } + + std::vector modifiers; + std::size_t addedControls = compatibilityControls; + if (addedControls > call.operands.size()) { + fail(call.location, "Invalid number of qubit operands for gate '" + + call.identifier + "'."); + } + for (const auto& modifier : call.modifiers) { + switch (modifier.kind) { + case Modifier::Kind::Inv: + modifiers.push_back({.kind = ModifierKind::Inv}); + break; + case Modifier::Kind::Pow: + if (!modifier.argument) { + fail(call.location, "pow modifier requires an argument"); + } + { + const auto operand = analyzeExpression(*modifier.argument); + if (program.expressions[operand].type == ScalarType::Bool) { + fail(call.location, "pow modifier requires a numeric argument"); + } + modifiers.push_back({.kind = ModifierKind::Pow, .operand = operand}); + } + break; + case Modifier::Kind::Ctrl: + case Modifier::Kind::NegCtrl: { + std::uint64_t count = 1; + std::optional operand; + if (modifier.argument) { + if (!isConstantExpression(*modifier.argument)) { + fail(call.location, + "gate control count must be a constant integer"); + } + const auto constant = evaluateConstant(*modifier.argument); + if (!isInteger(constant.type) || asSigned(constant) <= 0) { + fail(call.location, "gate control count must be positive"); + } + count = static_cast(asSigned(constant)); + operand = addConstant({.type = ScalarType::Int, + .value = static_cast(count)}); + } + if (count > call.operands.size() - addedControls) { + fail(call.location, "Invalid number of qubit operands for gate '" + + call.identifier + "'."); + } + addedControls += static_cast(count); + modifiers.push_back({.kind = modifier.kind == Modifier::Kind::Ctrl + ? ModifierKind::Ctrl + : ModifierKind::NegCtrl, + .operand = operand}); + break; + } + } + } + if (compatibilityControls != 0) { + modifiers.insert( + modifiers.begin(), + {.kind = ModifierKind::Ctrl, + .operand = addConstant( + {.type = ScalarType::Int, + .value = static_cast(compatibilityControls)})}); + } + + const auto baseOperandCount = call.operands.size() - addedControls; + if (signature.variadicControls ? baseOperandCount < signature.qubitCount + : baseOperandCount != signature.qubitCount) { + fail(call.location, "Invalid number of qubit operands for gate '" + + call.identifier + "'."); + } + + std::size_t emittedOperandCount = call.operands.size(); + if (standard != nullptr && standard->variadicControls) { + std::size_t activeBaseOperands = baseOperandCount; + if (standard->name == "mcx_vchain") { + if (baseOperandCount < 5) { + fail(call.location, + "mcx_vchain requires controls, a target, and ancillas"); + } + const auto ancillas = ((baseOperandCount + 1) / 2) - 2; + activeBaseOperands -= ancillas; + } else if (standard->name == "mcx_recursive" && baseOperandCount > 5) { + --activeBaseOperands; + } + if (activeBaseOperands <= standard->targetCount) { + fail(call.location, + "Invalid number of controls for gate '" + call.identifier + "'."); + } + const auto intrinsicControls = activeBaseOperands - standard->targetCount; + modifiers.push_back( + {.kind = ModifierKind::Ctrl, + .operand = addConstant( + {.type = ScalarType::Int, + .value = static_cast(intrinsicControls)})}); + callee = standard->primitive.str(); + emittedOperandCount = addedControls + activeBaseOperands; + } + + std::vector> selections; + std::size_t broadcastWidth = 1; + for (const auto& operand : call.operands) { + auto selection = resolveQubitOperand(operand); + if (selection.size() > 1) { + if (broadcastWidth != 1 && broadcastWidth != selection.size()) { + fail(call.location, + "all broadcasting operands must have the same width"); + } + broadcastWidth = selection.size(); + } + selections.push_back(std::move(selection)); + } + + std::vector applications; + applications.reserve(broadcastWidth); + for (std::size_t index = 0; index < broadcastWidth; ++index) { + GateApplication application{ + .callee = callee, .parameters = parameters, .modifiers = modifiers}; + for (const auto& selection : + ArrayRef(selections).take_front(emittedOperandCount)) { + application.qubits.push_back( + selection[selection.size() == 1 ? 0 : index]); + } + for (const auto [position, qubit] : llvm::enumerate(application.qubits)) { + if (llvm::is_contained( + ArrayRef(application.qubits).take_front(position), qubit)) { + fail(call.location, + "gate operands must not reference the same qubit more than " + "once"); + } + } + applications.push_back(std::move(application)); + } + return applications; + } + + [[nodiscard]] std::vector + resolveQubitOperand(const SyntaxOperand& operand) { + if (operand.hardwareQubit) { + if (insideGate) { + fail(operand.location, + "hardware qubits are not allowed in gate definitions"); + } + if (hasVirtualQubits) { + fail(operand.location, + "mixing physical and declared qubits is not supported by the QC " + "target"); + } + hasHardwareQubits = true; + hardwareQubits.insert(*operand.hardwareQubit); + return {{.kind = QubitReferenceKind::Hardware, + .index = *operand.hardwareQubit}}; + } + const auto* symbol = lookup(operand.identifier); + if (insideGate) { + if (symbol == nullptr || symbol->kind != SymbolKind::GateQubit) { + fail(operand.location, + "unknown gate-local qubit '" + operand.identifier + "'"); + } + if (operand.index) { + fail(operand.location, "gate-local qubits cannot be indexed"); + } + return {{.kind = QubitReferenceKind::GateArgument, .symbol = symbol->id}}; + } + if (symbol == nullptr || symbol->kind != SymbolKind::Register || + program.registers[symbol->id].kind != RegisterKind::Qubit) { + fail(operand.location, + "unknown qubit register '" + operand.identifier + "'"); + } + const auto reg = static_cast(symbol->id); + const auto width = program.registers[reg].width; + if (!operand.index) { + std::vector selection; + selection.reserve(width); + for (std::uint64_t index = 0; index < width; ++index) { + selection.push_back({.kind = QubitReferenceKind::Register, + .symbol = reg, + .index = index}); + } + return selection; + } + if (const auto index = + constantIndex(*operand.index, width, operand.location)) { + if (*index >= width) { + fail(operand.location, "qubit index is out of bounds"); + } + return {{.kind = QubitReferenceKind::Register, + .symbol = reg, + .index = *index}}; + } + const auto dynamic = analyzeExpression(*operand.index); + if (!isInteger(program.expressions[dynamic].type)) { + fail(operand.location, "qubit index must be an integer expression"); + } + return {{.kind = QubitReferenceKind::Register, + .symbol = reg, + .dynamicIndex = dynamic}}; + } + + [[nodiscard]] std::vector + resolveBits(const SyntaxBitReference& reference) { + const auto* symbol = lookup(reference.identifier); + if (symbol == nullptr || symbol->kind != SymbolKind::Register || + program.registers[symbol->id].kind == RegisterKind::Qubit) { + fail(reference.location, + "unknown classical register '" + reference.identifier + "'"); + } + const auto reg = static_cast(symbol->id); + const auto width = program.registers[reg].width; + if (!reference.index) { + std::vector result; + result.reserve(width); + for (std::uint64_t index = 0; index < width; ++index) { + result.push_back({.reg = reg, .index = index}); + } + return result; + } + if (const auto index = + constantIndex(*reference.index, width, reference.location)) { + if (*index >= width) { + fail(reference.location, "classical bit index is out of bounds"); + } + return {{.reg = reg, .index = *index}}; + } + const auto dynamic = analyzeExpression(*reference.index); + if (!isInteger(program.expressions[dynamic].type)) { + fail(reference.location, + "classical bit index must be an integer expression"); + } + return {{.reg = reg, .dynamicIndex = dynamic}}; + } + + void ensureBitInitialized(const frontend::BitReference& bit, + SMLoc location) const { + if (bit.dynamicIndex) { + if (llvm::all_of(initializedBits[bit.reg], + [](const bool initialized) { return initialized; })) { + return; + } + std::vector> dependencies; + collectDependencies(*bit.dynamicIndex, dependencies); + if (llvm::any_of(dynamicBitFacts[bit.reg], [&](const auto& fact) { + return fact.dependencies == dependencies && + sameExpression(fact.expression, *bit.dynamicIndex); + })) { + return; + } + fail(location, "dynamic classical index may read an uninitialized bit"); + } + if (!initializedBits[bit.reg][bit.index]) { + fail(location, "classical condition bit has not been initialized"); + } + } + + void finalizeOutputs() { + program.outputs = explicitOutputs.empty() ? bitRegisters : explicitOutputs; + for (const auto reg : program.outputs) { + if (llvm::any_of(initializedBits[reg], + [](const bool initialized) { return !initialized; })) { + throw SemanticError({.location = program.registers[reg].location, + .message = "Output register '" + + program.registers[reg].name + + "' is not fully initialized."}); + } + } + } +}; + +} // namespace + +SourceLocation sourceLocation(const llvm::SourceMgr& sources, + const llvm::SMLoc location) { + if (!location.isValid()) { + return {}; + } + const auto bufferId = sources.FindBufferContainingLoc(location); + if (bufferId == 0) { + return {}; + } + const auto [line, column] = sources.getLineAndColumn(location, bufferId); + const auto* buffer = sources.getMemoryBuffer(bufferId); + return {.filename = buffer->getBufferIdentifier().str(), + .line = line, + .column = column}; +} + +AnalysisResult analyzeSyntaxProgram(const SyntaxProgram& syntax, + const llvm::SourceMgr& sources, + const FrontendOptions& options) { + return SemanticAnalyzer(syntax, sources, options).run(); +} + +} // namespace mlir::oq3::frontend::detail diff --git a/mlir/lib/Target/OpenQASM/OpenQASMSyntax.cpp b/mlir/lib/Target/OpenQASM/OpenQASMSyntax.cpp new file mode 100644 index 0000000000..a8f9b2718e --- /dev/null +++ b/mlir/lib/Target/OpenQASM/OpenQASMSyntax.cpp @@ -0,0 +1,278 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#include "mlir/Target/OpenQASM/Detail/OpenQASMSyntax.h" + +#include +#include + +#include + +namespace mlir::oq3::frontend::detail { + +LogicalResult SyntaxBuilder::error(SMLoc location, const Twine& message) { + diagnostics.push_back({.location = location, .message = message.str()}); + return failure(); +} + +LogicalResult SyntaxBuilder::version(SMLoc location, const Version value) { + if (program.version || sawConstruct) { + return error(location, + "version declaration must be the first source construct"); + } + program.version = value; + program.versionLocation = location; + sawConstruct = true; + return success(); +} + +LogicalResult SyntaxBuilder::include(SMLoc location, StringRef filename) { + program.includes.push_back({.location = location, + .filename = filename, + .bodyOffset = program.body.size()}); + sawConstruct = true; + return success(); +} + +SyntaxStatementId SyntaxBuilder::standardLibraryInclude(SMLoc location) { + const auto id = static_cast(program.statements.size()); + program.statements.push_back( + {.location = location, .data = SyntaxStandardLibraryInclude{}}); + return id; +} + +void SyntaxBuilder::replaceBody(std::vector body) { + program.body = std::move(body); +} + +SyntaxStatementId SyntaxBuilder::addStatement(SMLoc location, + SyntaxStatementData data) { + sawConstruct = true; + const auto id = static_cast(program.statements.size()); + program.statements.push_back({.location = location, .data = std::move(data)}); + bodyStack.back()->push_back(id); + return id; +} + +SyntaxExpressionId SyntaxBuilder::copyExpression(const Expr& expression) { + SyntaxExpression copy{.kind = expression.kind, + .location = expression.loc, + .integer = expression.intValue, + .floatingPoint = expression.floatValue, + .boolean = expression.boolValue, + .identifier = expression.identifier, + .hardwareQubit = expression.hardwareQubit}; + if (expression.lhs != nullptr) { + copy.lhs = copyExpression(*expression.lhs); + } + if (expression.rhs != nullptr) { + copy.rhs = copyExpression(*expression.rhs); + } + const auto id = static_cast(program.expressions.size()); + program.expressions.push_back(copy); + return id; +} + +SyntaxOperand SyntaxBuilder::copyOperand(const Operand& operand) { + SyntaxOperand copy{.location = operand.loc, + .identifier = operand.identifier, + .hardwareQubit = operand.hardwareQubit}; + if (operand.index != nullptr) { + copy.index = copyExpression(*operand.index); + } + return copy; +} + +SyntaxBitReference +SyntaxBuilder::copyBitReference(const BitReference& reference) { + SyntaxBitReference copy{.location = reference.loc, + .identifier = reference.identifier}; + if (reference.index != nullptr) { + copy.index = copyExpression(*reference.index); + } + return copy; +} + +SyntaxGateCall SyntaxBuilder::copyGateCall(const GateCall& call) { + SyntaxGateCall copy{.location = call.loc, .identifier = call.identifier}; + copy.modifiers.reserve(call.modifiers.size()); + for (const auto& modifier : call.modifiers) { + SyntaxModifier converted{.kind = modifier.kind}; + if (modifier.argument != nullptr) { + converted.argument = copyExpression(*modifier.argument); + } + copy.modifiers.push_back(converted); + } + copy.parameters.reserve(call.parameters.size()); + for (const auto* parameter : call.parameters) { + copy.parameters.push_back(copyExpression(*parameter)); + } + copy.operands.reserve(call.operands.size()); + llvm::transform(call.operands, std::back_inserter(copy.operands), + [&](const Operand& operand) { return copyOperand(operand); }); + return copy; +} + +LogicalResult SyntaxBuilder::scalarDecl(SMLoc location, const ScalarKind kind, + StringRef identifier, + const Expr* initializer, + const bool isConst) { + SyntaxScalarDeclaration declaration{ + .kind = kind, .identifier = identifier, .isConst = isConst}; + if (initializer != nullptr) { + declaration.initializer = copyExpression(*initializer); + } + (void)addStatement(location, declaration); + return success(); +} + +LogicalResult SyntaxBuilder::assignment(SMLoc location, + const BitReference& target, + const Expr& value) { + (void)addStatement(location, + SyntaxAssignment{.target = copyBitReference(target), + .value = copyExpression(value)}); + return success(); +} + +LogicalResult SyntaxBuilder::qubitRegister(SMLoc location, StringRef identifier, + const Expr* size) { + SyntaxQubitDeclaration declaration{.identifier = identifier}; + if (size != nullptr) { + declaration.size = copyExpression(*size); + } + (void)addStatement(location, declaration); + return success(); +} + +LogicalResult SyntaxBuilder::classicalRegister(SMLoc location, + StringRef identifier, + const Expr* size, + const Expr* initializer, + const bool output) { + SyntaxBitDeclaration declaration{.identifier = identifier, .output = output}; + if (size != nullptr) { + declaration.size = copyExpression(*size); + } + if (initializer != nullptr) { + declaration.initializer = copyExpression(*initializer); + } + (void)addStatement(location, declaration); + return success(); +} + +LogicalResult SyntaxBuilder::measure(SMLoc location, const BitReference* target, + const Operand& source) { + SyntaxMeasurement measurement{.source = copyOperand(source)}; + if (target != nullptr) { + measurement.target = copyBitReference(*target); + } + (void)addStatement(location, measurement); + return success(); +} + +LogicalResult SyntaxBuilder::reset(SMLoc location, const Operand& operand) { + (void)addStatement(location, SyntaxReset{.operand = copyOperand(operand)}); + return success(); +} + +LogicalResult SyntaxBuilder::barrier(SMLoc location, + ArrayRef operands) { + SyntaxBarrier barrier; + barrier.operands.reserve(operands.size()); + llvm::transform(operands, std::back_inserter(barrier.operands), + [&](const Operand& operand) { return copyOperand(operand); }); + (void)addStatement(location, std::move(barrier)); + return success(); +} + +LogicalResult SyntaxBuilder::gateCall(const GateCall& call) { + (void)addStatement(call.loc, copyGateCall(call)); + return success(); +} + +LogicalResult SyntaxBuilder::gateDefinition( + SMLoc location, StringRef identifier, ArrayRef parameters, + ArrayRef qubits, function_ref continuation) { + SyntaxGateDefinition definition{.identifier = identifier, + .parameters = parameters.vec(), + .qubits = qubits.vec()}; + auto body = parseNestedBody(continuation); + if (failed(body)) { + return failure(); + } + definition.body = std::move(*body); + (void)addStatement(location, std::move(definition)); + return success(); +} + +FailureOr> +SyntaxBuilder::parseNestedBody(function_ref continuation) { + std::vector body; + bodyStack.push_back(&body); + const auto result = continuation(); + bodyStack.pop_back(); + if (failed(result)) { + return failure(); + } + return body; +} + +LogicalResult +SyntaxBuilder::ifStmt(SMLoc location, const Expr& condition, + function_ref thenContinuation, + function_ref elseContinuation) { + auto thenStatements = parseNestedBody(thenContinuation); + if (failed(thenStatements)) { + return failure(); + } + auto elseStatements = parseNestedBody(elseContinuation); + if (failed(elseStatements)) { + return failure(); + } + (void)addStatement(location, + SyntaxIf{.condition = copyExpression(condition), + .thenStatements = std::move(*thenStatements), + .elseStatements = std::move(*elseStatements)}); + return success(); +} + +LogicalResult +SyntaxBuilder::forStmt(SMLoc location, StringRef inductionVariable, + const bool isUnsigned, const Expr& start, + const Expr& step, const Expr& stop, + function_ref continuation) { + auto body = parseNestedBody(continuation); + if (failed(body)) { + return failure(); + } + (void)addStatement(location, SyntaxFor{.inductionVariable = inductionVariable, + .isUnsigned = isUnsigned, + .start = copyExpression(start), + .step = copyExpression(step), + .stop = copyExpression(stop), + .body = std::move(*body)}); + return success(); +} + +LogicalResult +SyntaxBuilder::whileStmt(SMLoc location, const Expr& condition, + function_ref continuation) { + auto body = parseNestedBody(continuation); + if (failed(body)) { + return failure(); + } + (void)addStatement(location, + SyntaxWhile{.condition = copyExpression(condition), + .body = std::move(*body)}); + return success(); +} + +} // namespace mlir::oq3::frontend::detail diff --git a/mlir/tools/mqt-cc/mqt-cc.cpp b/mlir/tools/mqt-cc/mqt-cc.cpp index 5287d5c7d0..5a029146d7 100644 --- a/mlir/tools/mqt-cc/mqt-cc.cpp +++ b/mlir/tools/mqt-cc/mqt-cc.cpp @@ -37,6 +37,7 @@ #include #include #include +#include #include #include #include @@ -334,7 +335,7 @@ int main(int argc, char** argv) { registry.insert(); + scf::SCFDialect, math::MathDialect, jeff::JeffDialect>(); registerBuiltinDialectTranslation(registry); registerLLVMDialectTranslation(registry); diff --git a/mlir/unittests/CMakeLists.txt b/mlir/unittests/CMakeLists.txt index da76b50c37..f5715b6148 100644 --- a/mlir/unittests/CMakeLists.txt +++ b/mlir/unittests/CMakeLists.txt @@ -18,3 +18,4 @@ add_subdirectory(programs) add_subdirectory(Compiler) add_subdirectory(Dialect) add_subdirectory(Conversion) +add_subdirectory(Target) diff --git a/mlir/unittests/Compiler/CMakeLists.txt b/mlir/unittests/Compiler/CMakeLists.txt index a750c1d0f8..0aee02bb65 100644 --- a/mlir/unittests/Compiler/CMakeLists.txt +++ b/mlir/unittests/Compiler/CMakeLists.txt @@ -13,6 +13,7 @@ target_link_libraries( PRIVATE GTest::gtest_main MQTCompilerPipeline MLIRQCTranslation + MLIRQASMPrograms MLIRQCProgramBuilder MLIRQIRProgramBuilder MLIRParser diff --git a/mlir/unittests/Compiler/test_compiler_pipeline.cpp b/mlir/unittests/Compiler/test_compiler_pipeline.cpp index 092a2a8811..c0568bb167 100644 --- a/mlir/unittests/Compiler/test_compiler_pipeline.cpp +++ b/mlir/unittests/Compiler/test_compiler_pipeline.cpp @@ -17,9 +17,11 @@ #include "mlir/Dialect/QCO/Builder/QCOProgramBuilder.h" #include "mlir/Dialect/QCO/IR/QCODialect.h" #include "mlir/Dialect/QIR/Builder/QIRProgramBuilder.h" +#include "mlir/Dialect/QIR/Utils/QIRUtils.h" #include "mlir/Dialect/QTensor/IR/QTensorDialect.h" #include "mlir/Support/IRVerification.h" #include "mlir/Support/Passes.h" +#include "qasm_programs.h" #include "qc_programs.h" #include "qco_programs.h" #include "qir_programs.h" @@ -32,8 +34,11 @@ #include #include #include +#include #include #include +#include +#include #include #include #include @@ -43,12 +48,15 @@ #include #include +#include #include +#include #include #include #include #include #include +#include #include #include #include @@ -263,6 +271,314 @@ h q; EXPECT_TRUE(mlir::verify(*reparsed).succeeded()); } +namespace { + +class OpenQASMCompilerPipelineTest + : public testing::TestWithParam {}; + +struct EntryInfo { + std::vector resultTypes; + std::vector outputRecordings; +}; + +[[nodiscard]] std::string +openQASMProgramName(const testing::TestParamInfo& info) { + std::string name = info.param.name.str(); + for (auto& character : name) { + if (!std::isalnum(static_cast(character))) { + character = '_'; + } + } + return name; +} + +[[nodiscard]] std::string printType(const Type type) { + std::string text; + llvm::raw_string_ostream stream(text); + type.print(stream); + return text; +} + +[[nodiscard]] std::optional inspectEntry(const llvm::StringRef ir) { + DialectRegistry registry; + registry.insert(); + MLIRContext context(registry); + context.loadAllAvailableDialects(); + auto module = parseSourceString(ir, &context); + if (!module) { + return std::nullopt; + } + + EntryInfo info; + if (auto main = module->lookupSymbol("main")) { + for (const auto type : main.getFunctionType().getResults()) { + info.resultTypes.push_back(printType(type)); + } + return info; + } + + auto main = module->lookupSymbol("main"); + if (!main) { + return std::nullopt; + } + const auto result = main.getFunctionType().getReturnType(); + if (!isa(result)) { + info.resultTypes.push_back(printType(result)); + } + main.walk([&](LLVM::CallOp call) { + const auto callee = call.getCallee(); + if (callee && + (*callee == QIR_RECORD_OUTPUT || *callee == QIR_ARRAY_RECORD_OUTPUT)) { + info.outputRecordings.emplace_back(*callee); + } + }); + return info; +} + +[[nodiscard]] testing::AssertionResult +throughOptimizedQCO(const qasm::OpenQASMProgram& source, + std::optional& restored, + std::vector& resultTypes) { + auto qc = QCProgram::fromQASMString(source.source.str()); + if (!qc) { + return testing::AssertionFailure() + << source.name.str() << ": OpenQASM to QC"; + } + const auto qcEntry = inspectEntry(qc->str()); + if (!qcEntry) { + return testing::AssertionFailure() + << source.name.str() << ": inspect QC entry"; + } + resultTypes = qcEntry->resultTypes; + auto qco = std::move(*qc).intoQCO(); + if (!qco || !qco->cleanup() || !qco->runPassPipeline("mqt-qco-default") || + !qco->cleanup()) { + return testing::AssertionFailure() + << source.name.str() << ": QC/QCO optimization"; + } + restored = std::move(*qco).intoQC(); + if (!restored || !restored->cleanup()) { + return testing::AssertionFailure() + << source.name.str() << ": optimized QCO to QC"; + } + const auto restoredEntry = inspectEntry(restored->str()); + if (!restoredEntry || restoredEntry->resultTypes != resultTypes) { + return testing::AssertionFailure() + << source.name.str() << ": reconstructed QC changed entry results"; + } + return testing::AssertionSuccess(); +} + +[[nodiscard]] testing::AssertionResult +roundTripThroughOptimizedJeff(const qasm::OpenQASMProgram& source, + std::optional& restored, + std::vector& resultTypes) { + auto qc = QCProgram::fromQASMString(source.source.str()); + if (!qc) { + return testing::AssertionFailure() + << source.name.str() << ": OpenQASM to QC"; + } + const auto qcEntry = inspectEntry(qc->str()); + if (!qcEntry) { + return testing::AssertionFailure() + << source.name.str() << ": inspect QC entry"; + } + resultTypes = qcEntry->resultTypes; + + const auto matchesEntry = [&](const Program& program, + const llvm::StringRef stage) { + const auto entry = inspectEntry(program.str()); + if (!entry) { + return testing::AssertionFailure() + << source.name.str() << ": inspect " << stage.str() << " entry"; + } + if (entry->resultTypes != resultTypes) { + return testing::AssertionFailure() + << source.name.str() << ": " << stage.str() + << " changed entry result types"; + } + return testing::AssertionSuccess(); + }; + + auto qco = std::move(*qc).intoQCO(); + if (!qco || !qco->cleanup() || !qco->runPassPipeline("mqt-qco-default") || + !qco->cleanup()) { + return testing::AssertionFailure() + << source.name.str() << ": QC/QCO optimization"; + } + if (auto result = matchesEntry(*qco, "optimized QCO"); !result) { + return result; + } + const auto optimizedQCO = qco->str(); + auto jeff = std::move(*qco).intoJeff(); + if (!jeff || !jeff->cleanup()) { + return testing::AssertionFailure() << source.name.str() << ": QCO to jeff\n" + << optimizedQCO; + } + if (auto result = matchesEntry(*jeff, "jeff"); !result) { + return result; + } + const auto bytes = jeff->toBytes(); + if (bytes.empty()) { + return testing::AssertionFailure() + << source.name.str() << ": jeff serialization"; + } + auto restoredJeff = JeffProgram::fromBytes(bytes); + if (!restoredJeff || !restoredJeff->cleanup()) { + return testing::AssertionFailure() + << source.name.str() << ": jeff deserialization"; + } + if (auto result = matchesEntry(*restoredJeff, "restored jeff"); !result) { + return result; + } + auto restoredQCO = std::move(*restoredJeff).intoQCO(); + if (!restoredQCO || !restoredQCO->cleanup()) { + return testing::AssertionFailure() + << source.name.str() << ": restored jeff to QCO"; + } + if (auto result = matchesEntry(*restoredQCO, "restored QCO"); !result) { + return result; + } + restored = std::move(*restoredQCO).intoQC(); + if (!restored || !restored->cleanup()) { + return testing::AssertionFailure() + << source.name.str() << ": restored QCO to QC"; + } + return matchesEntry(*restored, "restored QC"); +} + +enum class OutputRecordingShape : std::uint8_t { Arrays, Scalars }; + +void expectQIRArtifacts(const QIRProgram& program, const llvm::StringRef name, + const ArrayRef sourceResultTypes, + const OutputRecordingShape outputShape) { + const auto entry = inspectEntry(program.str()); + ASSERT_TRUE(entry) << name.str() << ": QIR entry inspection"; + ASSERT_EQ(entry->resultTypes.size(), 1) << name.str() << ": QIR main result"; + EXPECT_EQ(entry->resultTypes.front(), "i64") + << name.str() << ": QIR main status type"; + if (!sourceResultTypes.empty()) { + EXPECT_FALSE(entry->outputRecordings.empty()) + << name.str() << ": QIR output recording"; + } + if (name == "broadcast-custom-gate") { + const std::vector expected = + outputShape == OutputRecordingShape::Scalars + ? std::vector(4, QIR_RECORD_OUTPUT) + : std::vector(2, QIR_ARRAY_RECORD_OUTPUT); + EXPECT_EQ(entry->outputRecordings, expected) + << name.str() << ": QIR multi-output recording order"; + } + auto llvmIR = program.llvmIR(); + ASSERT_TRUE(llvmIR) << name.str() << ": LLVM IR translation"; + EXPECT_FALSE(llvmIR->empty()) << name.str() << ": LLVM IR is empty"; + auto bitcode = program.toBitcode(); + ASSERT_TRUE(bitcode) << name.str() << ": bitcode translation"; + ASSERT_GE(bitcode->size(), 4) << name.str() << ": bitcode header"; + EXPECT_EQ(std::to_integer((*bitcode)[0]), 0x42U); + EXPECT_EQ(std::to_integer((*bitcode)[1]), 0x43U); + EXPECT_EQ(std::to_integer((*bitcode)[2]), 0xC0U); + EXPECT_EQ(std::to_integer((*bitcode)[3]), 0xDEU); +} + +} // namespace + +TEST_P(OpenQASMCompilerPipelineTest, TraversesTheExplicitStandardPipeline) { + const auto& source = GetParam(); + std::optional restoredQC; + std::vector resultTypes; + ASSERT_TRUE(throughOptimizedQCO(source, restoredQC, resultTypes)); + auto qir = std::move(*restoredQC).intoQIR(QIRProfile::Adaptive); + ASSERT_TRUE(qir) << source.name.str() << ": QC to Adaptive QIR"; + expectQIRArtifacts(*qir, source.name, resultTypes, + OutputRecordingShape::Arrays); +} + +TEST_P(OpenQASMCompilerPipelineTest, TraversesTheDefaultAdaptivePipeline) { + const auto& source = GetParam(); + auto input = QCProgram::fromQASMString(source.source.str()); + ASSERT_TRUE(input) << source.name.str() << ": OpenQASM to QC"; + const auto inputEntry = inspectEntry(input->str()); + ASSERT_TRUE(inputEntry) << source.name.str() << ": inspect QC entry"; + auto output = runDefaultPipeline(CompilerInput{std::move(*input)}, + ProgramFormat::QIRAdaptive); + ASSERT_TRUE(output) << source.name.str() << ": default Adaptive pipeline"; + auto* qir = std::get_if(&*output); + ASSERT_NE(qir, nullptr) << source.name.str() << ": default output format"; + expectQIRArtifacts(*qir, source.name, inputEntry->resultTypes, + OutputRecordingShape::Arrays); +} + +class OpenQASMBasePipelineTest + : public testing::TestWithParam {}; + +class OpenQASMJeffPipelineTest + : public testing::TestWithParam {}; + +TEST_P(OpenQASMJeffPipelineTest, TraversesTheExplicitJeffRoundTrip) { + const auto& source = GetParam(); + std::optional restoredQC; + std::vector resultTypes; + ASSERT_TRUE(roundTripThroughOptimizedJeff(source, restoredQC, resultTypes)); + auto qir = std::move(*restoredQC).intoQIR(QIRProfile::Adaptive); + ASSERT_TRUE(qir) << source.name.str() << ": QC to Adaptive QIR"; + expectQIRArtifacts(*qir, source.name, resultTypes, + OutputRecordingShape::Scalars); +} + +class OpenQASMJeffBoundaryTest + : public testing::TestWithParam {}; + +TEST_P(OpenQASMJeffBoundaryTest, FailsAtQCOToJeff) { + const auto& source = GetParam(); + auto qc = QCProgram::fromQASMString(source.source.str()); + ASSERT_TRUE(qc) << source.name.str() << ": OpenQASM to QC"; + auto qco = std::move(*qc).intoQCO(); + ASSERT_TRUE(qco) << source.name.str() << ": QC to QCO"; + ASSERT_TRUE(qco->cleanup()) << source.name.str() << ": QCO cleanup"; + ASSERT_TRUE(qco->runPassPipeline("mqt-qco-default")) + << source.name.str() << ": QCO optimization"; + ASSERT_TRUE(qco->cleanup()) << source.name.str() << ": optimized QCO cleanup"; + EXPECT_FALSE(std::move(*qco).intoJeff()) + << source.name.str() << ": unexpectedly converted to jeff"; +} + +TEST_P(OpenQASMBasePipelineTest, ReachesBaseAndAdaptiveQIR) { + const auto& source = GetParam(); + std::optional restoredQC; + std::vector resultTypes; + ASSERT_TRUE(throughOptimizedQCO(source, restoredQC, resultTypes)); + for (const auto profile : {QIRProfile::Base, QIRProfile::Adaptive}) { + auto input = restoredQC->copy(); + auto qir = std::move(input).intoQIR(profile); + ASSERT_TRUE(qir) << source.name.str() << ": QC to QIR"; + expectQIRArtifacts(*qir, source.name, resultTypes, + profile == QIRProfile::Base + ? OutputRecordingShape::Scalars + : OutputRecordingShape::Arrays); + } +} + +INSTANTIATE_TEST_SUITE_P(OpenQASMPrograms, OpenQASMCompilerPipelineTest, + testing::ValuesIn(qasm::standardPipelinePrograms()), + openQASMProgramName); + +INSTANTIATE_TEST_SUITE_P(OpenQASMPrograms, OpenQASMBasePipelineTest, + testing::ValuesIn(qasm::baseProfilePrograms()), + openQASMProgramName); + +INSTANTIATE_TEST_SUITE_P(OpenQASMPrograms, OpenQASMJeffPipelineTest, + testing::ValuesIn(qasm::jeffCompatiblePrograms()), + openQASMProgramName); + +INSTANTIATE_TEST_SUITE_P(OpenQASMPrograms, OpenQASMJeffBoundaryTest, + testing::ValuesIn(qasm::jeffIncompatiblePrograms()), + openQASMProgramName); + /** * @brief Test: typed programs import MLIR and OpenQASM from their public APIs */ @@ -314,7 +630,7 @@ h q; } /** - * @brief Test: Jeff programs round-trip through their binary APIs + * @brief Test: jeff programs round-trip through their binary APIs */ TEST_F(CompilerPipelineTest, JeffProgramsRoundTripThroughBytesAndFiles) { const std::string qasm = R"(OPENQASM 3.0; diff --git a/mlir/unittests/Conversion/JeffRoundTrip/test_jeff_round_trip.cpp b/mlir/unittests/Conversion/JeffRoundTrip/test_jeff_round_trip.cpp index 6e89c3933c..4260fd7f71 100644 --- a/mlir/unittests/Conversion/JeffRoundTrip/test_jeff_round_trip.cpp +++ b/mlir/unittests/Conversion/JeffRoundTrip/test_jeff_round_trip.cpp @@ -157,6 +157,27 @@ static LogicalResult convertJeffToQCO(ModuleOp module) { return pm.run(module); } +TEST(JeffRoundTripRegressionTest, RestoresEntryPointWithObservableResults) { + DialectRegistry registry; + registry.insert(); + MLIRContext context(registry); + context.loadAllAvailableDialects(); + auto program = mqt::test::buildMLIRProgram( + &context, MQT_NAMED_BUILDER(qco::singleMeasurementToSingleBit)); + ASSERT_TRUE(program); + ASSERT_TRUE(succeeded(convertQCOToJeff(*program))); + ASSERT_TRUE(succeeded(convertJeffToQCO(*program))); + auto main = program->lookupSymbol("main"); + ASSERT_TRUE(main); + auto passthrough = main->getAttrOfType("passthrough"); + ASSERT_TRUE(passthrough); + EXPECT_TRUE(llvm::is_contained(passthrough, + StringAttr::get(&context, "entry_point"))); + ASSERT_EQ(main.getFunctionType().getNumResults(), 1); + EXPECT_TRUE(main.getFunctionType().getResult(0).isInteger(1)); +} + TEST_P(JeffRoundTripTest, ProgramEquivalence) { const auto& [nameStr, programBuilder, referenceBuilder] = GetParam(); const auto name = " (" + nameStr + ")"; diff --git a/mlir/unittests/Conversion/QCToQCO/test_qc_to_qco.cpp b/mlir/unittests/Conversion/QCToQCO/test_qc_to_qco.cpp index 0b78783834..566cd58eb6 100644 --- a/mlir/unittests/Conversion/QCToQCO/test_qc_to_qco.cpp +++ b/mlir/unittests/Conversion/QCToQCO/test_qc_to_qco.cpp @@ -14,6 +14,7 @@ #include "mlir/Dialect/QC/IR/QCDialect.h" #include "mlir/Dialect/QCO/Builder/QCOProgramBuilder.h" #include "mlir/Dialect/QCO/IR/QCODialect.h" +#include "mlir/Dialect/QCO/IR/QCOOps.h" #include "mlir/Dialect/QTensor/IR/QTensorDialect.h" #include "mlir/Support/IRVerification.h" #include "mlir/Support/Passes.h" @@ -27,12 +28,15 @@ #include #include #include +#include #include +#include #include #include #include #include +#include #include #include @@ -81,6 +85,226 @@ static LogicalResult runQCToQCOConversion(ModuleOp module) { return pm.run(module); } +namespace { + +class QCToQCORegressionTest : public testing::Test { +protected: + MLIRContext context; + + QCToQCORegressionTest() { + DialectRegistry registry; + registry.insert(); + context.appendDialectRegistry(registry); + context.loadAllAvailableDialects(); + } + + void expectNoQCOperations(ModuleOp module) { + bool retainsQCOperations = false; + module.walk([&](Operation* operation) { + retainsQCOperations |= + operation->getDialect() == context.getLoadedDialect(); + }); + EXPECT_FALSE(retainsQCOperations); + } +}; + +} // namespace + +TEST_F(QCToQCORegressionTest, ConvertsCompleteMixedSCFQuantumState) { + + constexpr llvm::StringLiteral source = R"mlir( +module { + func.func @main() attributes {passthrough = ["entry_point"]} { + %qco = qco.alloc : !qco.qubit + %qc = qc.alloc : !qc.qubit + %c0 = arith.constant 0 : index + %c1 = arith.constant 1 : index + %loop = scf.for %i = %c0 to %c1 step %c1 + iter_args(%qco_arg = %qco) -> (!qco.qubit) { + qc.h %qc : !qc.qubit + scf.yield %qco_arg : !qco.qubit + } + qco.sink %loop : !qco.qubit + qc.dealloc %qc : !qc.qubit + return + } +} +)mlir"; + + auto module = parseSourceString(source, &context); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(verify(*module))); + ASSERT_TRUE(succeeded(runQCToQCOConversion(*module))); + ASSERT_TRUE(succeeded(verify(*module))); + + bool sawMixedLoop = false; + module->walk([&](scf::ForOp loop) { + sawMixedLoop = true; + EXPECT_EQ(loop.getNumResults(), 2); + auto yield = cast(loop.getBody()->getTerminator()); + EXPECT_EQ(yield.getNumOperands(), loop.getNumResults()); + EXPECT_TRUE(llvm::equal(yield.getOperandTypes(), loop.getResultTypes())); + }); + EXPECT_TRUE(sawMixedLoop); + + expectNoQCOperations(*module); +} + +TEST_F(QCToQCORegressionTest, PreservesResultBearingIfValues) { + constexpr llvm::StringLiteral source = R"mlir( +module { + func.func @main() -> i1 attributes {passthrough = ["entry_point"]} { + %qc = qc.alloc : !qc.qubit + %condition = arith.constant true + %result = scf.if %condition -> i1 { + qc.h %qc : !qc.qubit + %value = arith.constant false + scf.yield %value : i1 + } else { + qc.x %qc : !qc.qubit + %value = arith.constant true + scf.yield %value : i1 + } + qc.dealloc %qc : !qc.qubit + return %result : i1 + } +} +)mlir"; + + auto module = parseSourceString(source, &context); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(verify(*module))); + ASSERT_TRUE(succeeded(runQCToQCOConversion(*module))); + ASSERT_TRUE(succeeded(verify(*module))); + auto main = module->lookupSymbol("main"); + ASSERT_TRUE(main); + ASSERT_EQ(main.getFunctionType().getNumResults(), 1); + EXPECT_TRUE(main.getFunctionType().getResult(0).isInteger(1)); + expectNoQCOperations(*module); + qco::IfOp branch; + module->walk([&](qco::IfOp candidate) { branch = candidate; }); + ASSERT_TRUE(branch); + const auto storedResult = [](Region& region) -> std::optional { + for (auto store : region.front().getOps()) { + APInt value; + if (matchPattern(store.getValue(), m_ConstantInt(&value))) { + return value; + } + } + return std::nullopt; + }; + const auto thenResult = storedResult(branch.getThenRegion()); + const auto elseResult = storedResult(branch.getElseRegion()); + ASSERT_TRUE(thenResult); + ASSERT_TRUE(elseResult); + EXPECT_TRUE(thenResult->isZero()); + EXPECT_TRUE(elseResult->isOne()); +} + +TEST_F(QCToQCORegressionTest, PreservesWhileConditionArgumentsAndOrdering) { + constexpr llvm::StringLiteral source = R"mlir( +module { + func.func @main() -> i1 attributes {passthrough = ["entry_point"]} { + %qco = qco.alloc : !qco.qubit + %qc = qc.alloc : !qc.qubit + %true = arith.constant true + %result:2 = scf.while (%flag = %true, %qco_arg = %qco) + : (i1, !qco.qubit) -> (i1, !qco.qubit) { + qc.h %qc : !qc.qubit + %false = arith.constant false + scf.condition(%flag) %false, %qco_arg : i1, !qco.qubit + } do { + ^bb0(%flag: i1, %qco_arg: !qco.qubit): + qc.x %qc : !qc.qubit + scf.yield %flag, %qco_arg : i1, !qco.qubit + } + qco.sink %result#1 : !qco.qubit + qc.dealloc %qc : !qc.qubit + return %result#0 : i1 + } +} +)mlir"; + + auto module = parseSourceString(source, &context); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(verify(*module))); + ASSERT_TRUE(succeeded(runQCToQCOConversion(*module))); + ASSERT_TRUE(succeeded(verify(*module))); + bool sawWhile = false; + module->walk([&](scf::WhileOp loop) { + sawWhile = true; + ASSERT_EQ(loop.getNumResults(), 3); + EXPECT_TRUE(loop.getResult(0).getType().isInteger(1)); + EXPECT_TRUE(isa(loop.getResult(1).getType())); + EXPECT_TRUE(isa(loop.getResult(2).getType())); + auto condition = + cast(loop.getBeforeBody()->getTerminator()); + EXPECT_TRUE( + llvm::equal(condition.getArgs().getTypes(), loop.getResultTypes())); + auto yield = cast(loop.getAfterBody()->getTerminator()); + EXPECT_TRUE(llvm::equal(yield.getOperandTypes(), loop.getResultTypes())); + }); + EXPECT_TRUE(sawWhile); + expectNoQCOperations(*module); + ASSERT_TRUE(succeeded(runQCOCleanupPipeline(*module))); + auto main = module->lookupSymbol("main"); + ASSERT_TRUE(main); + auto returnOp = cast(main.getBody().front().getTerminator()); + APInt result; + ASSERT_TRUE(matchPattern(returnOp.getOperand(0), m_ConstantInt(&result))); + EXPECT_TRUE(result.isZero()); +} + +TEST_F(QCToQCORegressionTest, ConvertsNestedResultsAfterRegionContents) { + constexpr llvm::StringLiteral source = R"mlir( +module { + func.func @main() -> i1 attributes {passthrough = ["entry_point"]} { + %qco = qco.alloc : !qco.qubit + %qc = qc.alloc : !qc.qubit + %c0 = arith.constant 0 : index + %c1 = arith.constant 1 : index + %true = arith.constant true + %loop:2 = scf.for %i = %c0 to %c1 step %c1 + iter_args(%flag = %true, %qco_arg = %qco) -> (i1, !qco.qubit) { + %next = scf.if %flag -> i1 { + qc.h %qc : !qc.qubit + %value = arith.constant false + scf.yield %value : i1 + } else { + qc.x %qc : !qc.qubit + %value = arith.constant true + scf.yield %value : i1 + } + qc.z %qc : !qc.qubit + scf.yield %next, %qco_arg : i1, !qco.qubit + } + qco.sink %loop#1 : !qco.qubit + qc.dealloc %qc : !qc.qubit + return %loop#0 : i1 + } +} +)mlir"; + + auto module = parseSourceString(source, &context); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(verify(*module))); + ASSERT_TRUE(succeeded(runQCToQCOConversion(*module))); + ASSERT_TRUE(succeeded(verify(*module))); + bool sawLoop = false; + module->walk([&](scf::ForOp loop) { + sawLoop = true; + ASSERT_EQ(loop.getNumResults(), 3); + auto yield = cast(loop.getBody()->getTerminator()); + EXPECT_TRUE(llvm::equal(yield.getOperandTypes(), loop.getResultTypes())); + EXPECT_TRUE(llvm::none_of(loop.getBody()->getOps(), + [](auto) { return true; })); + }); + EXPECT_TRUE(sawLoop); + expectNoQCOperations(*module); +} + TEST_P(QCToQCOTest, ProgramEquivalence) { const auto& [_, programBuilder, referenceBuilder] = GetParam(); const auto name = " (" + GetParam().name + ")"; diff --git a/mlir/unittests/Conversion/QCToQIR/QCToQIRAdaptive/test_qc_to_qir_adaptive.cpp b/mlir/unittests/Conversion/QCToQIR/QCToQIRAdaptive/test_qc_to_qir_adaptive.cpp index e386a4e579..dce8c78d73 100644 --- a/mlir/unittests/Conversion/QCToQIR/QCToQIRAdaptive/test_qc_to_qir_adaptive.cpp +++ b/mlir/unittests/Conversion/QCToQIR/QCToQIRAdaptive/test_qc_to_qir_adaptive.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -32,6 +33,7 @@ #include #include +#include #include #include #include @@ -83,6 +85,25 @@ static LogicalResult runQCToQIRAdaptiveConversion(ModuleOp module) { return pm.run(module); } +TEST(QCToQIRAdaptiveNativeTest, LowersControlFlowAssertions) { + MLIRContext context; + context + .loadDialect(); + qc::QCProgramBuilder builder(&context); + builder.initialize(); + cf::AssertOp::create(builder, builder.boolConstant(true), + "runtime precondition"); + auto module = builder.finalize(); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(verify(*module))); + ASSERT_TRUE(succeeded(runQCToQIRAdaptiveConversion(*module))); + EXPECT_TRUE(succeeded(verify(*module))); + std::size_t assertions = 0; + module->walk([&](cf::AssertOp) { ++assertions; }); + EXPECT_EQ(assertions, 0); +} + TEST_P(QCToQIRAdaptiveTest, ProgramEquivalence) { const auto& [_, programBuilder, referenceBuilder] = GetParam(); const auto name = " (" + GetParam().name + ")"; diff --git a/mlir/unittests/Dialect/QC/Translation/test_qasm3_translation.cpp b/mlir/unittests/Dialect/QC/Translation/test_qasm3_translation.cpp index c99059aa77..a4aa1eaec0 100644 --- a/mlir/unittests/Dialect/QC/Translation/test_qasm3_translation.cpp +++ b/mlir/unittests/Dialect/QC/Translation/test_qasm3_translation.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -28,7 +29,9 @@ #include #include +#include #include +#include #include #include @@ -59,8 +62,8 @@ class QASM3TranslationTest void SetUp() override { DialectRegistry registry; - registry.insert(); + registry.insert(); context = std::make_unique(); context->appendDialectRegistry(registry); context->loadAllAvailableDialects(); @@ -139,6 +142,180 @@ static Value ifNot(qc::QCProgramBuilder& b) { return out; } +static SmallVector broadcastRegisterAndQubit(qc::QCProgramBuilder& b) { + auto reg = b.allocQubitRegister(3); + auto scalar = b.allocQubitRegister(1); + auto q = scalar[0]; + b.cx(reg[0], q); + b.cx(reg[1], q); + b.cx(reg[2], q); + return {b.measure(reg[0]), b.measure(reg[1]), b.measure(reg[2]), + b.measure(q)}; +} + +static SmallVector broadcastCompoundGate(qc::QCProgramBuilder& b) { + auto reg = b.allocQubitRegister(3); + auto scalar = b.allocQubitRegister(1); + auto q = scalar[0]; + for (auto qubit : reg.qubits) { + b.x(qubit); + b.cx(qubit, q); + } + return {b.measure(reg[0]), b.measure(reg[1]), b.measure(reg[2]), + b.measure(q)}; +} + +static Value expressionArithmetic(qc::QCProgramBuilder& b) { + auto q = b.allocQubitRegister(1); + b.h(q[0]); + b.rx((((1.0 + 2.0) * 3.0) / 2.0) - 0.5, q[0]); + return b.measure(q[0]); +} + +static Value expressionUnaryMinus(qc::QCProgramBuilder& b) { + auto q = b.allocQubitRegister(1); + b.h(q[0]); + b.rx(-0.5, q[0]); + b.ry(-(1.0 + 2.0), q[0]); + b.rz(-(-0.25), q[0]); + return b.measure(q[0]); +} + +static Value expressionBuiltinConstants(qc::QCProgramBuilder& b) { + auto q = b.allocQubitRegister(1); + b.h(q[0]); + b.rx(std::numbers::pi / 2.0, q[0]); + b.ry((2.0 * std::numbers::pi) / 4.0, q[0]); + b.rz(std::numbers::e, q[0]); + return b.measure(q[0]); +} + +static Value expressionMathFunctions(qc::QCProgramBuilder& b) { + auto q = b.allocQubitRegister(1); + b.h(q[0]); + b.rx(std::acos(0.5), q[0]); + b.rx(std::asin(0.5), q[0]); + b.rx(std::atan(0.5), q[0]); + b.rx(std::cos(0.5), q[0]); + b.rx(std::exp(0.5), q[0]); + b.rx(std::numbers::ln2, q[0]); + b.rx(std::fmod(5.5, 2.0), q[0]); + b.rx(std::pow(2.0, 3.0), q[0]); + b.rx(std::sin(0.5), q[0]); + b.rx(std::numbers::sqrt2, q[0]); + b.rx(std::tan(0.5), q[0]); + return b.measure(q[0]); +} + +static Value expressionNestedMathFunctions(qc::QCProgramBuilder& b) { + auto q = b.allocQubitRegister(1); + b.h(q[0]); + b.rx(std::sqrt(std::pow(std::sin(0.5), 2.0) + std::pow(std::cos(0.5), 2.0)), + q[0]); + return b.measure(q[0]); +} + +static Value expressionConstFloat(qc::QCProgramBuilder& b) { + constexpr double theta = std::numbers::pi / 4.0; + auto q = b.allocQubitRegister(1); + b.h(q[0]); + b.rx(theta, q[0]); + b.ry(theta * 2.0, q[0]); + return b.measure(q[0]); +} + +static Value expressionMutableFloat(qc::QCProgramBuilder& b) { + auto q = b.allocQubitRegister(1); + b.h(q[0]); + b.rx(0.5, q[0]); + b.ry(0.75, q[0]); + return b.measure(q[0]); +} + +static SmallVector +expressionConstIntArithmetic(qc::QCProgramBuilder& b) { + auto q = b.allocQubitRegister(8); + b.h(q[3]); + b.h(q[5]); + b.rx(8.0, q[3]); + return {b.measure(q[3]), b.measure(q[5])}; +} + +static SmallVector conditionLiteral(qc::QCProgramBuilder& b) { + auto q = b.allocQubitRegister(2); + b.h(q[0]); + b.scfIf(true, [&] { b.x(q[0]); }); + b.scfIf(false, [&] { b.x(q[1]); }); + return {b.measure(q[0]), b.measure(q[1])}; +} + +static Value conditionMeasurement(qc::QCProgramBuilder& b) { + auto q = b.allocQubitRegister(2); + b.h(q[0]); + auto condition = b.measure(q[0]); + b.scfIf(condition, [&] { b.x(q[1]); }); + return b.measure(q[1]); +} + +static SmallVector conditionAnd(qc::QCProgramBuilder& b) { + auto q = b.allocQubitRegister(3); + b.h(q[0]); + b.h(q[1]); + auto c0 = b.measure(q[0]); + auto c1 = b.measure(q[1]); + auto condition = arith::AndIOp::create(b, c0, c1); + b.scfIf(condition, [&] { b.x(q[2]); }); + return {c0, c1, b.measure(q[2])}; +} + +static SmallVector conditionOr(qc::QCProgramBuilder& b) { + auto q = b.allocQubitRegister(3); + b.h(q[0]); + b.h(q[1]); + auto c0 = b.measure(q[0]); + auto c1 = b.measure(q[1]); + auto condition = arith::OrIOp::create(b, c0, c1); + b.scfIf(condition, [&] { b.x(q[2]); }, [&] { b.h(q[2]); }); + return {c0, c1, b.measure(q[2])}; +} + +static SmallVector conditionNotAndOr(qc::QCProgramBuilder& b) { + auto q = b.allocQubitRegister(4); + b.h(q[0]); + b.h(q[1]); + b.h(q[2]); + auto c0 = b.measure(q[0]); + auto c1 = b.measure(q[1]); + auto c2 = b.measure(q[2]); + auto both = arith::AndIOp::create(b, c0, c1); + auto notBoth = arith::XOrIOp::create(b, both, b.boolConstant(true)); + auto condition = arith::OrIOp::create(b, notBoth, c2); + b.scfIf(condition, [&] { b.x(q[3]); }); + return {c0, c1, c2, b.measure(q[3])}; +} + +static SmallVector conditionBoolVariable(qc::QCProgramBuilder& b) { + auto q = b.allocQubitRegister(3); + b.h(q[0]); + b.h(q[1]); + auto c0 = b.measure(q[0]); + auto c1 = b.measure(q[1]); + auto both = arith::AndIOp::create(b, c0, c1); + auto neither = arith::XOrIOp::create(b, both, b.boolConstant(true)); + b.scfIf(neither, [&] { b.x(q[2]); }); + return {c0, c1, b.measure(q[2])}; +} + +static SmallVector conditionIndexedBit(qc::QCProgramBuilder& b) { + auto q = b.allocQubitRegister(3); + b.h(q[0]); + b.h(q[1]); + auto c0 = b.measure(q[0]); + auto c1 = b.measure(q[1]); + b.scfIf(c1, [&] { b.x(q[2]); }); + return {c0, c1, b.measure(q[2])}; +} + TEST_P(QASM3TranslationTest, ProgramEquivalence) { const auto name = " (" + GetParam().name + ")"; const auto& source = GetParam().source; @@ -441,4 +618,52 @@ INSTANTIATE_TEST_SUITE_P( QASM3TranslationTestCase{"IfEmptyThen", qasm::ifEmptyThen, MQT_NAMED_BUILDER(ifNot)}, QASM3TranslationTestCase{"IfElse", qasm::ifElse, - MQT_NAMED_BUILDER(qc::ifElse)})); + MQT_NAMED_BUILDER(qc::ifElse)}, + QASM3TranslationTestCase{"BroadcastRegisterAndQubit", + qasm::broadcastRegisterAndQubit, + MQT_NAMED_BUILDER(broadcastRegisterAndQubit)}, + QASM3TranslationTestCase{"BroadcastCompoundGate", + qasm::broadcastCompoundGate, + MQT_NAMED_BUILDER(broadcastCompoundGate)}, + QASM3TranslationTestCase{"ExpressionArithmetic", + qasm::expressionArithmetic, + MQT_NAMED_BUILDER(expressionArithmetic)}, + QASM3TranslationTestCase{"ExpressionUnaryMinus", + qasm::expressionUnaryMinus, + MQT_NAMED_BUILDER(expressionUnaryMinus)}, + QASM3TranslationTestCase{"ExpressionBuiltinConstants", + qasm::expressionBuiltinConstants, + MQT_NAMED_BUILDER(expressionBuiltinConstants)}, + QASM3TranslationTestCase{"ExpressionMathFunctions", + qasm::expressionMathFunctions, + MQT_NAMED_BUILDER(expressionMathFunctions)}, + QASM3TranslationTestCase{ + "ExpressionNestedMathFunctions", + qasm::expressionNestedMathFunctions, + MQT_NAMED_BUILDER(expressionNestedMathFunctions)}, + QASM3TranslationTestCase{"ExpressionConstFloat", + qasm::expressionConstFloat, + MQT_NAMED_BUILDER(expressionConstFloat)}, + QASM3TranslationTestCase{"ExpressionMutableFloat", + qasm::expressionMutableFloat, + MQT_NAMED_BUILDER(expressionMutableFloat)}, + QASM3TranslationTestCase{ + "ExpressionConstIntArithmetic", qasm::expressionConstIntArithmetic, + MQT_NAMED_BUILDER(expressionConstIntArithmetic)}, + QASM3TranslationTestCase{"ConditionLiteral", qasm::conditionLiteral, + MQT_NAMED_BUILDER(conditionLiteral)}, + QASM3TranslationTestCase{"ConditionMeasurement", + qasm::conditionMeasurement, + MQT_NAMED_BUILDER(conditionMeasurement)}, + QASM3TranslationTestCase{"ConditionAnd", qasm::conditionAnd, + MQT_NAMED_BUILDER(conditionAnd)}, + QASM3TranslationTestCase{"ConditionOr", qasm::conditionOr, + MQT_NAMED_BUILDER(conditionOr)}, + QASM3TranslationTestCase{"ConditionNotAndOr", qasm::conditionNotAndOr, + MQT_NAMED_BUILDER(conditionNotAndOr)}, + QASM3TranslationTestCase{"ConditionBoolVariable", + qasm::conditionBoolVariable, + MQT_NAMED_BUILDER(conditionBoolVariable)}, + QASM3TranslationTestCase{"ConditionIndexedBit", + qasm::conditionIndexedBit, + MQT_NAMED_BUILDER(conditionIndexedBit)})); diff --git a/mlir/unittests/Target/CMakeLists.txt b/mlir/unittests/Target/CMakeLists.txt new file mode 100644 index 0000000000..7cecfeee4b --- /dev/null +++ b/mlir/unittests/Target/CMakeLists.txt @@ -0,0 +1,9 @@ +# Copyright (c) 2023 - 2026 Chair for Design Automation, TUM +# Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH +# All rights reserved. +# +# SPDX-License-Identifier: MIT +# +# Licensed under the MIT License + +add_subdirectory(OpenQASM) diff --git a/mlir/unittests/Target/OpenQASM/CMakeLists.txt b/mlir/unittests/Target/OpenQASM/CMakeLists.txt new file mode 100644 index 0000000000..94e96cfa12 --- /dev/null +++ b/mlir/unittests/Target/OpenQASM/CMakeLists.txt @@ -0,0 +1,21 @@ +# Copyright (c) 2023 - 2026 Chair for Design Automation, TUM +# Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH +# All rights reserved. +# +# SPDX-License-Identifier: MIT +# +# Licensed under the MIT License + +set(target_name mqt-core-mlir-unittest-openqasm-target) +add_executable(${target_name} test_openqasm.cpp) +target_link_libraries( + ${target_name} + PRIVATE MLIROpenQASMFrontend + MLIRQCToQCO + MLIRQCTranslation + MLIRQASMPrograms + MLIRQCDialect + MLIRSupportMQT + GTest::gtest_main) +mqt_mlir_configure_unittest_target(${target_name}) +gtest_discover_tests(${target_name} PROPERTIES LABELS mqt-mlir-unittests DISCOVERY_TIMEOUT 60) diff --git a/mlir/unittests/Target/OpenQASM/test_openqasm.cpp b/mlir/unittests/Target/OpenQASM/test_openqasm.cpp new file mode 100644 index 0000000000..794198e5f9 --- /dev/null +++ b/mlir/unittests/Target/OpenQASM/test_openqasm.cpp @@ -0,0 +1,2533 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#include "mlir/Conversion/QCToQCO/QCToQCO.h" +#include "mlir/Dialect/QC/IR/QCOps.h" +#include "mlir/Dialect/QC/Translation/TranslateQASM3ToQC.h" +#include "mlir/Target/OpenQASM/Frontend.h" +#include "qasm_programs.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +using namespace mlir; + +namespace { + +constexpr llvm::StringLiteral BROADCAST_PROGRAM = R"qasm( +OPENQASM 3.0; +include "stdgates.inc"; +qubit[2] q; +h q; +bit[2] c = measure q; +)qasm"; + +TEST(OpenQASMFrontendTest, SemanticAnalysisIsIndependentOfMLIR) { + auto parsed = oq3::frontend::parseOpenQASM(BROADCAST_PROGRAM); + ASSERT_TRUE(parsed) << parsed.diagnostics.front().message; + + auto analyzed = oq3::frontend::analyzeOpenQASM(*parsed.program); + ASSERT_TRUE(analyzed) << analyzed.diagnostics.front().message; + ASSERT_EQ(analyzed.program->registers.size(), 2); + EXPECT_EQ(analyzed.program->body.size(), 5); + EXPECT_EQ(analyzed.program->outputs.size(), 1); +} + +TEST(OpenQASMFrontendTest, TreatsOpenQASM30AsTheOpenQASM3Mode) { + constexpr llvm::StringLiteral V31 = R"qasm( +OPENQASM 3.1; +include "stdgates.inc"; +qubit q; +x q; +)qasm"; + EXPECT_TRUE(oq3::frontend::analyzeOpenQASM(BROADCAST_PROGRAM)); + EXPECT_TRUE(oq3::frontend::analyzeOpenQASM(V31)); +} + +TEST(OpenQASMFrontendTest, PreservesExactAndOptionalVersionSemantics) { + EXPECT_TRUE(oq3::frontend::analyzeOpenQASM("qubit q; x q;")); + EXPECT_TRUE(oq3::frontend::analyzeOpenQASM("OPENQASM 3; qubit q; x q;")); + + auto unsupported = + oq3::frontend::analyzeOpenQASM("OPENQASM 3.10; qubit q; x q;"); + ASSERT_FALSE(unsupported); + ASSERT_FALSE(unsupported.diagnostics.empty()); + EXPECT_NE(unsupported.diagnostics.front().message.find("3.10"), + std::string::npos); +} + +TEST(OpenQASMFrontendTest, RejectsUnsupportedOpenQASM3MinorVersions) { + constexpr llvm::StringLiteral SOURCE = R"qasm( +OPENQASM 3.2; +qubit q; +x q; +)qasm"; + auto analyzed = oq3::frontend::analyzeOpenQASM(SOURCE); + ASSERT_FALSE(analyzed); + ASSERT_FALSE(analyzed.diagnostics.empty()); + EXPECT_NE(analyzed.diagnostics.front().message.find("Unsupported OpenQASM"), + std::string::npos); +} + +TEST(OpenQASMFrontendTest, RejectsUnsupportedIntegerDeclarations) { + constexpr llvm::StringLiteral SOURCE = R"qasm( +OPENQASM 3.1; +int[32] counter; +)qasm"; + auto analyzed = oq3::frontend::analyzeOpenQASM(SOURCE); + ASSERT_FALSE(analyzed); + ASSERT_FALSE(analyzed.diagnostics.empty()); + EXPECT_NE(analyzed.diagnostics.front().message.find("Integer declarations"), + std::string::npos); +} + +TEST(OpenQASMFrontendTest, RejectsTooFewVariadicControlOperands) { + constexpr llvm::StringLiteral SOURCE = R"qasm( +OPENQASM 3.1; +qubit q; +mcx q; +)qasm"; + auto analyzed = oq3::frontend::analyzeOpenQASM(SOURCE); + ASSERT_FALSE(analyzed); + ASSERT_FALSE(analyzed.diagnostics.empty()); + EXPECT_NE(analyzed.diagnostics.front().message.find("qubit operands"), + std::string::npos); +} + +TEST(OpenQASMFrontendTest, RejectsDuplicateGateQubits) { + constexpr llvm::StringLiteral SOURCE = R"qasm( +OPENQASM 3.1; +qubit q; +cx q, q; +)qasm"; + auto analyzed = oq3::frontend::analyzeOpenQASM(SOURCE); + ASSERT_FALSE(analyzed); + ASSERT_FALSE(analyzed.diagnostics.empty()); + EXPECT_NE(analyzed.diagnostics.front().message.find("same qubit"), + std::string::npos); +} + +TEST(OpenQASMFrontendTest, CompatibilityGatePolicyIsExplicit) { + constexpr llvm::StringLiteral SOURCE = R"qasm( +OPENQASM 3.0; +qubit[2] q; +cu3(0.1, 0.2, 0.3) q[0], q[1]; +)qasm"; + EXPECT_TRUE(oq3::frontend::analyzeOpenQASM(SOURCE)); + + oq3::frontend::FrontendOptions strict; + strict.gatePolicy = oq3::frontend::GatePolicy::Strict; + auto analyzed = oq3::frontend::analyzeOpenQASM(SOURCE, strict); + ASSERT_FALSE(analyzed); + ASSERT_FALSE(analyzed.diagnostics.empty()); + EXPECT_NE(analyzed.diagnostics.front().message.find("No OpenQASM definition"), + std::string::npos); +} + +TEST(OpenQASMFrontendTest, StrictPolicyRequiresTheStandardLibraryInclude) { + constexpr llvm::StringLiteral WITHOUT_INCLUDE = R"qasm( +OPENQASM 3.0; +qubit q; +x q; +)qasm"; + constexpr llvm::StringLiteral WITH_INCLUDE = R"qasm( +OPENQASM 3.0; +include "stdgates.inc"; +qubit q; +x q; +)qasm"; + oq3::frontend::FrontendOptions strict; + strict.gatePolicy = oq3::frontend::GatePolicy::Strict; + + EXPECT_FALSE(oq3::frontend::analyzeOpenQASM(WITHOUT_INCLUDE, strict)); + EXPECT_TRUE(oq3::frontend::analyzeOpenQASM(WITH_INCLUDE, strict)); + EXPECT_TRUE(oq3::frontend::analyzeOpenQASM(WITHOUT_INCLUDE)); +} + +TEST(OpenQASMFrontendTest, StrictPolicyAllowsUserDefinedGateNames) { + constexpr llvm::StringLiteral SOURCE = R"qasm( +OPENQASM 3.0; +gate x q { + U(0, 0, 0) q; +} +qubit q; +x q; +)qasm"; + oq3::frontend::FrontendOptions strict; + strict.gatePolicy = oq3::frontend::GatePolicy::Strict; + EXPECT_TRUE(oq3::frontend::analyzeOpenQASM(SOURCE, strict)); +} + +TEST(OpenQASMFrontendTest, RequiresGateDefinitionScopes) { + constexpr llvm::StringLiteral SOURCE = R"qasm( +OPENQASM 3.1; +gate unbraced q x q; +)qasm"; + auto parsed = oq3::frontend::parseOpenQASM(SOURCE); + ASSERT_FALSE(parsed); + ASSERT_FALSE(parsed.diagnostics.empty()); + EXPECT_NE(parsed.diagnostics.front().message.find("expected '{'"), + std::string::npos); +} + +TEST(OpenQASMFrontendTest, AcceptsTrailingGateIdentifierCommas) { + constexpr llvm::StringLiteral SOURCE = R"qasm( +OPENQASM 3.1; +include "stdgates.inc"; +gate trailing(theta,) control, target, { + ctrl @ rx(theta) control, target; +} +qubit[2] q; +trailing(0.5) q[0], q[1]; +)qasm"; + auto analyzed = oq3::frontend::analyzeOpenQASM(SOURCE); + ASSERT_TRUE(analyzed) << analyzed.diagnostics.front().message; +} + +TEST(OpenQASMFrontendTest, PreservesSourceNamesInSemanticDiagnostics) { + llvm::SourceMgr sourceMgr; + sourceMgr.AddNewSourceBuffer( + llvm::MemoryBuffer::getMemBufferCopy( + "OPENQASM 3.0;\nqubit q;\nunknown q;\n", "fixture.qasm"), + llvm::SMLoc()); + auto analyzed = oq3::frontend::analyzeOpenQASM(sourceMgr); + ASSERT_FALSE(analyzed); + ASSERT_FALSE(analyzed.diagnostics.empty()); + EXPECT_EQ(analyzed.diagnostics.front().location.filename, "fixture.qasm"); + EXPECT_EQ(analyzed.diagnostics.front().location.line, 3); +} + +TEST(OpenQASMFrontendTest, LocatesVersionAndOutputDiagnosticsPrecisely) { + llvm::SourceMgr versionSources; + versionSources.AddNewSourceBuffer( + llvm::MemoryBuffer::getMemBufferCopy("OPENQASM 3.2;\nqubit q;\n", + "unsupported-version.qasm"), + llvm::SMLoc()); + auto version = oq3::frontend::analyzeOpenQASM(versionSources); + ASSERT_FALSE(version); + ASSERT_FALSE(version.diagnostics.empty()); + EXPECT_EQ(version.diagnostics.front().location.filename, + "unsupported-version.qasm"); + EXPECT_EQ(version.diagnostics.front().location.line, 1); + + llvm::SourceMgr outputSources; + outputSources.AddNewSourceBuffer( + llvm::MemoryBuffer::getMemBufferCopy( + "OPENQASM 3.1;\nqubit q;\noutput bit result;\n", + "incomplete-output.qasm"), + llvm::SMLoc()); + auto output = oq3::frontend::analyzeOpenQASM(outputSources); + ASSERT_FALSE(output); + ASSERT_FALSE(output.diagnostics.empty()); + EXPECT_EQ(output.diagnostics.front().location.filename, + "incomplete-output.qasm"); + EXPECT_EQ(output.diagnostics.front().location.line, 3); +} + +TEST(OpenQASMTargetTest, EmitsVerifiedQCDirectly) { + MLIRContext context; + auto module = qc::translateQASM3ToQC(BROADCAST_PROGRAM, &context); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(verify(*module))); + + std::size_t gates = 0; + module->walk([&](qc::HOp) { ++gates; }); + EXPECT_EQ(gates, 2); +} + +TEST(OpenQASMTargetTest, ProductionTranslationUsesTheStagedPipeline) { + MLIRContext context; + auto module = qc::translateQASM3ToQC(BROADCAST_PROGRAM, &context); + ASSERT_TRUE(module); + EXPECT_TRUE(succeeded(verify(*module))); + + bool hasQuantumOperation = false; + module->walk([&](Operation* operation) { + hasQuantumOperation |= isa(operation); + }); + EXPECT_TRUE(hasQuantumOperation); +} + +TEST(OpenQASMTargetTest, EmitsTypedMixedNumericGateExpressions) { + constexpr llvm::StringLiteral SOURCE = R"qasm( +OPENQASM 3.0; +include "stdgates.inc"; +gate shifted(theta) q { + rx(theta + 1) q; +} +qubit q; +shifted(0.5) q; +)qasm"; + + MLIRContext context; + auto module = qc::translateQASM3ToQC(SOURCE, &context); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(verify(*module))); + + std::size_t numericCasts = 0; + module->walk([&](Operation* operation) { + numericCasts += isa(operation); + }); + EXPECT_EQ(numericCasts, 1); +} + +TEST(OpenQASMTargetTest, EmitsScalarMathFunctions) { + constexpr llvm::StringLiteral SOURCE = R"qasm( +OPENQASM 3.0; +include "stdgates.inc"; +gate shaped(theta) q { + rx(sin(theta) + cos(theta) + tan(theta) + exp(theta) + ln(theta) + + sqrt(theta)) q; +} + +qubit q; +shaped(0.5) q; +)qasm"; + + MLIRContext context; + auto module = qc::translateQASM3ToQC(SOURCE, &context); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(verify(*module))); + + std::size_t functions = 0; + module->walk([&](Operation* operation) { + functions += isa(operation); + }); + EXPECT_EQ(functions, 6); +} + +TEST(OpenQASMTargetTest, NestsAlternatingControlsAndFlipsPolarityOutside) { + constexpr llvm::StringLiteral SOURCE = R"qasm( +OPENQASM 3.0; +include "stdgates.inc"; +qubit[5] q; +ctrl(2) @ negctrl @ inv @ ctrl @ x q[0], q[1], q[2], q[3], q[4]; +)qasm"; + + MLIRContext context; + auto module = qc::translateQASM3ToQC(SOURCE, &context); + ASSERT_TRUE(module); + + PassManager manager(&context); + ASSERT_TRUE(succeeded(manager.run(*module))); + ASSERT_TRUE(succeeded(verify(*module))); + + std::size_t controls = 0; + std::size_t outerPolarityFlips = 0; + module->walk([&](Operation* operation) { + if (auto control = dyn_cast(operation)) { + ++controls; + EXPECT_EQ(control.getNumControls(), 1); + } + if (isa(operation) && + operation->getParentOfType() == nullptr && + operation->getParentOfType() == nullptr) { + ++outerPolarityFlips; + } + }); + EXPECT_EQ(controls, 4); + EXPECT_EQ(outerPolarityFlips, 2); +} + +TEST(OpenQASMTargetTest, RejectsPowerAtTheQCTargetBoundary) { + constexpr llvm::StringLiteral SOURCE = R"qasm( +OPENQASM 3.0; +include "stdgates.inc"; +gate powered(exponent) q { + pow(exponent) @ x q; +} +qubit q; +powered(0.5) q; +)qasm"; + + MLIRContext context; + testing::internal::CaptureStderr(); + auto module = qc::translateQASM3ToQC(SOURCE, &context); + const auto diagnostic = testing::internal::GetCapturedStderr(); + EXPECT_FALSE(module); + EXPECT_NE(diagnostic.find("power gate modifiers are not supported"), + std::string::npos); + EXPECT_NE(diagnostic.find(":5:"), std::string::npos); +} + +TEST(OpenQASMTargetTest, + LowersCustomGatesConditionalsAndQuantumRuntimeOperations) { + constexpr llvm::StringLiteral SOURCE = R"qasm( +OPENQASM 3.1; +include "stdgates.inc"; +gate pair(theta) left, right { + rx(theta) left; + cx left, right; +} +qubit[2] q; +bit c = measure q[0]; +if (!c) { + pair(0.5) q[0], q[1]; +} else { + reset q[1]; +} +barrier q; +output bit[2] out = measure q; +)qasm"; + + MLIRContext context; + auto module = qc::translateQASM3ToQC(SOURCE, &context); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(verify(*module))); + + std::size_t conditionals = 0; + module->walk([&](Operation* operation) { + conditionals += operation->getName().getStringRef() == "scf.if"; + }); + EXPECT_EQ(conditionals, 1); + + PassManager manager(&context); + ASSERT_TRUE(succeeded(manager.run(*module))); + ASSERT_TRUE(succeeded(verify(*module))); + + std::size_t resets = 0; + std::size_t barriers = 0; + module->walk([&](Operation* operation) { + auto name = operation->getName().getStringRef(); + resets += name == "qc.reset"; + barriers += name == "qc.barrier"; + }); + EXPECT_EQ(resets, 1); + EXPECT_EQ(barriers, 1); +} + +TEST(OpenQASMTargetTest, LowersOpenQASM2ControlledGateCompatibilityPrefixes) { + constexpr llvm::StringLiteral SOURCE = R"qasm( +OPENQASM 2.0; +include "qelib1.inc"; +qreg q[4]; +creg c[4]; +cccx q[0], q[1], q[2], q[3]; +measure q -> c; +)qasm"; + + MLIRContext context; + auto module = qc::translateQASM3ToQC(SOURCE, &context); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(verify(*module))); + + std::size_t controls = 0; + module->walk( + [&](Operation* operation) { controls += isa(operation); }); + EXPECT_EQ(controls, 3); +} + +TEST(OpenQASMTargetTest, LowersLanguageBuiltinsOnHardwareQubits) { + constexpr llvm::StringLiteral SOURCE = R"qasm( +OPENQASM 3.1; +gphase(pi / 2); +x $3; +)qasm"; + + MLIRContext context; + auto module = qc::translateQASM3ToQC(SOURCE, &context); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(verify(*module))); + + std::size_t globalPhases = 0; + std::size_t xGates = 0; + module->walk([&](Operation* operation) { + globalPhases += isa(operation); + xGates += isa(operation); + }); + EXPECT_EQ(globalPhases, 1); + EXPECT_EQ(xGates, 1); +} + +TEST(OpenQASMFrontendTest, RejectsUninitializedOutputsAndInvalidConditions) { + constexpr llvm::StringLiteral UNMEASURED_OUTPUT = R"qasm( +OPENQASM 3.1; +qubit q; +output bit result; +)qasm"; + constexpr llvm::StringLiteral UNMEASURED_CONDITION = R"qasm( +OPENQASM 3.1; +qubit q; +bit c; +if (c) { x q; } +)qasm"; + + auto uninitializedOutput = oq3::frontend::analyzeOpenQASM(UNMEASURED_OUTPUT); + ASSERT_FALSE(uninitializedOutput); + ASSERT_FALSE(uninitializedOutput.diagnostics.empty()); + EXPECT_NE(uninitializedOutput.diagnostics.front().message.find( + "not fully initialized"), + std::string::npos); + + auto uninitializedCondition = + oq3::frontend::analyzeOpenQASM(UNMEASURED_CONDITION); + ASSERT_FALSE(uninitializedCondition); + ASSERT_FALSE(uninitializedCondition.diagnostics.empty()); + EXPECT_NE(uninitializedCondition.diagnostics.front().message.find( + "has not been initialized"), + std::string::npos); +} + +TEST(OpenQASMFrontendTest, RejectsInvalidGateControlAndBroadcastShapes) { + constexpr llvm::StringLiteral ZERO_CONTROL = R"qasm( +OPENQASM 3.1; +qubit[2] q; +ctrl(0) @ x q[0], q[1]; +)qasm"; + constexpr llvm::StringLiteral MISMATCHED_BROADCAST = R"qasm( +OPENQASM 3.1; +qubit[2] q; +qubit[3] r; +cx q, r; +)qasm"; + constexpr llvm::StringLiteral OVERFLOWING_CONTROL_COUNT = R"qasm( +OPENQASM 3.1; +qubit q; +ctrl(9223372036854775807) @ ctrl(9223372036854775807) @ ctrl(2) @ x q; +)qasm"; + constexpr llvm::StringLiteral EXCESSIVE_DYNAMIC_DISPATCH = R"qasm( +OPENQASM 3.1; +qubit[16] q; +int a = 0; +int b = 1; +int c = 2; +int d = 3; +mcx q[a], q[b], q[c], q[d]; +)qasm"; + + auto zeroControl = oq3::frontend::analyzeOpenQASM(ZERO_CONTROL); + ASSERT_FALSE(zeroControl); + ASSERT_FALSE(zeroControl.diagnostics.empty()); + EXPECT_NE(zeroControl.diagnostics.front().message.find("must be positive"), + std::string::npos); + + auto mismatchedBroadcast = + oq3::frontend::analyzeOpenQASM(MISMATCHED_BROADCAST); + ASSERT_FALSE(mismatchedBroadcast); + ASSERT_FALSE(mismatchedBroadcast.diagnostics.empty()); + EXPECT_NE(mismatchedBroadcast.diagnostics.front().message.find("same width"), + std::string::npos); + + auto overflowingControlCount = + oq3::frontend::analyzeOpenQASM(OVERFLOWING_CONTROL_COUNT); + ASSERT_FALSE(overflowingControlCount); + ASSERT_FALSE(overflowingControlCount.diagnostics.empty()); + EXPECT_NE(overflowingControlCount.diagnostics.front().message.find( + "Invalid number of qubit operands"), + std::string::npos); + + auto excessiveDispatch = + oq3::frontend::analyzeOpenQASM(EXCESSIVE_DYNAMIC_DISPATCH); + EXPECT_TRUE(excessiveDispatch); +} + +TEST(OpenQASMFrontendTest, TracksLexicalScopeAndEnclosingAssignments) { + constexpr llvm::StringLiteral SOURCE = R"qasm( +OPENQASM 3.1; +int value = 1; +if (true) { + int value = 2; + value += 3; +} else { + value = 4; +} +value += 5; +)qasm"; + + auto analyzed = oq3::frontend::analyzeOpenQASM(SOURCE); + ASSERT_TRUE(analyzed) << analyzed.diagnostics.front().message; + ASSERT_EQ(analyzed.program->scalars.size(), 2); + + std::size_t outerAssignments = 0; + std::size_t innerAssignments = 0; + for (const auto& statement : analyzed.program->statements) { + if (const auto* assignment = + std::get_if( + &statement.data)) { + outerAssignments += assignment->scalar == 0; + innerAssignments += assignment->scalar == 1; + } + } + EXPECT_EQ(outerAssignments, 2); + EXPECT_EQ(innerAssignments, 1); +} + +TEST(OpenQASMTargetTest, RejectsExcessiveDynamicDispatch) { + constexpr llvm::StringLiteral SOURCE = R"qasm( +OPENQASM 3.1; +include "stdgates.inc"; +qubit[317] q; +qubit[317] aux; +int i = 0; + int j = 1; +cx q[i], aux[j]; +)qasm"; + + MLIRContext context; + testing::internal::CaptureStderr(); + auto module = qc::translateQASM3ToQC(SOURCE, &context); + const auto diagnostic = testing::internal::GetCapturedStderr(); + EXPECT_FALSE(module); + EXPECT_NE(diagnostic.find(":8:1"), std::string::npos); + EXPECT_NE(diagnostic.find("projected emitted operation count"), + std::string::npos); +} + +TEST(OpenQASMTargetTest, RejectsExcessiveCustomGateExpansion) { + std::string source = "OPENQASM 3.1;\n" + "include \"stdgates.inc\";\n" + "gate g0 q { x q; }\n"; + for (std::size_t level = 1; level <= 17; ++level) { + source += "gate g" + std::to_string(level) + " q { g" + + std::to_string(level - 1) + " q; g" + std::to_string(level - 1) + + " q; }\n"; + } + source += "qubit q;\ng17 q;\n"; + + MLIRContext context; + testing::internal::CaptureStderr(); + auto module = qc::translateQASM3ToQC(source, &context); + const auto diagnostic = testing::internal::GetCapturedStderr(); + EXPECT_FALSE(module); + EXPECT_NE(diagnostic.find(":"), std::string::npos); + EXPECT_NE(diagnostic.find("projected emitted operation count"), + std::string::npos); +} + +TEST(OpenQASMTargetTest, ComposesDispatchAndCustomGateExpansionBudgets) { + std::string source = "OPENQASM 3.1;\n" + "include \"stdgates.inc\";\n" + "gate expanded a, b {\n"; + for (std::size_t operation = 0; operation < 25; ++operation) { + source += operation % 2 == 0 ? " x a;\n" : " x b;\n"; + } + source += "}\n" + "qubit[64] q;\n" + "qubit[64] aux;\n" + "int i = 0;\n" + "int j = 1;\n" + "expanded q[i], aux[j];\n"; + + MLIRContext context; + testing::internal::CaptureStderr(); + auto module = qc::translateQASM3ToQC(source, &context); + const auto diagnostic = testing::internal::GetCapturedStderr(); + EXPECT_FALSE(module); + EXPECT_NE(diagnostic.find(":"), std::string::npos); + EXPECT_NE(diagnostic.find("projected emitted operation count"), + std::string::npos); +} + +TEST(OpenQASMTargetTest, LowersGateBodyLoopsAndBuiltinConstants) { + constexpr llvm::StringLiteral SOURCE = R"qasm( +OPENQASM 3.1; +include "stdgates.inc"; +gate repeated(theta) q { + for int i in [0:2] { rx(theta + pi + i) q; } + while (false) { x q; } +} +qubit q; +repeated(0.5) q; +bit result = measure q; +)qasm"; + + MLIRContext context; + auto module = qc::translateQASM3ToQC(SOURCE, &context); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(verify(*module))); + std::size_t forLoops = 0; + std::size_t whileLoops = 0; + module->walk([&](Operation* operation) { + forLoops += isa(operation); + whileLoops += isa(operation); + }); + EXPECT_EQ(forLoops, 1); + EXPECT_EQ(whileLoops, 1); + + EXPECT_TRUE(succeeded(verify(*module))); +} + +TEST(OpenQASMFrontendTest, RejectsMutableGlobalCapturesInGateBodies) { + constexpr std::pair fixtures[] = { + {"mutable-capture", + "OPENQASM 3.1; float theta = 0.5; gate g q { rx(theta) q; }"}, + {"declaration", "OPENQASM 3.1; gate g q { int i = 0; }"}, + {"measurement", "OPENQASM 3.1; bit c; gate g q { measure q -> c; }"}, + {"reset", "OPENQASM 3.1; gate g q { reset q; }"}, + {"conditional", "OPENQASM 3.1; gate g q { if (true) { x q; } }"}, + }; + for (const auto& [name, source] : fixtures) { + SCOPED_TRACE(name.str()); + auto parsed = oq3::frontend::parseOpenQASM(source); + ASSERT_TRUE(parsed) << parsed.diagnostics.front().message; + auto analyzed = oq3::frontend::analyzeOpenQASM(*parsed.program); + ASSERT_FALSE(analyzed); + ASSERT_FALSE(analyzed.diagnostics.empty()); + } +} + +TEST(OpenQASMTargetTest, GateDefinitionsCaptureGlobalConstants) { + constexpr llvm::StringLiteral SOURCE = R"qasm( +OPENQASM 3.1; +include "stdgates.inc"; +const float theta = pi / 2; +gate g q { rx(theta) q; } +qubit q; +g q; +)qasm"; + + MLIRContext context; + auto module = qc::translateQASM3ToQC(SOURCE, &context); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(verify(*module))); + qc::RXOp rotation; + module->walk([&](qc::RXOp application) { rotation = application; }); + ASSERT_TRUE(rotation); + FloatAttr angle; + EXPECT_TRUE(matchPattern(rotation.getParameter(0), m_Constant(&angle))); + EXPECT_DOUBLE_EQ(angle.getValueAsDouble(), std::numbers::pi / 2); +} + +TEST(OpenQASMTargetTest, SupportsWholeBitRegisterAssignment) { + constexpr llvm::StringLiteral SOURCE = R"qasm( +OPENQASM 3.1; +include "stdgates.inc"; +qubit[2] q; +bit[2] source = measure q; +bit[2] target = measure q; +target = source; +if (target[0] || target[1]) { x q[0]; } +)qasm"; + + auto analyzed = oq3::frontend::analyzeOpenQASM(SOURCE); + ASSERT_TRUE(analyzed) << analyzed.diagnostics.front().message; + std::size_t assignments = 0; + for (const auto& statement : analyzed.program->statements) { + assignments += + std::holds_alternative( + statement.data); + } + EXPECT_EQ(assignments, 2); + + MLIRContext context; + auto module = qc::translateQASM3ToQC(SOURCE, &context); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(verify(*module))); +} + +TEST(OpenQASMTargetTest, SupportsOpenQASM2RegisterConditions) { + constexpr llvm::StringLiteral SOURCE = R"qasm( +OPENQASM 2.0; +include "qelib1.inc"; +qreg q[1]; +creg c[1]; +measure q -> c; +if (c == 1) x q[0]; +)qasm"; + + MLIRContext context; + auto module = qc::translateQASM3ToQC(SOURCE, &context); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(verify(*module))); + std::size_t conditionals = 0; + module->walk([&](scf::IfOp) { ++conditionals; }); + EXPECT_EQ(conditionals, 1); +} + +TEST(OpenQASMTargetTest, SelectsFloatingPowForNegativeSignedExponent) { + constexpr llvm::StringLiteral SOURCE = R"qasm( +OPENQASM 3.1; +int base = 4; +float result = pow(base, -2); +qubit q; +if (result == 0.0625) { x q; } +)qasm"; + + MLIRContext context; + auto module = qc::translateQASM3ToQC(SOURCE, &context); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(verify(*module))); + bool foundResult = false; + module->walk([&](arith::ConstantFloatOp constant) { + foundResult |= constant.value().convertToDouble() == 0.0625; + }); + EXPECT_TRUE(foundResult); +} + +TEST(OpenQASMTargetTest, SupportsScalarMeasurementReassignment) { + constexpr llvm::StringLiteral SOURCE = R"qasm( +OPENQASM 3.1; +qubit q; +bool measured; +measured = measure q; +if (measured) { x q; } +measured = measure q; +if (!measured) { h q; } +)qasm"; + MLIRContext context; + auto module = qc::translateQASM3ToQC(SOURCE, &context); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(verify(*module))); + std::size_t measurements = 0; + module->walk([&](qc::MeasureOp) { ++measurements; }); + EXPECT_EQ(measurements, 2); +} + +TEST(OpenQASMFrontendTest, InvalidatesDynamicBitFactsOnIndexChanges) { + constexpr llvm::StringLiteral SOURCE = R"qasm( +OPENQASM 3.1; +qubit[2] q; +bit[2] c; +int i = 0; +c[i] = measure q[i]; +i = 1; +if (c[i]) { x q[i]; } +)qasm"; + auto analyzed = oq3::frontend::analyzeOpenQASM(SOURCE); + ASSERT_FALSE(analyzed); + ASSERT_FALSE(analyzed.diagnostics.empty()); + EXPECT_NE(analyzed.diagnostics.front().message.find("uninitialized bit"), + std::string::npos); +} + +TEST(OpenQASMFrontendTest, RejectsAConstantZeroRangeStep) { + constexpr llvm::StringLiteral SOURCE = R"qasm( +OPENQASM 3.1; +for int i in [0:0:3] {} +)qasm"; + auto analyzed = oq3::frontend::analyzeOpenQASM(SOURCE); + ASSERT_FALSE(analyzed); + ASSERT_FALSE(analyzed.diagnostics.empty()); + EXPECT_NE(analyzed.diagnostics.front().message.find("must not be zero"), + std::string::npos); +} + +TEST(OpenQASMFrontendTest, OwnsAndAnalyzesProvidedIncludeBuffers) { + oq3::frontend::ParseResult parsed; + { + llvm::SourceMgr sourceMgr; + sourceMgr.AddNewSourceBuffer( + llvm::MemoryBuffer::getMemBufferCopy(R"qasm( +OPENQASM 3.1; +include "custom.inc"; +qubit q; +custom q; +bit result = measure q; +)qasm", + "main.qasm"), + llvm::SMLoc()); + sourceMgr.AddNewSourceBuffer( + llvm::MemoryBuffer::getMemBufferCopy(R"qasm( +gate custom q { x q; } +)qasm", + "custom.inc"), + llvm::SMLoc()); + parsed = oq3::frontend::parseOpenQASM(sourceMgr); + } + + ASSERT_TRUE(parsed) << parsed.diagnostics.front().message; + auto analyzed = oq3::frontend::analyzeOpenQASM(*parsed.program); + ASSERT_TRUE(analyzed) << analyzed.diagnostics.front().message; + ASSERT_EQ(analyzed.program->gates.size(), 1); + EXPECT_EQ(analyzed.program->gates.front().name, "custom"); + EXPECT_EQ(analyzed.program->gates.front().location.filename, "custom.inc"); +} + +TEST(OpenQASMFrontendTest, ExpandsNestedIncludesAtTheirSourceLocations) { + llvm::SourceMgr sourceMgr; + sourceMgr.AddNewSourceBuffer( + llvm::MemoryBuffer::getMemBufferCopy(R"qasm( +OPENQASM 3.1; +include "outer.inc"; +int result = outer + nested; +)qasm", + "main.qasm"), + llvm::SMLoc()); + sourceMgr.AddNewSourceBuffer( + llvm::MemoryBuffer::getMemBufferCopy(R"qasm( +int outer = 1; +include "nested.inc"; +int after = nested; +)qasm", + "outer.inc"), + llvm::SMLoc()); + sourceMgr.AddNewSourceBuffer( + llvm::MemoryBuffer::getMemBufferCopy("int nested = 2;\n", "nested.inc"), + llvm::SMLoc()); + + auto analyzed = oq3::frontend::analyzeOpenQASM(sourceMgr); + ASSERT_TRUE(analyzed) << analyzed.diagnostics.front().message; + ASSERT_EQ(analyzed.program->scalars.size(), 4); + EXPECT_EQ(analyzed.program->scalars[0].name, "outer"); + EXPECT_EQ(analyzed.program->scalars[1].name, "nested"); + EXPECT_EQ(analyzed.program->scalars[2].name, "after"); + EXPECT_EQ(analyzed.program->scalars[3].name, "result"); +} + +TEST(OpenQASMFrontendTest, RejectsRecursiveIncludesResolvedThroughSearchPaths) { + auto fileSystem = llvm::makeIntrusiveRefCnt(); + ASSERT_TRUE(fileSystem->addFile( + "/includes/recursive.inc", 0, + llvm::MemoryBuffer::getMemBuffer("include \"recursive.inc\";"))); + + llvm::SourceMgr sourceMgr; + sourceMgr.setVirtualFileSystem(fileSystem); + sourceMgr.setIncludeDirs({"/includes"}); + sourceMgr.AddNewSourceBuffer( + llvm::MemoryBuffer::getMemBufferCopy( + "OPENQASM 3.1; include \"recursive.inc\";", "main.qasm"), + llvm::SMLoc()); + + auto parsed = oq3::frontend::parseOpenQASM(sourceMgr); + ASSERT_FALSE(parsed); + ASSERT_FALSE(parsed.diagnostics.empty()); + EXPECT_NE(parsed.diagnostics.front().message.find("recursive include"), + std::string::npos); +} + +TEST(OpenQASMFrontendTest, LimitsIncludeNesting) { + llvm::SourceMgr sourceMgr; + sourceMgr.AddNewSourceBuffer( + llvm::MemoryBuffer::getMemBufferCopy( + "OPENQASM 3.1; include \"depth-0.inc\";", "main.qasm"), + llvm::SMLoc()); + for (std::size_t index = 0; index <= 64; ++index) { + std::string source; + if (index == 64) { + source = "int leaf = 1;"; + } else { + source = "include \"depth-" + std::to_string(index + 1) + ".inc\";"; + } + sourceMgr.AddNewSourceBuffer( + llvm::MemoryBuffer::getMemBufferCopy( + source, "depth-" + std::to_string(index) + ".inc"), + llvm::SMLoc()); + } + + auto parsed = oq3::frontend::parseOpenQASM(sourceMgr); + ASSERT_FALSE(parsed); + ASSERT_FALSE(parsed.diagnostics.empty()); + EXPECT_NE(parsed.diagnostics.front().message.find("include nesting"), + std::string::npos); +} + +TEST(OpenQASMFrontendTest, LimitsTextualIncludeExpansion) { + llvm::SourceMgr sourceMgr; + sourceMgr.AddNewSourceBuffer( + llvm::MemoryBuffer::getMemBufferCopy( + "OPENQASM 3.1; include \"level-0.inc\";", "main.qasm"), + llvm::SMLoc()); + for (std::size_t index = 0; index < 18; ++index) { + std::string source; + if (index == 17) { + source = "int leaf = 1;"; + } else { + const auto next = "level-" + std::to_string(index + 1) + ".inc"; + source = "include \"" + next + "\"; include \"" + next + "\";"; + } + sourceMgr.AddNewSourceBuffer( + llvm::MemoryBuffer::getMemBufferCopy( + source, "level-" + std::to_string(index) + ".inc"), + llvm::SMLoc()); + } + + auto parsed = oq3::frontend::parseOpenQASM(sourceMgr); + ASSERT_FALSE(parsed); + ASSERT_FALSE(parsed.diagnostics.empty()); + EXPECT_NE(parsed.diagnostics.front().message.find("statement limit"), + std::string::npos); +} + +TEST(OpenQASMFrontendTest, EnforcesUnicodeIdentifierCategoriesAndUtf8) { + EXPECT_TRUE(oq3::frontend::analyzeOpenQASM( + "OPENQASM 3.1; int θ = 1; int Ångström = θ;")); + + auto symbol = oq3::frontend::analyzeOpenQASM("OPENQASM 3.1; int 💥 = 1;"); + ASSERT_FALSE(symbol); + ASSERT_FALSE(symbol.diagnostics.empty()); + + std::string invalid = "OPENQASM 3.1; int "; + invalid.push_back(static_cast(0xC3)); + invalid += " = 1;"; + auto malformed = oq3::frontend::analyzeOpenQASM(invalid); + ASSERT_FALSE(malformed); + ASSERT_FALSE(malformed.diagnostics.empty()); +} + +TEST(OpenQASMFrontendTest, RejectsShadowingBuiltInConstants) { + constexpr llvm::StringLiteral SOURCES[] = { + "OPENQASM 3.1; int pi = 0;", + "OPENQASM 3.1; if (true) { int tau = 0; }", + "OPENQASM 3.1; gate g(euler) q { U(euler, 0, 0) q; }", + }; + for (const auto source : SOURCES) { + auto analyzed = oq3::frontend::analyzeOpenQASM(source); + ASSERT_FALSE(analyzed) << source.str(); + ASSERT_FALSE(analyzed.diagnostics.empty()); + EXPECT_NE(analyzed.diagnostics.front().message.find("already declared"), + std::string::npos); + } +} + +TEST(OpenQASMFrontendTest, PropagatesDynamicBitFactsThroughKnownControlFlow) { + constexpr llvm::StringLiteral SOURCE = R"qasm( +OPENQASM 3.1; +qubit[2] q; +bit[2] c; +int i = 1; +if (true) { c[i] = measure q[i]; } +if (c[i]) { x q[0]; } +output bit result = measure q[0]; +)qasm"; + auto analyzed = oq3::frontend::analyzeOpenQASM(SOURCE); + ASSERT_TRUE(analyzed) << analyzed.diagnostics.front().message; +} + +TEST(OpenQASMFrontendTest, ResolvesIncludedNamesWithoutBasenameAliasing) { + llvm::SourceMgr sourceMgr; + sourceMgr.AddNewSourceBuffer( + llvm::MemoryBuffer::getMemBufferCopy(R"qasm( +OPENQASM 3.1; +include "a/defs.inc"; +include "b/defs.inc"; +counter += 1; +qubit q; +if (enabled) { x q; } +bit result = measure q; +)qasm", + "main.qasm"), + llvm::SMLoc()); + sourceMgr.AddNewSourceBuffer( + llvm::MemoryBuffer::getMemBufferCopy("int counter = 0;\n", "a/defs.inc"), + llvm::SMLoc()); + sourceMgr.AddNewSourceBuffer(llvm::MemoryBuffer::getMemBufferCopy( + "bool enabled = true;\n", "b/defs.inc"), + llvm::SMLoc()); + + auto analyzed = oq3::frontend::analyzeOpenQASM(sourceMgr); + ASSERT_TRUE(analyzed) << analyzed.diagnostics.front().message; + ASSERT_EQ(analyzed.program->scalars.size(), 2); + EXPECT_EQ(analyzed.program->scalars[0].name, "counter"); + EXPECT_EQ(analyzed.program->scalars[1].name, "enabled"); +} + +TEST(OpenQASMFrontendTest, ExpandsEveryTextualIncludeOccurrence) { + llvm::SourceMgr sources; + sources.AddNewSourceBuffer(llvm::MemoryBuffer::getMemBufferCopy(R"qasm( +OPENQASM 3.1; +qubit q; +include "operations.inc"; +include "operations.inc"; +bit result = measure q; +)qasm", + "main.qasm"), + llvm::SMLoc()); + sources.AddNewSourceBuffer( + llvm::MemoryBuffer::getMemBufferCopy("x q;\n", "operations.inc"), + llvm::SMLoc()); + + auto analyzed = oq3::frontend::analyzeOpenQASM(sources); + ASSERT_TRUE(analyzed) << analyzed.diagnostics.front().message; + std::size_t applications = 0; + for (const auto& statement : analyzed.program->statements) { + applications += + std::holds_alternative(statement.data); + } + EXPECT_EQ(applications, 2); +} + +TEST(OpenQASMFrontendTest, RejectsRecursiveAndRepeatedStandardIncludes) { + llvm::SourceMgr recursiveSources; + recursiveSources.AddNewSourceBuffer( + llvm::MemoryBuffer::getMemBufferCopy("OPENQASM 3.1; include \"a.inc\";", + "main.qasm"), + llvm::SMLoc()); + recursiveSources.AddNewSourceBuffer( + llvm::MemoryBuffer::getMemBufferCopy("include \"b.inc\";", "a.inc"), + llvm::SMLoc()); + recursiveSources.AddNewSourceBuffer( + llvm::MemoryBuffer::getMemBufferCopy("include \"a.inc\";", "b.inc"), + llvm::SMLoc()); + auto recursive = oq3::frontend::parseOpenQASM(recursiveSources); + ASSERT_FALSE(recursive); + ASSERT_FALSE(recursive.diagnostics.empty()); + EXPECT_NE(recursive.diagnostics.front().message.find("recursive"), + std::string::npos); + + auto repeated = oq3::frontend::analyzeOpenQASM( + "OPENQASM 3.1; include \"stdgates.inc\"; include " + "\"stdgates.inc\";"); + ASSERT_FALSE(repeated); + ASSERT_FALSE(repeated.diagnostics.empty()); + EXPECT_NE(repeated.diagnostics.front().message.find("more than once"), + std::string::npos); +} + +TEST(OpenQASMFrontendTest, ActivatesStandardGatesSequentially) { + oq3::frontend::FrontendOptions strict; + strict.gatePolicy = oq3::frontend::GatePolicy::Strict; + auto beforeInclude = oq3::frontend::analyzeOpenQASM(R"qasm( +OPENQASM 3.1; +qubit q; +x q; +include "stdgates.inc"; +)qasm", + strict); + ASSERT_FALSE(beforeInclude); + + auto afterInclude = oq3::frontend::analyzeOpenQASM(R"qasm( +OPENQASM 3.1; +include "stdgates.inc"; +qubit q; +x q; +)qasm", + strict); + ASSERT_TRUE(afterInclude) << afterInclude.diagnostics.front().message; + + auto collision = oq3::frontend::analyzeOpenQASM(R"qasm( +OPENQASM 3.1; +gate r(theta, phi) q { U(theta, phi, 0) q; } +include "stdgates.inc"; +)qasm"); + ASSERT_FALSE(collision); + ASSERT_FALSE(collision.diagnostics.empty()); + EXPECT_NE(collision.diagnostics.front().message.find("already declared"), + std::string::npos); +} + +TEST(OpenQASMFrontendTest, RejectsIncludesInsideBlocks) { + auto parsed = oq3::frontend::parseOpenQASM( + "OPENQASM 3.1; if (true) { include \"nested.inc\"; }"); + ASSERT_FALSE(parsed); + ASSERT_FALSE(parsed.diagnostics.empty()); + EXPECT_NE(parsed.diagnostics.front().message.find("only allowed globally"), + std::string::npos); +} + +TEST(OpenQASMFrontendTest, CollectsMultipleRecoverableSyntaxDiagnostics) { + constexpr llvm::StringLiteral SOURCE = R"qasm( +OPENQASM 3.1; +qubit ; +bit ; +)qasm"; + auto parsed = oq3::frontend::parseOpenQASM(SOURCE); + ASSERT_FALSE(parsed); + EXPECT_EQ(parsed.diagnostics.size(), 2); + EXPECT_EQ(parsed.diagnostics[0].location.line, 3); + EXPECT_EQ(parsed.diagnostics[1].location.line, 4); +} + +TEST(OpenQASMFrontendTest, RejectsMisplacedVersionsAndRecursiveGates) { + constexpr llvm::StringLiteral MISPLACED_VERSION = R"qasm( +qubit q; +OPENQASM 3.1; +)qasm"; + constexpr llvm::StringLiteral RECURSIVE_GATES = R"qasm( +OPENQASM 3.1; +gate first q { first q; } +qubit q; +first q; +bit result = measure q; +)qasm"; + + auto misplaced = oq3::frontend::analyzeOpenQASM(MISPLACED_VERSION); + ASSERT_FALSE(misplaced); + ASSERT_FALSE(misplaced.diagnostics.empty()); + EXPECT_NE(misplaced.diagnostics.front().message.find("must be the first"), + std::string::npos); + + auto recursive = oq3::frontend::analyzeOpenQASM(RECURSIVE_GATES); + ASSERT_FALSE(recursive); + ASSERT_FALSE(recursive.diagnostics.empty()); + EXPECT_NE(recursive.diagnostics.front().message.find("recursive"), + std::string::npos); +} + +TEST(OpenQASMFrontendTest, DiagnosesMalformedLexicalAndGrammarFamilies) { + struct InvalidSource { + llvm::StringRef name; + llvm::StringRef source; + }; + const InvalidSource fixtures[] = { + {"unterminated-comment", "OPENQASM 3.1; /*"}, + {"unterminated-string", "OPENQASM 3.1; include \"missing.inc;"}, + {"missing-include", "OPENQASM 3.1; include \"missing.inc\";"}, + {"invalid-hardware-qubit", "OPENQASM 3.1; qubit q; x $;"}, + {"integer-overflow", + "OPENQASM 3.1; int value = 999999999999999999999999999999;"}, + {"float-overflow", "OPENQASM 3.1; float value = 1e99999;"}, + {"unsupported-angle", "OPENQASM 3.1; angle theta;"}, + {"unsupported-duration", "OPENQASM 3.1; duration delay;"}, + {"unsupported-opaque", "OPENQASM 3.1; opaque custom q;"}, + {"output-qubit", "OPENQASM 3.1; output qubit q;"}, + {"const-qubit", "OPENQASM 3.1; const qubit q;"}, + {"duplicate-version", "OPENQASM 3.1; OPENQASM 3.1;"}, + {"non-string-include", "OPENQASM 3.1; include stdgates.inc;"}, + {"gate-designator", "OPENQASM 3.1; gate custom[2] q {}"}, + {"missing-range-members", "OPENQASM 3.1; for int i in [:] {}"}, + {"missing-while-condition", "OPENQASM 3.1; while () {}"}, + {"const-without-initializer", "OPENQASM 3.1; const int value;"}, + }; + + for (const auto& fixture : fixtures) { + SCOPED_TRACE(fixture.name.str()); + auto parsed = oq3::frontend::parseOpenQASM(fixture.source); + ASSERT_FALSE(parsed); + ASSERT_FALSE(parsed.diagnostics.empty()); + EXPECT_FALSE(parsed.diagnostics.front().message.empty()); + } +} + +TEST(OpenQASMFrontendTest, RejectsUnsupportedReservedWordsAsIdentifiers) { + constexpr llvm::StringLiteral RESERVED_WORDS[] = { + "defcalgrammar", "def", "cal", "defcal", "extern", + "box", "let", "break", "continue", "end", + "return", "switch", "case", "default", "pragma", + "input", "readonly", "mutable", "complex", "array", + "void", "stretch", "durationof", "delay", "im", + "#dim", "#pragma", + }; + for (const auto keyword : RESERVED_WORDS) { + SCOPED_TRACE(keyword.str()); + const std::string source = "OPENQASM 3.1; int " + keyword.str() + " = 0;"; + auto parsed = oq3::frontend::parseOpenQASM(source); + ASSERT_FALSE(parsed); + ASSERT_FALSE(parsed.diagnostics.empty()); + EXPECT_NE(parsed.diagnostics.front().message.find("reserved keyword"), + std::string::npos); + } +} + +TEST(OpenQASMFrontendTest, DiagnosesUnsupportedReservedFeatureSyntax) { + constexpr llvm::StringLiteral SOURCES[] = { + "OPENQASM 3.1; input int value;", + "OPENQASM 3.1; const complex value = 0;", + "OPENQASM 3.1; output array[int, 2] values;", + "OPENQASM 3.1; for complex value in [0:1] {}", + "OPENQASM 3.1; int value = durationof({});", + }; + for (const auto source : SOURCES) { + SCOPED_TRACE(source.str()); + auto parsed = oq3::frontend::parseOpenQASM(source); + ASSERT_FALSE(parsed); + ASSERT_FALSE(parsed.diagnostics.empty()); + EXPECT_NE(parsed.diagnostics.front().message.find("reserved keyword"), + std::string::npos); + } +} + +TEST(OpenQASMFrontendTest, EnforcesNumericSeparatorPlacement) { + constexpr llvm::StringLiteral INVALID_LITERALS[] = { + "1e+_2", "1e-_2", "1_e2", "1._2", "1e_2", "0xA__B", "0b_1", "0o7_", + }; + for (const auto literal : INVALID_LITERALS) { + SCOPED_TRACE(literal.str()); + const std::string source = + "OPENQASM 3.1; float value = " + literal.str() + ";"; + auto parsed = oq3::frontend::parseOpenQASM(source); + ASSERT_FALSE(parsed); + ASSERT_FALSE(parsed.diagnostics.empty()); + } + + auto valid = oq3::frontend::parseOpenQASM( + "OPENQASM 3.1; int hex = 0xA_B; float value = 1_2.3_4e+5_6;"); + ASSERT_TRUE(valid) << valid.diagnostics.front().message; +} + +TEST(OpenQASMFrontendTest, SourceManagerOverloadsPreserveParseFailures) { + llvm::SourceMgr sources; + sources.AddNewSourceBuffer(llvm::MemoryBuffer::getMemBufferCopy( + "OPENQASM 3.1; qubit ;", "broken.qasm"), + llvm::SMLoc()); + + auto parsed = oq3::frontend::parseOpenQASM(sources); + ASSERT_FALSE(parsed); + ASSERT_FALSE(parsed.diagnostics.empty()); + EXPECT_EQ(parsed.diagnostics.front().location.filename, "broken.qasm"); + + auto analyzed = oq3::frontend::analyzeOpenQASM(sources); + ASSERT_FALSE(analyzed); + ASSERT_FALSE(analyzed.diagnostics.empty()); + EXPECT_EQ(analyzed.diagnostics.front().location.filename, "broken.qasm"); +} + +TEST(OpenQASMFrontendTest, FoldsTypedConstantExpressionFamilies) { + constexpr llvm::StringLiteral SOURCE = R"qasm( +OPENQASM 3.1; +const float circle = pi + tau + euler; +const bool logic = !false && true || false; +const bool bool_equal = true == true; +const bool bool_not_equal = true != false; +const bool equal = 1 == 1; +const bool not_equal = 1 != 2; +const bool less = -1 < 9223372036854775808; +const bool less_equal = 1 <= 1; +const bool greater = 2 > 1; +const bool greater_equal = 2 >= 2; +const float float_arithmetic = + (1.5 + 2.5) - (3.0 * 0.5) + (4.0 / 2.0) + mod(5.0, 2.0) + pow(2.0, 3.0); +const int integer_arithmetic = + (1 + 2) - (3 * 1) + (8 / 2) + (5 % 2) + pow(2, 3); +)qasm"; + + auto analyzed = oq3::frontend::analyzeOpenQASM(SOURCE); + ASSERT_TRUE(analyzed) << analyzed.diagnostics.front().message; + EXPECT_TRUE(analyzed.program->body.empty()); +} + +TEST(OpenQASMFrontendTest, AppliesC99SignedUnsignedConstantPromotion) { + constexpr llvm::StringLiteral SOURCE = R"qasm( +OPENQASM 3.1; +const uint maximum = 18446744073709551615; +const uint wrapped_add = maximum + 1; +const uint one = 1; +const uint wrapped_negation = -one; +const bool mixed_order = -1 < maximum; +qubit q; +rx(wrapped_add) q; +if (mixed_order) { x q; } +)qasm"; + + auto analyzed = oq3::frontend::analyzeOpenQASM(SOURCE); + ASSERT_TRUE(analyzed) << analyzed.diagnostics.front().message; + + bool sawWrappedParameter = false; + bool sawFalseCondition = false; + for (const auto statement : analyzed.program->body) { + const auto& data = analyzed.program->statements[statement].data; + if (const auto* application = + std::get_if(&data); + application != nullptr && application->callee == "rx") { + ASSERT_EQ(application->parameters.size(), 1); + const auto& parameter = + analyzed.program->expressions[application->parameters.front()]; + sawWrappedParameter = parameter.type == oq3::frontend::ScalarType::Uint && + std::get(parameter.constant) == 0; + } + if (const auto* conditional = + std::get_if(&data)) { + const auto& condition = + analyzed.program->conditions[conditional->condition]; + sawFalseCondition = + condition.kind == oq3::frontend::ConditionKind::Literal && + !condition.literal; + } + } + EXPECT_TRUE(sawWrappedParameter); + EXPECT_TRUE(sawFalseCondition); +} + +TEST(OpenQASMFrontendTest, AppliesC99ScalarAssignmentConversions) { + constexpr llvm::StringLiteral SOURCE = R"qasm( +OPENQASM 3.1; +const int truncated = 1.75; +const bool truthy = -2; +const int from_bool = true; +const uint wrapped = -1; +const int signed_wrap = 9223372036854775808; +int mutable_int = 2.5; +bool mutable_bool = 3; +float mutable_float = true; +uint mutable_uint = -1; +mutable_int = mutable_float; +mutable_bool = mutable_int; +mutable_float = mutable_uint; +mutable_uint = mutable_bool; +qubit q; +rx(truncated + from_bool + wrapped + signed_wrap + mutable_int + mutable_float) + q; +if (truthy && mutable_bool) { x q; } +)qasm"; + + auto analyzed = oq3::frontend::analyzeOpenQASM(SOURCE); + ASSERT_TRUE(analyzed) << analyzed.diagnostics.front().message; +} + +TEST(OpenQASMFrontendTest, RejectsInvalidProgramsAcrossSemanticFamilies) { + struct InvalidSource { + llvm::StringRef name; + llvm::StringRef source; + }; + const InvalidSource fixtures[] = { + {"duplicate-scalar", "OPENQASM 3.1; int value; int value;"}, + {"unknown-assignment", "OPENQASM 3.1; value = 1;"}, + {"const-assignment", "OPENQASM 3.1; const int value = 1; value = 2;"}, + {"negated-signed-minimum", + "OPENQASM 3.1; const int minimum = -9223372036854775808; const int " + "value = -minimum;"}, + {"negation-overflow", + "OPENQASM 3.1; const int value = -9223372036854775809;"}, + {"constant-bool-arithmetic", + "OPENQASM 3.1; const bool value = true + false;"}, + {"mixed-bool-comparison", "OPENQASM 3.1; const bool value = 1 < true;"}, + {"non-finite-constant-math", + "OPENQASM 3.1; const float value = sqrt(-1.0);"}, + {"float-division-by-zero", + "OPENQASM 3.1; const float value = 1.0 / 0.0;"}, + {"float-modulo-by-zero", "OPENQASM 3.1; const float value = 1.0 % 0.0;"}, + {"float-percent", "OPENQASM 3.1; const float value = 5.0 % 2.0;"}, + {"integer-division-by-zero", "OPENQASM 3.1; const int value = 1 / 0;"}, + {"integer-division-overflow", + "OPENQASM 3.1; const int value = -9223372036854775808 / -1;"}, + {"integer-modulo-by-zero", "OPENQASM 3.1; const int value = 1 % 0;"}, + {"integer-modulo-overflow", + "OPENQASM 3.1; const int value = -9223372036854775808 % -1;"}, + {"negative-integer-power", "OPENQASM 3.1; const int value = 2 ** -1;"}, + {"integer-add-overflow", + "OPENQASM 3.1; const int value = 9223372036854775807 + 1;"}, + {"integer-subtract-overflow", + "OPENQASM 3.1; const int value = -9223372036854775808 - 1;"}, + {"integer-multiply-overflow", + "OPENQASM 3.1; const int value = 9223372036854775807 * 2;"}, + {"bool-ordering", + "OPENQASM 3.1; bool left = true; bool right = false; if (left < " + "right) {}"}, + {"zero-register", "OPENQASM 3.1; qubit[0] q;"}, + {"negative-register", "OPENQASM 3.1; qubit[-1] q;"}, + {"dynamic-register-size", "OPENQASM 3.1; int size = 2; qubit[size] q;"}, + {"float-register-size", "OPENQASM 3.1; qubit[1.5] q;"}, + {"out-of-bounds-qubit", "OPENQASM 3.1; qubit[2] q; x q[2];"}, + {"float-qubit-index", "OPENQASM 3.1; qubit[2] q; x q[1.0];"}, + {"measurement-width", + "OPENQASM 3.1; qubit[2] q; bit[3] c; c = measure q;"}, + {"unknown-reset", "OPENQASM 3.1; reset missing;"}, + {"unknown-barrier", "OPENQASM 3.1; barrier missing;"}, + {"duplicate-gate-parameter", + "OPENQASM 3.1; gate custom(a, a) q { U(a, 0, 0) q; }"}, + {"duplicate-gate-qubit", "OPENQASM 3.1; gate custom q, q { cx q, q; }"}, + {"duplicate-custom-gate", + "OPENQASM 3.1; gate custom q {} gate custom q {}"}, + {"custom-gate-conflicts-with-catalog", "OPENQASM 3.1; gate x q {}"}, + {"wrong-gate-parameter-count", "OPENQASM 3.1; qubit q; rx(1, 2) q;"}, + {"wrong-gate-qubit-count", "OPENQASM 3.1; qubit q; cx q;"}, + {"negative-control-count", + "OPENQASM 3.1; qubit[2] q; ctrl(-1) @ x q[0], q[1];"}, + {"dynamic-control-count", + "OPENQASM 3.1; int n = 1; qubit[2] q; ctrl(n) @ x q[0], q[1];"}, + {"non-integer-range", "OPENQASM 3.1; for int i in [0.0:1.0] {}"}, + {"non-bool-condition", "OPENQASM 3.1; int value = 1; if (value) {}"}, + {"bool-compound-assignment", + "OPENQASM 3.1; bool value = true; value += false;"}, + {"measurement-compound-assignment", + "OPENQASM 3.1; qubit q; bit value; value += measure q;"}, + {"unsupported-bitwise-not", "OPENQASM 3.1; int value = ~1;"}, + {"unsupported-bitwise-and", "OPENQASM 3.1; int value = 1 & 2;"}, + {"unsupported-bitwise-or", "OPENQASM 3.1; int value = 1 | 2;"}, + {"unsupported-bitwise-xor", "OPENQASM 3.1; int value = 1 ^ 2;"}, + {"unsupported-shift-left", "OPENQASM 3.1; int value = 1 << 2;"}, + {"unsupported-shift-right", "OPENQASM 3.1; int value = 2 >> 1;"}, + {"uninitialized-scalar", "OPENQASM 3.1; int x; int y = x + 1;"}, + {"self-initialization", "OPENQASM 3.1; int x = x + 1;"}, + {"uninitialized-condition", "OPENQASM 3.1; bool ready; if (ready) {}"}, + {"partially-initialized-branch", + "OPENQASM 3.1; qubit q; bool choose = measure q; int x; if (choose) " + "{ x = 1; } int y = x;"}, + {"forward-gate-call", + "OPENQASM 3.1; qubit q; later q; gate later a { x a; }"}, + {"forward-gate-in-definition", + "OPENQASM 3.1; gate first q { second q; } gate second q { x q; }"}, + {"hardware-qubit-in-gate", "OPENQASM 3.1; gate invalid q { x $0; }"}, + {"negative-index-out-of-bounds", "OPENQASM 3.1; qubit[2] q; x q[-3];"}, + {"bool-gate-parameter", + "OPENQASM 3.1; bool value = true; qubit q; rx(value) q;"}, + {"local-qubit", + "OPENQASM 3.1; bool value = true; if (value) { qubit q; }"}, + {"local-output", + "OPENQASM 3.1; bool value = true; if (value) { output bit c; }"}, + }; + + for (const auto& fixture : fixtures) { + SCOPED_TRACE(fixture.name.str()); + auto parsed = oq3::frontend::parseOpenQASM(fixture.source); + ASSERT_TRUE(parsed) << parsed.diagnostics.front().message; + auto analyzed = oq3::frontend::analyzeOpenQASM(*parsed.program); + ASSERT_FALSE(analyzed); + ASSERT_FALSE(analyzed.diagnostics.empty()); + EXPECT_FALSE(analyzed.diagnostics.front().message.empty()); + } +} + +TEST(OpenQASMFrontendTest, TracksDefiniteStateAndBlockLocalBits) { + constexpr llvm::StringLiteral SOURCE = R"qasm( +OPENQASM 3.1; +qubit[2] q; +int fromTrueBranch; +if (true) { fromTrueBranch = 1; } +int fromSelectedElse; +if (false) { fromSelectedElse = 1; } else { fromSelectedElse = 2; } +bool choose = measure q[0]; +int fromBothBranches; +if (choose) { fromBothBranches = 1; } else { fromBothBranches = 2; } +int fromNonemptyLoop; +for int iteration in [0:0] { fromNonemptyLoop = iteration; } +int combined = fromTrueBranch + fromSelectedElse + fromBothBranches + + fromNonemptyLoop; +if (true) { + bit local = measure q[0]; + if (local) { x q[1]; } +} +bit branch; +if (true) { branch = measure q[0]; } +bit loop; +for int i in [0:0] { loop = measure q[i]; } +if (branch && loop && combined >= 4) { h q[1]; } +output bit[2] result = measure q; +)qasm"; + + auto analyzed = oq3::frontend::analyzeOpenQASM(SOURCE); + ASSERT_TRUE(analyzed) << analyzed.diagnostics.front().message; + ASSERT_EQ(analyzed.program->outputs.size(), 1); + EXPECT_EQ(analyzed.program->registers[analyzed.program->outputs.front()].name, + "result"); +} + +TEST(OpenQASMFrontendTest, RejectsSignedMinimumDivisionAndModuloOverflow) { + constexpr llvm::StringLiteral sources[] = { + "OPENQASM 3.1; const int minimum = -9223372036854775808; " + "const int value = minimum / -1;", + "OPENQASM 3.1; const int minimum = -9223372036854775808; " + "const int value = minimum % -1;", + }; + for (const auto source : sources) { + auto analyzed = oq3::frontend::analyzeOpenQASM(source); + ASSERT_FALSE(analyzed); + ASSERT_FALSE(analyzed.diagnostics.empty()); + EXPECT_NE(analyzed.diagnostics.front().message.find("overflows"), + std::string::npos); + } +} + +TEST(OpenQASMFrontendTest, AcceptsAllScalarOperatorsAndComparisonPredicates) { + constexpr llvm::StringLiteral SOURCE = R"qasm( +OPENQASM 3.1; +include "stdgates.inc"; +qubit[4] q; +int signedValue = 7; +uint unsignedValue = 8; +float realValue = 0.5; +bool enabled = true; +signedValue = -signedValue + 2 * 3 - 1; +signedValue /= 2; +signedValue %= 3; +signedValue = signedValue ** 2; +unsignedValue = unsignedValue + 1; +unsignedValue /= 2; +unsignedValue %= 3; +unsignedValue = unsignedValue ** 2; +realValue = -realValue + 2.0 * 3.0 - 1.0; +realValue /= 2.0; +realValue = mod(realValue, 3.0); +realValue = realValue ** 2.0; +float functions = arccos(realValue) + arcsin(realValue) + arctan(realValue) + + sin(signedValue) + cos(unsignedValue) + tan(realValue) + + exp(realValue) + log(realValue) + sqrt(realValue); +enabled = signedValue != 0 && realValue >= 0.0; +if (signedValue == 0) { x q[0]; } +if (signedValue != 0) { x q[0]; } +if (signedValue < 0) { x q[0]; } +if (signedValue <= 0) { x q[0]; } +if (signedValue > 0) { x q[0]; } +if (signedValue >= 0) { x q[0]; } +if (unsignedValue == 0) { x q[1]; } +if (unsignedValue != 0) { x q[1]; } +if (unsignedValue < 1) { x q[1]; } +if (unsignedValue <= 1) { x q[1]; } +if (unsignedValue > 1) { x q[1]; } +if (unsignedValue >= 1) { x q[1]; } +if (signedValue < unsignedValue) { x q[1]; } +if (realValue == 0.0) { x q[2]; } +if (realValue != 0.0) { x q[2]; } +if (realValue < 0.0) { x q[2]; } +if (realValue <= 0.0) { x q[2]; } +if (realValue > 0.0) { x q[2]; } +if (realValue >= 0.0) { x q[2]; } +bit[3] scratch; +scratch[0] = measure q[0]; +scratch[1] = measure q[1]; +scratch[2] = measure q[2]; +for uint i in [0:2] { + scratch[i] = measure q[i]; + if (scratch[i] || !enabled) { h q[3]; } +} +rx(functions) q[3]; +output bit[4] result = measure q; +)qasm"; + + auto analyzed = oq3::frontend::analyzeOpenQASM(SOURCE); + ASSERT_TRUE(analyzed) << analyzed.diagnostics.front().message; +} + +TEST(OpenQASMTargetTest, NormalizesNegativeIndicesAndChecksDynamicAliases) { + constexpr llvm::StringLiteral INDEX_SOURCE = R"qasm( +OPENQASM 3.1; +qubit[3] q; +x q[-1]; +bit[3] c = measure q; +if (c[-1]) { h q[-1]; } +int i = -1; +x q[i]; +c[i] = measure q[i]; +if (c[i]) { x q[0]; } +output bit[3] result = measure q; +)qasm"; + MLIRContext indexContext; + auto indexed = qc::translateQASM3ToQC(INDEX_SOURCE, &indexContext); + ASSERT_TRUE(indexed); + ASSERT_TRUE(succeeded(verify(*indexed))); + std::size_t indexSelections = 0; + indexed->walk([&](arith::SelectOp) { ++indexSelections; }); + EXPECT_GE(indexSelections, 3); + + constexpr llvm::StringLiteral ALIAS_SOURCE = R"qasm( +OPENQASM 3.1; +qubit[2] q; +int i = 0; +cx q[i], q[i]; +bit[2] result = measure q; +)qasm"; + MLIRContext aliasContext; + auto aliased = qc::translateQASM3ToQC(ALIAS_SOURCE, &aliasContext); + ASSERT_TRUE(aliased); + ASSERT_TRUE(succeeded(verify(*aliased))); + std::size_t aliasAssertions = 0; + aliased->walk([&](cf::AssertOp) { ++aliasAssertions; }); + EXPECT_GE(aliasAssertions, 3); +} + +TEST(OpenQASMTargetTest, DispatchesDynamicQubitGatesWithStructuredControlFlow) { + constexpr llvm::StringLiteral SOURCE = R"qasm( +OPENQASM 3.1; +include "stdgates.inc"; +qubit[2] q; +int i = 0; +x q[i]; +)qasm"; + + MLIRContext context; + auto module = qc::translateQASM3ToQC(SOURCE, &context); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(verify(*module))); + EXPECT_EQ(std::distance(module->getOps().begin(), + module->getOps().end()), + 0); + std::size_t dispatches = 0; + module->walk([&](scf::IfOp) { ++dispatches; }); + EXPECT_EQ(dispatches, 1); +} + +TEST(OpenQASMTargetTest, + DispatchesDynamicQubitMeasurementsWithStructuredControlFlow) { + constexpr llvm::StringLiteral SOURCE = R"qasm( +OPENQASM 3.1; +qubit[2] q; +bit c; +int i = 0; +c = measure q[i]; +)qasm"; + + MLIRContext context; + auto module = qc::translateQASM3ToQC(SOURCE, &context); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(verify(*module))); + std::size_t dispatches = 0; + module->walk([&](scf::IfOp) { ++dispatches; }); + EXPECT_EQ(dispatches, 1); +} + +TEST(OpenQASMTargetTest, SupportsOrdinaryBitInitializationAndAssignment) { + constexpr llvm::StringLiteral SOURCE = R"qasm( +OPENQASM 3.1; +include "stdgates.inc"; +qubit q; +bit enabled = false; +enabled = true; +bit[2] flags; +flags[0] = enabled; +flags[1] = !enabled; +if (flags[0] && !flags[1]) { x q; } +)qasm"; + + MLIRContext context; + auto module = qc::translateQASM3ToQC(SOURCE, &context); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(verify(*module))); + std::size_t xGates = 0; + module->walk([&](qc::XOp) { ++xGates; }); + EXPECT_EQ(xGates, 1); +} + +TEST(OpenQASMTargetTest, SupportsTargetlessMeasurements) { + constexpr llvm::StringLiteral SOURCE = R"qasm( +OPENQASM 3.1; +qubit q; +measure q; +)qasm"; + + MLIRContext context; + auto module = qc::translateQASM3ToQC(SOURCE, &context); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(verify(*module))); + std::size_t measurements = 0; + module->walk([&](qc::MeasureOp) { ++measurements; }); + EXPECT_EQ(measurements, 1); +} + +TEST(OpenQASMTargetTest, PromotesMixedRangeEndpointsBeforeIteration) { + constexpr llvm::StringLiteral SOURCE = R"qasm( +OPENQASM 3.1; +include "stdgates.inc"; +const uint start = 0; +const int stop = -1; +qubit q; +for uint i in [start:-1:stop] { x q; } +)qasm"; + + MLIRContext context; + auto module = qc::translateQASM3ToQC(SOURCE, &context); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(verify(*module))); + PassManager canonicalizer(&context); + canonicalizer.addPass(createCanonicalizerPass()); + ASSERT_TRUE(succeeded(canonicalizer.run(*module))); + std::size_t loops = 0; + std::size_t xGates = 0; + module->walk([&](Operation* operation) { + loops += isa(operation); + xGates += isa(operation); + }); + EXPECT_EQ(loops, 0); + EXPECT_EQ(xGates, 0); +} + +TEST(OpenQASMTargetTest, ThreadsGateParametersIntoWhileConditions) { + constexpr llvm::StringLiteral SOURCE = R"qasm( +OPENQASM 3.1; +include "stdgates.inc"; +gate conditional(theta) q { + while (theta > 0.0) { x q; } +} +qubit q; +conditional(0.0) q; +)qasm"; + + MLIRContext context; + auto module = qc::translateQASM3ToQC(SOURCE, &context); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(verify(*module))); + std::size_t loops = 0; + module->walk([&](scf::WhileOp) { ++loops; }); + EXPECT_EQ(loops, 1); +} + +TEST(OpenQASMTargetTest, RejectsModifiersOnStructuredCustomGatesAtQCTarget) { + constexpr llvm::StringLiteral SOURCE = R"qasm( +OPENQASM 3.1; +include "stdgates.inc"; +gate looped(theta) q { + for int i in [0:0] { p(theta) q; } +} +qubit q; +inv @ looped(pi / 2) q; +)qasm"; + + MLIRContext context; + testing::internal::CaptureStderr(); + auto module = qc::translateQASM3ToQC(SOURCE, &context); + const auto diagnostic = testing::internal::GetCapturedStderr(); + EXPECT_FALSE(module); + EXPECT_NE(diagnostic.find("structured control flow"), std::string::npos); +} + +TEST(OpenQASMTargetTest, + RejectsModifiersOnTransitivelyStructuredCustomGatesAtQCTarget) { + constexpr llvm::StringLiteral SOURCE = R"qasm( +OPENQASM 3.1; +include "stdgates.inc"; +gate looped q { for int i in [0:0] { x q; } } +gate wrapper q { looped q; } +qubit q; +inv @ wrapper q; +)qasm"; + + MLIRContext context; + testing::internal::CaptureStderr(); + auto module = qc::translateQASM3ToQC(SOURCE, &context); + const auto diagnostic = testing::internal::GetCapturedStderr(); + EXPECT_FALSE(module); + EXPECT_NE(diagnostic.find(":7:1"), std::string::npos); + EXPECT_NE(diagnostic.find("structured control flow"), std::string::npos); +} + +TEST(OpenQASMTargetTest, IgnoresUnreachableStructuredCustomGates) { + constexpr llvm::StringLiteral SOURCE = R"qasm( +OPENQASM 3.1; +include "stdgates.inc"; +gate looped q { for int i in [0:0] { x q; } } +gate wrapper q { looped q; } +qubit q; +x q; +)qasm"; + + MLIRContext context; + auto module = qc::translateQASM3ToQC(SOURCE, &context); + ASSERT_TRUE(module); + EXPECT_TRUE(succeeded(verify(*module))); +} + +TEST(OpenQASMTargetTest, LowersRuntimeDynamicIndicesWithBoundsChecks) { + constexpr llvm::StringLiteral SOURCE = R"qasm( +OPENQASM 3.1; +include "stdgates.inc"; +qubit[2] q; +int i = 0; +if (measure q[0]) { i = 1; } +x q[i]; +)qasm"; + + MLIRContext context; + auto module = qc::translateQASM3ToQC(SOURCE, &context); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(verify(*module))); + std::size_t assertions = 0; + module->walk([&](cf::AssertOp) { ++assertions; }); + EXPECT_GE(assertions, 1); +} + +TEST(OpenQASMTargetTest, LowersRuntimeIndicesAcrossStatementKinds) { + constexpr llvm::StringLiteral sources[] = { + "OPENQASM 3.1; include \"stdgates.inc\"; qubit[2] q; bit[2] c = measure " + "q; " + "int i = 0; if (measure q[0]) { i = 1; } if (c[i]) { x q[0]; }", + "OPENQASM 3.1; qubit[2] q; bit[2] c = measure q; int i = 0; " + "if (measure q[0]) { i = 1; } bool value = c[i];", + "OPENQASM 3.1; qubit[2] q; bit[2] c = measure q; int i = 0; " + "bool value = false; " + "if (measure q[0]) { i = 1; } value = c[i];", + "OPENQASM 3.1; qubit[2] q; bit[2] c = measure q; int i = 0; " + "if (measure q[0]) { i = 1; } c[i] = true;", + "OPENQASM 3.1; qubit[2] q; bit[2] c = measure q; int i = 0; " + "if (measure q[0]) { i = 1; } c[i] = measure q[0];", + "OPENQASM 3.1; qubit[2] q; int i = 0; " + "if (measure q[0]) { i = 1; } reset q[i];", + "OPENQASM 3.1; qubit[2] q; int i = 0; " + "if (measure q[0]) { i = 1; } barrier q[i];", + }; + + for (const auto source : sources) { + SCOPED_TRACE(source.str()); + MLIRContext context; + auto module = qc::translateQASM3ToQC(source, &context); + ASSERT_TRUE(module); + EXPECT_TRUE(succeeded(verify(*module))); + } +} + +TEST(OpenQASMTargetTest, LowersLoopVariantDynamicIndicesAtQCTarget) { + constexpr llvm::StringLiteral SOURCE = R"qasm( +OPENQASM 3.1; +include "stdgates.inc"; +qubit[2] q; +int i = 0; +while (measure q[0]) { x q[i]; i = 1; } +)qasm"; + + MLIRContext context; + auto module = qc::translateQASM3ToQC(SOURCE, &context); + ASSERT_TRUE(module); + EXPECT_TRUE(succeeded(verify(*module))); +} + +TEST(OpenQASMTargetTest, LowersMultiIterationInductionIndicesAtQCTarget) { + constexpr llvm::StringLiteral SOURCE = R"qasm( +OPENQASM 3.1; +include "stdgates.inc"; +qubit[4] q; +for uint i in [0:2] { int x = i + 1; h q[x]; } +)qasm"; + + MLIRContext context; + auto module = qc::translateQASM3ToQC(SOURCE, &context); + ASSERT_TRUE(module); + EXPECT_TRUE(succeeded(verify(*module))); +} + +TEST(OpenQASMTargetTest, LowersCheckedIntegerArithmeticAtQCTarget) { + constexpr llvm::StringLiteral sources[] = { + R"qasm( +OPENQASM 3.1; +include "stdgates.inc"; +qubit q; +int turns = 0; +for int i in [0:2] { turns += 1; } +rx(turns) q; +)qasm", + "OPENQASM 3.1; int value = 1; int derived = value + 1;", + "OPENQASM 3.1; include \"stdgates.inc\"; qubit q; int value = 1; " + "if (value + 1 > 0) { x q; }", + "OPENQASM 3.1; include \"stdgates.inc\"; qubit q; int value = 1; " + "rx(value + 1) q;", + "OPENQASM 3.1; int value = 1; bool result = value + 1 > 0;", + "OPENQASM 3.1; int value = 1; bit result; result = value + 1 > 0;", + }; + + for (const auto source : sources) { + SCOPED_TRACE(source.str()); + MLIRContext context; + auto module = qc::translateQASM3ToQC(source, &context); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(verify(*module))); + std::size_t assertions = 0; + module->walk([&](cf::AssertOp) { ++assertions; }); + EXPECT_GE(assertions, 1); + } +} + +TEST(OpenQASMTargetTest, LowersRuntimeSignedAndUnsignedIntegerOperators) { + constexpr llvm::StringLiteral SOURCE = R"qasm( +OPENQASM 3.1; +int signedValue = 7; +int signedOperand = 2; +signedValue = -signedValue; +signedValue = signedValue + signedOperand; +signedValue = signedValue - signedOperand; +signedValue = signedValue * signedOperand; +signedValue = signedValue / signedOperand; +signedValue = signedValue % signedOperand; +signedValue = signedValue ** signedOperand; +uint unsignedValue = 7; +uint unsignedOperand = 2; +unsignedValue = -unsignedValue; +unsignedValue = unsignedValue + unsignedOperand; +unsignedValue = unsignedValue - unsignedOperand; +unsignedValue = unsignedValue * unsignedOperand; +unsignedValue = unsignedValue / unsignedOperand; +unsignedValue = unsignedValue % unsignedOperand; +unsignedValue = unsignedValue ** unsignedOperand; +)qasm"; + + MLIRContext context; + auto module = qc::translateQASM3ToQC(SOURCE, &context); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(verify(*module))); + std::size_t assertions = 0; + std::size_t powerLoops = 0; + module->walk([&](cf::AssertOp) { ++assertions; }); + module->walk([&](scf::WhileOp) { ++powerLoops; }); + EXPECT_GE(assertions, 7); + EXPECT_EQ(powerLoops, 2); +} + +TEST(OpenQASMTargetTest, UsesConstantBoundsForStaticInclusiveRanges) { + constexpr llvm::StringLiteral sources[] = { + "OPENQASM 3.1; qubit q; for int i in [0:1:2] { x q; }", + "OPENQASM 3.1; qubit q; for int i in [2:-1:0] { x q; }", + "OPENQASM 3.1; qubit q; for int i in [3:1:0] { x q; }", + "OPENQASM 3.1; qubit q; for int i in [7:1:7] { x q; }", + "OPENQASM 3.1; qubit q; for int i in [0:2:3] { x q; }", + "OPENQASM 3.1; qubit q; for int i in " + "[9223372036854775806:1:9223372036854775807] { x q; }", + }; + for (const auto source : sources) { + SCOPED_TRACE(source.str()); + MLIRContext context; + auto module = qc::translateQASM3ToQC(source, &context); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(verify(*module))); + std::size_t forLoops = 0; + std::size_t whileLoops = 0; + std::size_t divisions = 0; + module->walk([&](scf::ForOp) { ++forLoops; }); + module->walk([&](scf::WhileOp) { ++whileLoops; }); + module->walk([&](arith::DivUIOp) { ++divisions; }); + EXPECT_EQ(forLoops, 1); + EXPECT_EQ(whileLoops, 0); + EXPECT_EQ(divisions, 0); + } +} + +TEST(OpenQASMTargetTest, UsesComparisonDrivenDynamicInclusiveRanges) { + constexpr llvm::StringLiteral SOURCE = R"qasm( +OPENQASM 3.1; +qubit q; +int start = 0; +int step = 1; +int stop = 2; +if (measure q) { start = 1; } +for int i in [start:step:stop] { x q; } +)qasm"; + + MLIRContext context; + auto module = qc::translateQASM3ToQC(SOURCE, &context); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(verify(*module))); + std::size_t whileLoops = 0; + std::size_t divisions = 0; + std::size_t assertions = 0; + module->walk([&](scf::WhileOp) { ++whileLoops; }); + module->walk([&](arith::DivUIOp) { ++divisions; }); + module->walk([&](cf::AssertOp) { ++assertions; }); + EXPECT_EQ(whileLoops, 1); + EXPECT_EQ(divisions, 0); + EXPECT_GE(assertions, 1); +} + +TEST(OpenQASMTargetTest, PreservesStaticallySelectedIndexState) { + constexpr llvm::StringLiteral SOURCE = R"qasm( +OPENQASM 3.1; +include "stdgates.inc"; +qubit[2] q; +int i = 0; +if (false) { i = 1; pow(2) @ x q[1]; } +x q[i]; +)qasm"; + + MLIRContext context; + auto module = qc::translateQASM3ToQC(SOURCE, &context); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(verify(*module))); + std::size_t conditionals = 0; + module->walk([&](scf::IfOp) { ++conditionals; }); + EXPECT_EQ(conditionals, 1); +} + +TEST(OpenQASMTargetTest, PreservesEqualConstantIndexJoins) { + constexpr llvm::StringLiteral SOURCE = R"qasm( +OPENQASM 3.1; +include "stdgates.inc"; +qubit[2] q; +int i = 0; +if (measure q[0]) { i = 1; } else { i = 1; } +x q[i]; +)qasm"; + + MLIRContext context; + auto module = qc::translateQASM3ToQC(SOURCE, &context); + ASSERT_TRUE(module); + EXPECT_TRUE(succeeded(verify(*module))); +} + +TEST(OpenQASMTargetTest, DynamicQubitDispatchLowersThroughQCO) { + constexpr llvm::StringLiteral SOURCE = R"qasm( +OPENQASM 3.1; +include "stdgates.inc"; +qubit[2] q; +int i = 0; +x q[i]; +bit result = measure q[i]; +)qasm"; + + MLIRContext context; + auto module = qc::translateQASM3ToQC(SOURCE, &context); + ASSERT_TRUE(module); + bool sawResultDispatch = false; + std::size_t stackAllocations = 0; + module->walk([&](Operation* operation) { + stackAllocations += isa(operation); + auto conditional = dyn_cast(operation); + if (!conditional || conditional.getNumResults() != 1 || + !conditional.getResult(0).getType().isInteger(1)) { + return; + } + std::size_t measurements = 0; + conditional->walk([&](qc::MeasureOp) { ++measurements; }); + sawResultDispatch |= measurements > 0; + }); + EXPECT_EQ(stackAllocations, 0); + EXPECT_TRUE(sawResultDispatch); + PassManager manager(&context); + manager.addPass(createQCToQCO()); + ASSERT_TRUE(succeeded(manager.run(*module))); + ASSERT_TRUE(succeeded(verify(*module))); + bool retainsQCReferences = false; + module->walk([&](Operation* operation) { + const auto isQCQubit = [](Type type) { return isa(type); }; + retainsQCReferences |= + llvm::any_of(operation->getOperandTypes(), isQCQubit) || + llvm::any_of(operation->getResultTypes(), isQCQubit); + }); + EXPECT_FALSE(retainsQCReferences); +} + +TEST(OpenQASMTargetTest, PreservesBooleanEvaluationOrderAndIEEEInequality) { + constexpr llvm::StringLiteral SOURCE = R"qasm( +OPENQASM 3.1; +qubit[2] q; +bool measured = measure q[0] && measure q[1]; +float negative = -1.0; +float notANumber = sqrt(negative); +if (measured || notANumber != notANumber) { x q[0]; } +output bit[2] result = measure q; +)qasm"; + + MLIRContext context; + auto module = qc::translateQASM3ToQC(SOURCE, &context); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(verify(*module))); + + SmallVector firstMeasuredIndices; + bool sawUnorderedInequality = false; + module->walk([&](Operation* operation) { + if (auto comparison = dyn_cast(operation)) { + sawUnorderedInequality |= + comparison.getPredicate() == arith::CmpFPredicate::UNE; + } + auto measurement = dyn_cast(operation); + if (!measurement || firstMeasuredIndices.size() == 2) { + return; + } + auto load = measurement.getQubit().getDefiningOp(); + if (!load || load.getIndices().empty()) { + return; + } + APInt index; + if (matchPattern(load.getIndices().front(), m_ConstantInt(&index))) { + firstMeasuredIndices.push_back(index.getSExtValue()); + } + }); + EXPECT_EQ(firstMeasuredIndices, (SmallVector{0, 1})); + EXPECT_TRUE(sawUnorderedInequality); +} + +TEST(OpenQASMTargetTest, EmitsStructuredLoopsWithCarriedMutableState) { + constexpr llvm::StringLiteral SOURCE = R"qasm( +OPENQASM 3.1; +include "stdgates.inc"; +qubit q; +float theta = 0.0; +for int i in [0:2] { + theta += 0.125; + h q; +} +while (measure q) { + theta += 0.25; + rx(theta) q; +} +rx(theta) q; +bit result = measure q; +)qasm"; + + MLIRContext context; + auto module = qc::translateQASM3ToQC(SOURCE, &context); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(verify(*module))); + + scf::ForOp forLoop; + scf::WhileOp whileLoop; + module->walk([&](Operation* operation) { + if (auto loop = dyn_cast(operation)) { + forLoop = loop; + } + if (auto loop = dyn_cast(operation)) { + whileLoop = loop; + } + }); + ASSERT_TRUE(forLoop); + ASSERT_TRUE(whileLoop); + EXPECT_EQ(forLoop.getInitArgs().size(), 1); + EXPECT_EQ(forLoop.getNumResults(), 1); + EXPECT_EQ(forLoop.getBody()->getTerminator()->getNumOperands(), 1); + EXPECT_EQ(whileLoop.getInits().size(), 1); + EXPECT_EQ(whileLoop.getNumResults(), 1); + EXPECT_EQ(whileLoop.getBeforeBody()->getTerminator()->getNumOperands(), 2); + EXPECT_EQ(whileLoop.getAfterBody()->getTerminator()->getNumOperands(), 1); + PassManager canonicalizer(&context); + canonicalizer.addPass(createCanonicalizerPass()); + ASSERT_TRUE(succeeded(canonicalizer.run(*module))); + forLoop = {}; + module->walk([&](scf::ForOp loop) { forLoop = loop; }); + ASSERT_TRUE(forLoop); + APInt lower; + APInt upper; + APInt step; + ASSERT_TRUE(matchPattern(forLoop.getLowerBound(), m_ConstantInt(&lower))); + ASSERT_TRUE(matchPattern(forLoop.getUpperBound(), m_ConstantInt(&upper))); + ASSERT_TRUE(matchPattern(forLoop.getStep(), m_ConstantInt(&step))); + EXPECT_EQ(lower.getSExtValue(), 0); + EXPECT_EQ(upper.getSExtValue(), 3); + EXPECT_EQ(step.getSExtValue(), 1); + + PassManager manager(&context); + ASSERT_TRUE(succeeded(manager.run(*module))); + EXPECT_TRUE(succeeded(verify(*module))); +} + +TEST(OpenQASMTargetTest, PreservesBranchAndWhileCarriedClassicalBits) { + constexpr llvm::StringLiteral SOURCE = R"qasm( +OPENQASM 3.1; +include "stdgates.inc"; +qubit[2] q; +bool choose = true; +bit branch; +if (choose) { + branch = measure q[0]; +} else { + branch = measure q[1]; +} +while (branch) { + h q[0]; + branch = measure q[0]; +} +if (branch) { x q[1]; } +output bit[2] result = measure q; +)qasm"; + + MLIRContext context; + auto module = qc::translateQASM3ToQC(SOURCE, &context); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(verify(*module))); + std::size_t resultBearingConditionals = 0; + std::size_t resultBearingWhiles = 0; + module->walk([&](Operation* operation) { + if (auto conditional = dyn_cast(operation)) { + resultBearingConditionals += conditional.getNumResults() != 0; + } + if (auto loop = dyn_cast(operation)) { + resultBearingWhiles += loop.getNumResults() != 0; + } + }); + EXPECT_EQ(resultBearingConditionals, 1); + EXPECT_EQ(resultBearingWhiles, 1); + module->walk([&](scf::IfOp conditional) { + if (conditional.getNumResults() == 0) { + return; + } + EXPECT_EQ(conditional.getNumResults(), 1); + EXPECT_TRUE(conditional.getResult(0).getType().isInteger(1)); + EXPECT_EQ( + conditional.getThenRegion().front().getTerminator()->getNumOperands(), + 1); + EXPECT_EQ( + conditional.getElseRegion().front().getTerminator()->getNumOperands(), + 1); + }); + module->walk([&](scf::WhileOp loop) { + EXPECT_EQ(loop.getInits().size(), 1); + EXPECT_EQ(loop.getNumResults(), 1); + EXPECT_EQ(loop.getBeforeBody()->getTerminator()->getNumOperands(), 2); + EXPECT_EQ(loop.getAfterBody()->getTerminator()->getNumOperands(), 1); + }); +} + +TEST(OpenQASMFrontendTest, SupportsRequiredLiteralFormsAndOperatorPrecedence) { + constexpr llvm::StringLiteral SOURCE = R"qasm( +OPENQASM 3.1; +const int binary = 0b1010; +const int octal = 0o12; +const int hexadecimal = 0xA; +const int separated = 1_0; +const float fraction = .5; +const float trailing = 1.; +const float separated_float = 1_0.5_0; +const bool precedence = 1 < 2 == true; +int powered = 2; +powered **= 3; +qubit q; +if (precedence && powered == binary && binary == octal && octal == hexadecimal && + hexadecimal == separated && fraction + trailing + separated_float > 0.0) { + x q; +} +)qasm"; + + auto analyzed = oq3::frontend::analyzeOpenQASM(SOURCE); + ASSERT_TRUE(analyzed) << analyzed.diagnostics.front().message; +} + +TEST(OpenQASMTargetTest, HandlesTheMaximumUnsignedSingletonRange) { + constexpr llvm::StringLiteral SOURCE = R"qasm( +OPENQASM 3.1; +const uint maximum = 18446744073709551615; +qubit q; +for uint i in [maximum:maximum] { if (i == maximum) { x q; } } +)qasm"; + MLIRContext context; + auto module = qc::translateQASM3ToQC(SOURCE, &context); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(verify(*module))); + scf::ForOp loop; + module->walk([&](scf::ForOp current) { loop = current; }); + ASSERT_TRUE(loop); + APInt lower; + APInt step; + ASSERT_TRUE(matchPattern(loop.getLowerBound(), m_ConstantInt(&lower))); + ASSERT_TRUE(matchPattern(loop.getStep(), m_ConstantInt(&step))); + EXPECT_EQ(lower.getSExtValue(), 0); + EXPECT_EQ(step.getSExtValue(), 1); + + PassManager canonicalizer(&context); + canonicalizer.addPass(createCanonicalizerPass()); + ASSERT_TRUE(succeeded(canonicalizer.run(*module))); + std::size_t remainingLoops = 0; + std::size_t xApplications = 0; + module->walk([&](Operation* operation) { + remainingLoops += isa(operation); + xApplications += isa(operation); + }); + EXPECT_EQ(remainingLoops, 0); + EXPECT_EQ(xApplications, 1); +} + +TEST(OpenQASMFrontendTest, DiagnosesMixedPhysicalAndDeclaredQubits) { + constexpr llvm::StringLiteral sources[] = { + "OPENQASM 3.1; qubit q; x q; x $0;", + "OPENQASM 3.1; x $0; qubit q; x q;", + }; + for (const auto source : sources) { + auto analyzed = oq3::frontend::analyzeOpenQASM(source); + ASSERT_FALSE(analyzed); + ASSERT_FALSE(analyzed.diagnostics.empty()); + EXPECT_NE(analyzed.diagnostics.front().message.find("mixing physical"), + std::string::npos); + } +} + +TEST(OpenQASMTargetTest, ExpandsAnOperandlessBarrierToAllDeclaredQubits) { + constexpr llvm::StringLiteral SOURCE = R"qasm( +OPENQASM 3.1; +qubit[3] q; +barrier; +bit[3] result = measure q; +)qasm"; + MLIRContext context; + auto module = qc::translateQASM3ToQC(SOURCE, &context); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(verify(*module))); + std::size_t barriers = 0; + module->walk([&](qc::BarrierOp barrier) { + ++barriers; + EXPECT_EQ(barrier.getNumQubits(), 3); + }); + EXPECT_EQ(barriers, 1); +} + +TEST(OpenQASMTargetTest, CanonicalizesVariadicCompatibilityGates) { + constexpr llvm::StringLiteral SOURCE = R"qasm( +OPENQASM 3.1; +qubit[10] q; +mcx q[0], q[1], q[2], q[3]; +mcphase(0.5) q[0], q[1], q[2]; +mcx_vchain q[0], q[1], q[2], q[3], q[4], q[8], q[9]; +mcx_recursive q[0], q[1], q[2], q[3], q[4], q[9]; +)qasm"; + MLIRContext context; + auto module = qc::translateQASM3ToQC(SOURCE, &context); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(verify(*module))); + std::size_t controls = 0; + std::size_t xGates = 0; + std::size_t phaseGates = 0; + module->walk([&](Operation* operation) { + controls += isa(operation); + xGates += isa(operation); + phaseGates += isa(operation); + }); + EXPECT_EQ(controls, 13); + EXPECT_EQ(xGates, 3); + EXPECT_EQ(phaseGates, 1); +} + +TEST(OpenQASMTargetTest, BroadcastsRegistersAlongsideScalarQubits) { + constexpr llvm::StringLiteral SOURCE = R"qasm( +OPENQASM 3.1; +include "stdgates.inc"; +qubit[3] controls; +qubit target; +cx controls, target; +bit[3] left = measure controls; +bit right = measure target; +)qasm"; + + MLIRContext context; + auto module = qc::translateQASM3ToQC(SOURCE, &context); + ASSERT_TRUE(module); + std::size_t controls = 0; + module->walk([&](qc::CtrlOp) { ++controls; }); + EXPECT_EQ(controls, 3); +} + +TEST(OpenQASMTargetTest, PreservesImportedWhileBehavior) { + struct OperationCounts { + std::size_t h; + std::size_t x; + std::size_t measurements; + std::size_t controls; + }; + struct ConditionalCounts { + std::size_t semantic; + std::size_t dispatch; + std::size_t whileMeasurements; + }; + struct Fixture { + llvm::StringRef name; + llvm::StringRef source; + SmallVector tripCounts; + std::size_t whileLoops; + OperationCounts operations; + ConditionalCounts conditionals; + }; + const Fixture fixtures[] = { + {"simple-while", qasm::simpleWhileReset, {}, 1, {2, 0, 2, 0}, {0, 0, 1}}, + {"condition-while-and", + qasm::conditionWhileAnd, + {}, + 1, + {3, 0, 4, 0}, + {0, 0, 2}}, + }; + + for (const auto& fixture : fixtures) { + SCOPED_TRACE(fixture.name.str()); + MLIRContext context; + auto module = qc::translateQASM3ToQC(fixture.source, &context); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(verify(*module))); + + PassManager canonicalizer(&context); + canonicalizer.addPass(createCanonicalizerPass()); + ASSERT_TRUE(succeeded(canonicalizer.run(*module))); + + SmallVector forLoops; + std::size_t whileLoops = 0; + bool hasQubitSelect = false; + module->walk([&](Operation* operation) { + if (auto loop = dyn_cast(operation)) { + forLoops.push_back(loop); + } + whileLoops += isa(operation); + if (auto select = dyn_cast(operation)) { + hasQubitSelect |= isa(select.getType()); + } + }); + ASSERT_EQ(forLoops.size(), fixture.tripCounts.size()); + EXPECT_EQ(whileLoops, fixture.whileLoops); + EXPECT_FALSE(hasQubitSelect); + for (const auto [loop, expectedCount] : + llvm::zip_equal(forLoops, fixture.tripCounts)) { + APInt lower; + APInt upper; + APInt step; + ASSERT_TRUE(matchPattern(loop.getLowerBound(), m_ConstantInt(&lower))); + ASSERT_TRUE(matchPattern(loop.getUpperBound(), m_ConstantInt(&upper))); + ASSERT_TRUE(matchPattern(loop.getStep(), m_ConstantInt(&step))); + EXPECT_EQ(lower.getSExtValue(), 0); + EXPECT_EQ(upper.getSExtValue(), expectedCount); + EXPECT_EQ(step.getSExtValue(), 1); + } + + PassManager lowering(&context); + ASSERT_TRUE(succeeded(lowering.run(*module))); + ASSERT_TRUE(succeeded(verify(*module))); + std::size_t hGates = 0; + std::size_t xGates = 0; + std::size_t measurements = 0; + std::size_t controls = 0; + std::size_t semanticConditionals = 0; + std::size_t dispatchConditionals = 0; + SmallVector loweredForLoops; + SmallVector loweredWhileLoops; + module->walk([&](Operation* operation) { + hGates += isa(operation); + xGates += isa(operation); + measurements += isa(operation); + controls += isa(operation); + if (auto control = dyn_cast(operation)) { + std::size_t controlledXGates = 0; + control->walk([&](qc::XOp) { ++controlledXGates; }); + EXPECT_EQ(controlledXGates, 1) + << "each imported controlled-X must retain its controlled body"; + } + if (auto loop = dyn_cast(operation)) { + loweredForLoops.push_back(loop); + } + if (auto loop = dyn_cast(operation)) { + loweredWhileLoops.push_back(loop); + } + auto conditional = dyn_cast(operation); + if (!conditional) { + return; + } + auto comparison = + conditional.getCondition().getDefiningOp(); + if (!comparison || + comparison.getPredicate() != arith::CmpIPredicate::eq) { + ++semanticConditionals; + return; + } + + APInt candidate; + const bool lhsCandidate = + matchPattern(comparison.getLhs(), m_ConstantInt(&candidate)); + const bool rhsCandidate = + matchPattern(comparison.getRhs(), m_ConstantInt(&candidate)); + if (lhsCandidate == rhsCandidate) { + ++semanticConditionals; + return; + } + ++dispatchConditionals; + std::size_t dispatchedQuantumOperations = 0; + conditional->walk([&](Operation* nested) { + dispatchedQuantumOperations += + isa(nested); + }); + EXPECT_GT(dispatchedQuantumOperations, 0) + << "each dynamic-index dispatch must retain quantum behavior"; + }); + EXPECT_EQ(hGates, fixture.operations.h); + EXPECT_EQ(xGates, fixture.operations.x); + EXPECT_EQ(measurements, fixture.operations.measurements); + EXPECT_EQ(controls, fixture.operations.controls); + EXPECT_EQ(semanticConditionals, fixture.conditionals.semantic); + EXPECT_EQ(dispatchConditionals, fixture.conditionals.dispatch); + + ASSERT_EQ(loweredForLoops.size(), fixture.tripCounts.size()); + for (auto loop : loweredForLoops) { + std::size_t bodyGates = 0; + loop.getRegion().walk([&](Operation* nested) { + bodyGates += isa(nested); + }); + EXPECT_GT(bodyGates, 0) + << "each imported for-loop body must retain its gate behavior"; + } + + ASSERT_EQ(loweredWhileLoops.size(), fixture.whileLoops); + for (auto loop : loweredWhileLoops) { + std::size_t conditionMeasurements = 0; + std::size_t bodyGates = 0; + loop.getBefore().walk([&](qc::MeasureOp) { ++conditionMeasurements; }); + loop.getAfter().walk([&](Operation* nested) { + bodyGates += isa(nested); + }); + EXPECT_EQ(conditionMeasurements, fixture.conditionals.whileMeasurements); + EXPECT_GT(bodyGates, 0) + << "each imported while-loop body must retain its gate behavior"; + } + } +} + +} // namespace diff --git a/mlir/unittests/programs/qasm_programs.cpp b/mlir/unittests/programs/qasm_programs.cpp index 97cf133e2a..3fd809bcb2 100644 --- a/mlir/unittests/programs/qasm_programs.cpp +++ b/mlir/unittests/programs/qasm_programs.cpp @@ -880,5 +880,400 @@ if (c) { bit[1] out = measure q; )qasm"; +const std::string nestedIfOpForLoop = R"qasm(OPENQASM 3.0; +include "stdgates.inc"; +qubit[3] reg; +qubit q; +h q; +bit c = measure q; +if (c) { h q; } else { + for uint i in [0:2] { h reg[i]; } +} +output bit out = measure q; +)qasm"; + +const std::string simpleWhileReset = R"qasm(OPENQASM 3.0; +include "stdgates.inc"; +qubit q; +h q; +while (measure q) { h q; } +bit out = measure q; +)qasm"; + +const std::string simpleForLoop = R"qasm(OPENQASM 3.0; +include "stdgates.inc"; +qubit[2] q; +for uint i in [0:1] { h q[i]; } +bit[2] out = measure q; +)qasm"; + +const std::string nestedForLoopIfOp = R"qasm(OPENQASM 3.0; +include "stdgates.inc"; +qubit[2] reg; +qubit q; +for uint i in [0:1] { + h q; + if (measure q) { h reg[i]; } +} +bit out = measure q; +)qasm"; + +const std::string nestedForLoopWhileOp = R"qasm(OPENQASM 3.0; +include "stdgates.inc"; +qubit[2] q; +for uint i in [0:1] { h q[i]; } +for uint i in [0:1] { + while (measure q[i]) { h q[i]; } +} +bit[2] out = measure q; +)qasm"; + +const std::string nestedForLoopCtrlOpWithSeparateQubit = R"qasm(OPENQASM 3.0; +include "stdgates.inc"; +qubit[3] q; +qubit control; +h control; +for uint i in [0:2] { h q[i]; cx control, q[i]; } +bit out = measure control; +)qasm"; + +const std::string nestedForLoopCtrlOpWithExtractedQubit = + R"qasm(OPENQASM 3.0; +include "stdgates.inc"; +qubit[4] q; +h q[0]; +for uint i in [1:3] { h q[i]; cx q[0], q[i]; } +bit out = measure q[0]; +)qasm"; + +const std::string broadcastRegisterAndQubit = R"qasm(OPENQASM 3.0; +include "stdgates.inc"; +qubit[3] reg; +qubit q; +cx reg, q; +bit[3] left = measure reg; +bit right = measure q; +)qasm"; + +const std::string broadcastCompoundGate = R"qasm(OPENQASM 3.0; +include "stdgates.inc"; +gate compound a, b { x a; cx a, b; } +qubit[3] reg; +qubit q; +compound reg, q; +bit[3] left = measure reg; +bit right = measure q; +)qasm"; + +const std::string expressionArithmetic = R"qasm(OPENQASM 3.0; +include "stdgates.inc"; +qubit q; +h q; +rx((1.0 + 2.0) * 3.0 / 2.0 - 0.5) q; +bit c = measure q; +)qasm"; + +const std::string expressionUnaryMinus = R"qasm(OPENQASM 3.0; +include "stdgates.inc"; +qubit q; +h q; +rx(-0.5) q; +ry(-(1.0 + 2.0)) q; +rz(-(-0.25)) q; +bit c = measure q; +)qasm"; + +const std::string expressionBuiltinConstants = R"qasm(OPENQASM 3.0; +include "stdgates.inc"; +qubit q; +h q; +rx(pi / 2) q; +ry(tau / 4) q; +rz(euler) q; +bit c = measure q; +)qasm"; + +const std::string expressionMathFunctions = R"qasm(OPENQASM 3.0; +include "stdgates.inc"; +qubit q; +h q; +rx(arccos(0.5)) q; +rx(arcsin(0.5)) q; +rx(arctan(0.5)) q; +rx(cos(0.5)) q; +rx(exp(0.5)) q; +rx(log(2.0)) q; +rx(mod(5.5, 2.0)) q; +rx(pow(2.0, 3.0)) q; +rx(sin(0.5)) q; +rx(sqrt(2.0)) q; +rx(tan(0.5)) q; +bit c = measure q; +)qasm"; + +const std::string expressionNestedMathFunctions = R"qasm(OPENQASM 3.0; +include "stdgates.inc"; +qubit q; +h q; +rx(sqrt(pow(sin(0.5), 2.0) + pow(cos(0.5), 2.0))) q; +bit c = measure q; +)qasm"; + +const std::string expressionConstFloat = R"qasm(OPENQASM 3.0; +include "stdgates.inc"; +const float theta = pi / 4; +qubit q; +h q; +rx(theta) q; +ry(theta * 2.0) q; +bit c = measure q; +)qasm"; + +const std::string expressionMutableFloat = R"qasm(OPENQASM 3.0; +include "stdgates.inc"; +qubit q; +float theta = 0.5; +h q; +rx(theta) q; +theta = theta + 0.25; +ry(theta) q; +bit c = measure q; +)qasm"; + +const std::string expressionConstIntArithmetic = R"qasm(OPENQASM 3.0; +include "stdgates.inc"; +const int n = pow(2, 3); +const int m = mod(11, 4); +const int k = (1 + 2) * 3 - 4; +qubit[n] q; +h q[m]; +h q[k]; +rx(m + k) q[m]; +bit[2] c; +c[0] = measure q[m]; +c[1] = measure q[k]; +)qasm"; + +const std::string expressionDynamicIntIndex = R"qasm(OPENQASM 3.0; +include "stdgates.inc"; +qubit[4] q; +for uint i in [0:2] { int x = i + 1; h q[x]; } +bit[4] c = measure q; +)qasm"; + +const std::string expressionModIndex = R"qasm(OPENQASM 3.0; +include "stdgates.inc"; +qubit[2] q; +for uint i in [0:3] { h q[mod(i, 2)]; } +bit[2] c = measure q; +)qasm"; + +const std::string conditionLiteral = R"qasm(OPENQASM 3.0; +include "stdgates.inc"; +qubit[2] q; +h q[0]; +if (true) { x q[0]; } +if (false) { x q[1]; } +bit[2] c = measure q; +)qasm"; + +const std::string conditionMeasurement = R"qasm(OPENQASM 3.0; +include "stdgates.inc"; +qubit[2] q; +h q[0]; +if (measure q[0]) { x q[1]; } +bit c = measure q[1]; +)qasm"; + +const std::string conditionAnd = R"qasm(OPENQASM 3.0; +include "stdgates.inc"; +qubit[3] q; +h q[0]; h q[1]; +bit c0 = measure q[0]; bit c1 = measure q[1]; +if (c0 && c1) { x q[2]; } +bit out = measure q[2]; +)qasm"; + +const std::string conditionOr = R"qasm(OPENQASM 3.0; +include "stdgates.inc"; +qubit[3] q; +h q[0]; h q[1]; +bit c0 = measure q[0]; bit c1 = measure q[1]; +if (c0 || c1) { x q[2]; } else { h q[2]; } +bit out = measure q[2]; +)qasm"; + +const std::string conditionNotAndOr = R"qasm(OPENQASM 3.0; +include "stdgates.inc"; +qubit[4] q; +h q[0]; h q[1]; h q[2]; +bit c0 = measure q[0]; bit c1 = measure q[1]; +bit c2 = measure q[2]; +if (!(c0 && c1) || c2) { x q[3]; } +bit out = measure q[3]; +)qasm"; + +const std::string conditionBoolVariable = R"qasm(OPENQASM 3.0; +include "stdgates.inc"; +qubit[3] q; +h q[0]; h q[1]; +bit c0 = measure q[0]; bit c1 = measure q[1]; +bool both = c0 && c1; +bool neither = !both; +if (neither) { x q[2]; } +bit out = measure q[2]; +)qasm"; + +const std::string conditionIndexedBit = R"qasm(OPENQASM 3.0; +include "stdgates.inc"; +qubit[3] q; +h q[0]; h q[1]; +bit[2] c; +c[0] = measure q[0]; c[1] = measure q[1]; +if (c[1]) { x q[2]; } +bit out = measure q[2]; +)qasm"; + +const std::string conditionWhileAnd = R"qasm(OPENQASM 3.0; +include "stdgates.inc"; +qubit[2] q; +h q[0]; +while (measure q[0] && measure q[1]) { h q[0]; h q[1]; } +bit[2] c = measure q; +)qasm"; + +static const std::string nestedStaticControlFlow = R"qasm(OPENQASM 3.1; +include "stdgates.inc"; +qubit q; +bit enabled = false; +if (enabled) { h q; } else { for int i in [0:2] { x q; } } +output bit result = measure q; +)qasm"; +static const std::string mutableLoopState = R"qasm(OPENQASM 3.1; +include "stdgates.inc"; +qubit q; +bit enabled = measure q; +while (enabled) { + x q; + enabled = measure q; +} +if (enabled) { h q; } +output bit result = measure q; +)qasm"; +static const std::string resolvedDynamicIndex = R"qasm(OPENQASM 3.1; +include "stdgates.inc"; +qubit[2] q; +int index = 1; +x q[index]; +output bit[2] result = measure q; +)qasm"; +static const std::string equalConstantIndexJoin = R"qasm(OPENQASM 3.1; +include "stdgates.inc"; +qubit[2] q; +int index = 0; +if (measure q[0]) { index = 1; } else { index = 1; } +x q[index]; +output bit[2] result = measure q; +)qasm"; +static const std::string scalarLoopState = R"qasm(OPENQASM 3.1; +include "stdgates.inc"; +qubit q; +float theta = 0.0; +for int i in [0:2] { theta += 0.125; } +while (measure q) { + theta += 0.25; + rx(theta) q; +} +rx(theta) q; +output bit result = measure q; +)qasm"; +static const std::string runtimeDynamicIndex = R"qasm(OPENQASM 3.1; +include "stdgates.inc"; +qubit[2] q; +int index = 0; +if (measure q[0]) { index = 1; } +x q[index]; +output bit[2] result = measure q; +)qasm"; +static const std::string inductionVariableIndex = R"qasm(OPENQASM 3.1; +include "stdgates.inc"; +qubit[3] q; +for uint i in [0:2] { x q[i]; } +output bit[3] result = measure q; +)qasm"; +static const std::string checkedIntegerState = R"qasm(OPENQASM 3.1; +include "stdgates.inc"; +qubit q; +int turns = 0; +for int i in [0:2] { turns += 1; } +rx(turns) q; +output bit result = measure q; +)qasm"; +static const std::string dynamicRange = R"qasm(OPENQASM 3.1; +include "stdgates.inc"; +qubit q; +int start = 0; +int step = 1; +int stop = 2; +if (measure q) { start = 1; } +for int i in [start:step:stop] { x q; } +output bit result = measure q; +)qasm"; + +llvm::ArrayRef standardPipelinePrograms() { + static const OpenQASMProgram programs[]{ + {"broadcast-custom-gate", broadcastCompoundGate}, + {"arithmetic-parameters", expressionArithmetic}, + {"math-parameters", expressionMathFunctions}, + {"simple-if", conditionLiteral}, + {"nested-static-control-flow", nestedStaticControlFlow}, + {"mutable-loop-state", mutableLoopState}, + {"measurement-controlled-while", conditionWhileAnd}, + {"resolved-dynamic-index", resolvedDynamicIndex}, + {"equal-constant-index-join", equalConstantIndexJoin}, + {"scalar-loop-state", scalarLoopState}, + {"reset", resetQubitAfterSingleOp}, + {"barrier", barrierMultipleQubits}, + {"mixed-controls", mixedControlledX}, + {"runtime-dynamic-index", runtimeDynamicIndex}, + {"induction-variable-index", inductionVariableIndex}, + {"checked-integer-state", checkedIntegerState}, + {"dynamic-range", dynamicRange}, + }; + return programs; +} + +llvm::ArrayRef jeffCompatiblePrograms() { + static const OpenQASMProgram programs[]{ + {"broadcast-custom-gate", broadcastCompoundGate}, + {"nested-static-control-flow", nestedStaticControlFlow}, + {"mutable-loop-state", mutableLoopState}, + {"scalar-loop-state", scalarLoopState}, + {"reset", resetQubitAfterSingleOp}, + {"mixed-controls", mixedControlledX}, + }; + return programs; +} + +llvm::ArrayRef jeffIncompatiblePrograms() { + static const OpenQASMProgram programs[]{ + {"runtime-dynamic-index", runtimeDynamicIndex}, + {"induction-variable-index", inductionVariableIndex}, + {"checked-integer-state", checkedIntegerState}, + {"dynamic-range", dynamicRange}, + }; + return programs; +} + +llvm::ArrayRef baseProfilePrograms() { + static const OpenQASMProgram programs[]{ + {"broadcast-custom-gate", broadcastCompoundGate}, + {"arithmetic-parameters", expressionArithmetic}, + {"math-parameters", expressionMathFunctions}, + {"barrier", barrierMultipleQubits}, + }; + return programs; +} + } // namespace mlir::qasm // NOLINTEND(readability-identifier-naming) diff --git a/mlir/unittests/programs/qasm_programs.h b/mlir/unittests/programs/qasm_programs.h index 5da45582c7..c2889be25d 100644 --- a/mlir/unittests/programs/qasm_programs.h +++ b/mlir/unittests/programs/qasm_programs.h @@ -10,11 +10,33 @@ #pragma once +#include +#include + #include // NOLINTBEGIN(readability-identifier-naming) namespace mlir::qasm { +struct OpenQASMProgram { + llvm::StringRef name; + llvm::StringRef source; +}; + +/// OpenQASM programs expected to traverse QC, optimized QCO, reconstructed QC, +/// and Adaptive QIR. +[[nodiscard]] llvm::ArrayRef standardPipelinePrograms(); + +/// OpenQASM programs that additionally round-trip through jeff. +[[nodiscard]] llvm::ArrayRef jeffCompatiblePrograms(); + +/// OpenQASM programs accepted by the standard pipeline but rejected when QCO +/// is converted to jeff. +[[nodiscard]] llvm::ArrayRef jeffIncompatiblePrograms(); + +/// Straight-line compiler programs that additionally support Base QIR. +[[nodiscard]] llvm::ArrayRef baseProfilePrograms(); + /// Allocates a single qubit. extern const std::string allocQubit; @@ -370,5 +392,86 @@ extern const std::string ifEmptyThen; /// Creates a circuit with an if operation with an else branch. extern const std::string ifElse; +/// Creates an if operation with a nested register loop. +extern const std::string nestedIfOpForLoop; + +/// Creates a measurement-controlled while loop. +extern const std::string simpleWhileReset; + +/// Applies a gate to every qubit selected by a source-level for loop. +extern const std::string simpleForLoop; + +/// Creates a for loop containing a measurement-controlled branch. +extern const std::string nestedForLoopIfOp; + +/// Creates two register loops, one containing a while loop. +extern const std::string nestedForLoopWhileOp; + +/// Uses a separately allocated control in a register loop. +extern const std::string nestedForLoopCtrlOpWithSeparateQubit; + +/// Uses one register element as a control for dynamically selected elements. +extern const std::string nestedForLoopCtrlOpWithExtractedQubit; + +/// Broadcasts a controlled gate over a register and scalar qubit. +extern const std::string broadcastRegisterAndQubit; + +/// Broadcasts a custom compound gate over a register and scalar qubit. +extern const std::string broadcastCompoundGate; + +/// Exercises arithmetic precedence in gate parameters. +extern const std::string expressionArithmetic; + +/// Exercises unary negation in a gate parameter. +extern const std::string expressionUnaryMinus; + +/// Exercises the built-in pi, tau, and Euler constants. +extern const std::string expressionBuiltinConstants; + +/// Exercises scalar mathematical functions in gate parameters. +extern const std::string expressionMathFunctions; + +/// Exercises nested scalar mathematical functions. +extern const std::string expressionNestedMathFunctions; + +/// Uses a constant floating-point variable as a gate parameter. +extern const std::string expressionConstFloat; + +/// Reassigns a mutable floating-point variable used by a gate. +extern const std::string expressionMutableFloat; + +/// Exercises constant integer arithmetic used as an index. +extern const std::string expressionConstIntArithmetic; + +/// Uses a mutable integer as a dynamic qubit index. +extern const std::string expressionDynamicIntIndex; + +/// Uses the integer modulo operator to derive a dynamic qubit index. +extern const std::string expressionModIndex; + +/// Creates a conditional from a Boolean literal. +extern const std::string conditionLiteral; + +/// Creates a conditional directly from a measurement. +extern const std::string conditionMeasurement; + +/// Combines two measurement conditions with logical conjunction. +extern const std::string conditionAnd; + +/// Combines two measurement conditions with logical disjunction. +extern const std::string conditionOr; + +/// Exercises precedence among negation, conjunction, and disjunction. +extern const std::string conditionNotAndOr; + +/// Creates a conditional from a mutable Boolean variable. +extern const std::string conditionBoolVariable; + +/// Creates a conditional from an indexed classical bit. +extern const std::string conditionIndexedBit; + +/// Combines a while loop with a compound measurement condition. +extern const std::string conditionWhileAnd; + } // namespace mlir::qasm // NOLINTEND(readability-identifier-naming) diff --git a/pyproject.toml b/pyproject.toml index 6157ec6fc8..7fe5414e12 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -261,6 +261,7 @@ mch = "mch" ket = "ket" optin = "optin" nd = "nd" +olt = "olt" [tool.repo-review.ignore]