From b19875fe200dce1e167972bdbc7620588d824a7e Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Wed, 19 Aug 2026 16:54:32 +0200 Subject: [PATCH 01/13] =?UTF-8?q?=E2=9A=A1=20Keep=20wide=20OpenQASM=20regi?= =?UTF-8?q?ster=20conditions=20linear?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Share pure register conditions within one classical snapshot and reconstruct arbitrary-width equalities during OpenQASM emission. Keep stale, dynamic, ambiguous, and unsafe partial forms fail-closed. Assisted-by: Codex Signed-off-by: Simon Hofmann --- docs/mlir/OpenQASM.md | 36 +- .../QC/Translation/OpenQASMToQCEmitter.cpp | 174 ++++- .../QC/Translation/TranslateQCToOpenQASM3.cpp | 613 +++++++++++++++++- .../lib/Target/OpenQASM/OpenQASMSemantics.cpp | 17 +- .../Translation/test_openqasm3_emission.cpp | 420 +++++++++++- .../Target/OpenQASM/test_openqasm_emitter.cpp | 98 +++ .../OpenQASM/test_openqasm_semantics.cpp | 42 ++ 7 files changed, 1348 insertions(+), 52 deletions(-) diff --git a/docs/mlir/OpenQASM.md b/docs/mlir/OpenQASM.md index a8244c56e1..e196786b3e 100644 --- a/docs/mlir/OpenQASM.md +++ b/docs/mlir/OpenQASM.md @@ -47,6 +47,10 @@ mqt-cc --input-format=qasm program.txt | Dynamic indexing | Classical bit indices can be dynamic and receive runtime bounds checks. A nonconstant qubit index must be a proven affine expression as described below. | | Unsupported language areas | Subroutines, `extern`, calibration and timing constructs, input declarations, arbitrary arrays, `break`, and `continue` are diagnosed. | +Bit-register equality accepts unsigned integer constants of arbitrary width. +OpenQASM 3 requires every compared bit to be initialized; OpenQASM 2 retains its +standard zero-initialized register behavior. + Syntax and semantic diagnostics retain source locations and include stacks. Runtime integer preconditions and classical-index bounds are represented explicitly in QC. This safety machinery is supported by the normal compiler and @@ -145,16 +149,25 @@ QCO optimization pipeline, converts back to QC, and then exports. Calling {code}`mlir::QCProgram::toOpenQASM3` applies the QC cleanup pipeline but bypasses that QCO optimization round trip. +For measurement-conditioned programs, target compilation can expose the +frontend's bit-register equality as a classical SSA expression. The exporter +recognizes that exact unchanged expression, fuses eligible direct measurement +stores, and emits one register comparison. OpenQASM 3 input follows the same +path when every compared bit is initialized. The constant is not limited to a +machine integer, so this compatibility path also supports registers wider than +64 bits. Other expression shapes continue through the normal support checks +below. + ### Export and round-trip support -| QC or MLIR concept | Export support | -| ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Qubits and classical bits | Logical and physical qubits, scalar qubit allocations, static rank-one qubit memrefs, and CBit registers. Qubit memory indices must resolve statically. CBit indices can be dynamic. | -| Quantum operations | Measurement, reset, barrier, deallocation, global phase, and QC unitary operations. The exporter uses standard gates where available; for example, `sxdg` becomes `inv @ sx` and `u2` uses the standard compatibility alias. | -| Gate modifiers | Nested `ctrl`, `inv`, and `pow`. A multi-operation modifier body with target qubits becomes a private generated gate. | -| Scalar values | `i1`, `i64`, `f64`, and internal `index` values, including arithmetic, comparisons, Boolean operations, value-preserving casts, and supported math functions. | -| Structured control | Result-free `scf.if` and `scf.index_switch`, constant-range `scf.for` without iterated state, and zero-state expression-based `scf.while`. Index switches use native `switch`, `case`, and `default` statements. | -| Results | Multiple scalar and bit-register outputs using the canonical type and naming rules below. | +| QC or MLIR concept | Export support | +| ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Qubits and classical bits | Logical and physical qubits, scalar qubit allocations, static rank-one qubit memrefs, and CBit registers. Qubit memory indices must resolve statically. CBit indices can be dynamic. | +| Quantum operations | Measurement, reset, barrier, deallocation, global phase, and QC unitary operations. The exporter uses standard gates where available; for example, `sxdg` becomes `inv @ sx` and `u2` uses the standard compatibility alias. | +| Gate modifiers | Nested `ctrl`, `inv`, and `pow`. A multi-operation modifier body with target qubits becomes a private generated gate. | +| Scalar values | `i1`, `i64`, `f64`, and internal `index` values, including arithmetic, comparisons, Boolean operations, value-preserving casts, and supported math functions. | +| Structured control | Result-free `scf.if` and `scf.index_switch`, constant-range `scf.for` without iterated state, and zero-state expression-based `scf.while`. Complete register-equality conditions produced by the frontend are reconstructed as direct comparisons, including registers wider than 64 bits. Index switches use native `switch`, `case`, and `default` statements. | +| Results | Multiple scalar and bit-register outputs using the canonical type and naming rules below. | The exporter writes an OpenQASM 3.1 version declaration and includes `stdgates.inc`. Gates in MQT Core's compatibility catalog, such as `r`, `rzz`, @@ -199,7 +212,12 @@ arbitrary CFGs, multi-block SCF regions, dynamic qubit indices or ranges, general memrefs, unsupported integer widths, packed bit-vector operations, unknown operations, and non-unitary content inside modifier regions. CBit loads, stores, and dynamic indices are supported. SCF results, loop-carried values, -nonempty `scf.yield`, and `arith.select` are outside the export subset. +nonempty `scf.yield`, and `arith.select` are outside the export subset. The sole +result-bearing SCF exception is an unchanged bit-register equality over CBit +storage produced by the frontend. The exporter emits this compatibility form as +one register comparison and rejects mixed, dynamically indexed, or modified +register conditions. A zero-initialized register can omit bits that no +intervening write changed. An uninitialized register must constrain every bit. Multi-operation modifier bodies must have a target qubit and cannot capture additional qubits from an enclosing scope. diff --git a/mlir/lib/Dialect/QC/Translation/OpenQASMToQCEmitter.cpp b/mlir/lib/Dialect/QC/Translation/OpenQASMToQCEmitter.cpp index c0c2de6659..9287fdd270 100644 --- a/mlir/lib/Dialect/QC/Translation/OpenQASMToQCEmitter.cpp +++ b/mlir/lib/Dialect/QC/Translation/OpenQASMToQCEmitter.cpp @@ -50,6 +50,7 @@ #include #include #include +#include #include #include #include @@ -119,7 +120,8 @@ class OpenQASMToQCEmitter { classicalRegisters(program.registers.size()), scalarValues(program.scalars.size()), expressionEmissionCosts(program.expressions.size()), - bitVectorExpressionEmissionCosts(program.bitVectorExpressions.size()) { + bitVectorExpressionEmissionCosts(program.bitVectorExpressions.size()), + canonicalConditions(program.conditions.size()) { context .loadDialect emit() { @@ -192,6 +195,11 @@ class OpenQASMToQCEmitter { llvm::DenseMap provenInductionValues; mutable std::vector> expressionEmissionCosts; mutable std::vector> bitVectorExpressionEmissionCosts; + // Canonical IDs exist only for pure, static classical-bit condition trees. + // Cached values are valid only in the current region and memory snapshot. + std::vector> canonicalConditions; + DenseMap conditionCache; + uint64_t classicalStateGeneration = 0; DenseMap structuredGateCapabilities; llvm::StringMap customGateIndex; @@ -199,6 +207,72 @@ class OpenQASMToQCEmitter { using StateSlot = frontend::ScalarId; + struct ConditionCacheKey { + frontend::ConditionKind kind = frontend::ConditionKind::Literal; + bool literal = false; + uint32_t first = 0; + uint32_t second = 0; + uint64_t index = 0; + + [[nodiscard]] bool operator<(const ConditionCacheKey& other) const { + return std::tie(kind, literal, first, second, index) < + std::tie(other.kind, other.literal, other.first, other.second, + other.index); + } + }; + + void initializeCanonicalConditions() { + std::map representatives; + for (const auto [id, condition] : llvm::enumerate(program.conditions)) { + std::optional key; + switch (condition.kind) { + case frontend::ConditionKind::Literal: + key = ConditionCacheKey{.kind = condition.kind, + .literal = condition.literal}; + break; + case frontend::ConditionKind::Bit: + if (!condition.bit.dynamicIndex) { + key = ConditionCacheKey{.kind = condition.kind, + .first = condition.bit.reg, + .index = condition.bit.index}; + } + break; + case frontend::ConditionKind::Not: + if (canonicalConditions.at(condition.lhs)) { + key = ConditionCacheKey{.kind = condition.kind, + .first = + *canonicalConditions.at(condition.lhs)}; + } + break; + case frontend::ConditionKind::And: + case frontend::ConditionKind::Or: + if (canonicalConditions.at(condition.lhs) && + canonicalConditions.at(condition.rhs)) { + key = ConditionCacheKey{ + .kind = condition.kind, + .first = *canonicalConditions.at(condition.lhs), + .second = *canonicalConditions.at(condition.rhs)}; + } + break; + case frontend::ConditionKind::Scalar: + case frontend::ConditionKind::Measurement: + case frontend::ConditionKind::Comparison: + break; + } + if (!key) { + continue; + } + const auto conditionId = static_cast(id); + const auto it = representatives.try_emplace(*key, conditionId).first; + canonicalConditions[conditionId] = it->second; + } + } + + void invalidateConditionCache() { + conditionCache.clear(); + ++classicalStateGeneration; + } + [[nodiscard]] Location getLocation(const frontend::SourceLocation& source) const { return getOpenQASMLocation(source, context); @@ -1927,9 +2001,9 @@ class OpenQASMToQCEmitter { return arith::CmpIOp::create(builder, predicate, lhs, rhs); } - [[nodiscard]] Value emitCondition(const frontend::ConditionId id, - ValueRange gateParameters, - ValueRange gateQubits) { + [[nodiscard]] Value emitConditionUncached(const frontend::ConditionId id, + ValueRange gateParameters, + ValueRange gateQubits) { const auto& condition = program.conditions.at(id); switch (condition.kind) { case frontend::ConditionKind::Literal: @@ -1944,10 +2018,12 @@ class OpenQASMToQCEmitter { [&](Value qubit) { return builder.measure(qubit); }); case frontend::ConditionKind::Not: return arith::XOrIOp::create( - builder, emitCondition(condition.lhs, gateParameters, gateQubits), + builder, + emitConditionUncached(condition.lhs, gateParameters, gateQubits), builder.boolConstant(true)); case frontend::ConditionKind::And: { - auto lhs = emitCondition(condition.lhs, gateParameters, gateQubits); + auto lhs = + emitConditionUncached(condition.lhs, gateParameters, gateQubits); auto ifOp = scf::IfOp::create(builder, builder.getI1Type(), lhs, true); OpBuilder::InsertionGuard guard(builder); auto& thenBlock = ifOp.getThenRegion().front(); @@ -1956,7 +2032,8 @@ class OpenQASMToQCEmitter { } builder.setInsertionPointToEnd(&thenBlock); scf::YieldOp::create( - builder, emitCondition(condition.rhs, gateParameters, gateQubits)); + builder, + emitConditionUncached(condition.rhs, gateParameters, gateQubits)); auto& elseBlock = ifOp.getElseRegion().front(); if (!elseBlock.empty()) { elseBlock.back().erase(); @@ -1966,7 +2043,8 @@ class OpenQASMToQCEmitter { return ifOp.getResult(0); } case frontend::ConditionKind::Or: { - auto lhs = emitCondition(condition.lhs, gateParameters, gateQubits); + auto lhs = + emitConditionUncached(condition.lhs, gateParameters, gateQubits); auto ifOp = scf::IfOp::create(builder, builder.getI1Type(), lhs, true); OpBuilder::InsertionGuard guard(builder); auto& thenBlock = ifOp.getThenRegion().front(); @@ -1981,7 +2059,8 @@ class OpenQASMToQCEmitter { } builder.setInsertionPointToEnd(&elseBlock); scf::YieldOp::create( - builder, emitCondition(condition.rhs, gateParameters, gateQubits)); + builder, + emitConditionUncached(condition.rhs, gateParameters, gateQubits)); return ifOp.getResult(0); } case frontend::ConditionKind::Comparison: @@ -1990,6 +2069,24 @@ class OpenQASMToQCEmitter { llvm_unreachable("unknown condition kind"); } + [[nodiscard]] Value emitCondition(const frontend::ConditionId id, + ValueRange gateParameters, + ValueRange gateQubits) { + // Cache only complete statement conditions. Short-circuit operands can be + // defined inside an scf.if region and cannot be reused in the parent block. + const auto canonical = canonicalConditions.at(id); + if (canonical) { + if (const auto cached = conditionCache.find(*canonical); + cached != conditionCache.end()) { + return cached->second; + } + } + auto value = emitConditionUncached(id, gateParameters, gateQubits); + if (canonical) { + conditionCache.try_emplace(*canonical, value); + } + return value; + } static void recordMutation(const StateSlot slot, llvm::DenseSet& mutationKeys, SmallVectorImpl& mutations) { @@ -2147,6 +2244,7 @@ class OpenQASMToQCEmitter { } else if (statement.conditionInitializer) { value = emitCondition(*statement.conditionInitializer, {}, gateQubits); } + invalidateConditionCache(); scalarValues.at(statement.scalar) = value; } @@ -2154,12 +2252,14 @@ class OpenQASMToQCEmitter { emitScalarAssignment(const frontend::ScalarAssignmentStatement& statement, ValueRange gateQubits) { if (statement.value) { - scalarValues.at(statement.scalar) = - emitExpression(builder, *statement.value, {}); + auto value = emitExpression(builder, *statement.value, {}); + invalidateConditionCache(); + scalarValues.at(statement.scalar) = value; return; } - scalarValues.at(statement.scalar) = - emitCondition(*statement.condition, {}, gateQubits); + auto value = emitCondition(*statement.condition, {}, gateQubits); + invalidateConditionCache(); + scalarValues.at(statement.scalar) = value; } void emitDeclaration(const frontend::DeclarationStatement& statement) { @@ -2187,9 +2287,11 @@ class OpenQASMToQCEmitter { static_cast(declaration.width), declaration.name, program.openQASM2 ? cbit::Initialization::Zero : cbit::Initialization::Undefined); + invalidateConditionCache(); } void assignBit(const frontend::BitReference& target, Value value) { + invalidateConditionCache(); auto reg = classicalRegisters[target.reg]; assert(reg && "semantic analysis must declare bit registers before use"); if (!target.dynamicIndex) { @@ -2215,6 +2317,7 @@ class OpenQASMToQCEmitter { const frontend::BitVectorAssignmentStatement& assignment) { auto value = emitBitVectorExpression(builder, assignment.value); const auto bits = ensureBits(builder, value); + invalidateConditionCache(); auto reg = classicalRegisters[assignment.target]; assert(reg && "semantic analysis must declare bit registers before use"); for (auto [index, bit] : llvm::enumerate(bits)) { @@ -2266,6 +2369,8 @@ class OpenQASMToQCEmitter { const auto slots = mutatedState(nestedStatements); const auto initialValues = stateValues(slots); const auto savedScalars = scalarValues; + const auto savedConditionCache = conditionCache; + const auto savedClassicalStateGeneration = classicalStateGeneration; const auto* thenStatements = &conditional.thenStatements; const auto* elseStatements = &conditional.elseStatements; if (slots.empty() && thenStatements->empty() && !elseStatements->empty()) { @@ -2280,6 +2385,7 @@ class OpenQASMToQCEmitter { const auto emitBranch = [&](Block& block, ArrayRef statements) { scalarValues = savedScalars; + conditionCache.clear(); if (!block.empty()) { block.back().erase(); } @@ -2295,6 +2401,11 @@ class OpenQASMToQCEmitter { } scalarValues = savedScalars; assignState(slots, ifOp.getResults()); + if (classicalStateGeneration == savedClassicalStateGeneration) { + conditionCache = savedConditionCache; + } else { + conditionCache.clear(); + } } [[nodiscard]] Value extendRangeValue(Value value, Type targetType, @@ -2360,6 +2471,8 @@ class OpenQASMToQCEmitter { const auto slots = mutatedState(loop.body); const auto initialValues = stateValues(slots); const auto savedScalars = scalarValues; + const auto savedConditionCache = conditionCache; + const auto savedClassicalStateGeneration = classicalStateGeneration; if (loop.provenPositiveRange) { auto start = emitProvenIndexExpression(builder, loop.start); @@ -2377,6 +2490,7 @@ class OpenQASMToQCEmitter { } builder.setInsertionPointToEnd(body); scalarValues = savedScalars; + conditionCache.clear(); assignState(slots, forOp.getRegionIterArgs()); provenInductionValues[loop.inductionVariable] = forOp.getInductionVar(); scalarValues.at(loop.inductionVariable) = arith::IndexCastOp::create( @@ -2389,6 +2503,11 @@ class OpenQASMToQCEmitter { scalarValues = savedScalars; provenInductionValues.erase(loop.inductionVariable); assignState(slots, forOp.getResults()); + if (classicalStateGeneration == savedClassicalStateGeneration) { + conditionCache = savedConditionCache; + } else { + conditionCache.clear(); + } return; } @@ -2419,6 +2538,7 @@ class OpenQASMToQCEmitter { } builder.setInsertionPointToEnd(body); scalarValues = savedScalars; + conditionCache.clear(); assignState(slots, forOp.getRegionIterArgs()); auto counter = arith::IndexCastOp::create(builder, builder.getI64Type(), forOp.getInductionVar()); @@ -2434,6 +2554,11 @@ class OpenQASMToQCEmitter { } scalarValues = savedScalars; assignState(slots, forOp.getResults()); + if (classicalStateGeneration == savedClassicalStateGeneration) { + conditionCache = savedConditionCache; + } else { + conditionCache.clear(); + } return; } @@ -2468,6 +2593,7 @@ class OpenQASMToQCEmitter { builder.setInsertionPoint(nested.getInsertionBlock(), nested.getInsertionPoint()); scalarValues = savedScalars; + conditionCache.clear(); assignState(slots, arguments.drop_front()); scalarValues.at(loop.inductionVariable) = arith::TruncIOp::create( builder, builder.getI64Type(), arguments.front()); @@ -2481,6 +2607,11 @@ class OpenQASMToQCEmitter { }); scalarValues = savedScalars; assignState(slots, whileOp.getResults().drop_front()); + if (classicalStateGeneration == savedClassicalStateGeneration) { + conditionCache = savedConditionCache; + } else { + conditionCache.clear(); + } } void emitWhile(const frontend::WhileStatement& loop, @@ -2488,6 +2619,8 @@ class OpenQASMToQCEmitter { const auto slots = mutatedState(loop.body); const auto initialValues = stateValues(slots); const auto savedScalars = scalarValues; + const auto savedConditionCache = conditionCache; + const auto savedClassicalStateGeneration = classicalStateGeneration; auto whileOp = scf::WhileOp::create( builder, ValueRange(initialValues).getTypes(), initialValues, [&](OpBuilder& nested, Location, ValueRange arguments) { @@ -2495,6 +2628,7 @@ class OpenQASMToQCEmitter { builder.setInsertionPoint(nested.getInsertionBlock(), nested.getInsertionPoint()); scalarValues = savedScalars; + conditionCache.clear(); assignState(slots, arguments); auto condition = emitCondition(loop.condition, gateParameters, gateQubits); @@ -2505,6 +2639,7 @@ class OpenQASMToQCEmitter { builder.setInsertionPoint(nested.getInsertionBlock(), nested.getInsertionPoint()); scalarValues = savedScalars; + conditionCache.clear(); assignState(slots, arguments); for (const auto statement : loop.body) { emitStatement(statement, gateParameters, gateQubits); @@ -2513,6 +2648,11 @@ class OpenQASMToQCEmitter { }); scalarValues = savedScalars; assignState(slots, whileOp.getResults()); + if (classicalStateGeneration == savedClassicalStateGeneration) { + conditionCache = savedConditionCache; + } else { + conditionCache.clear(); + } } void emitSwitch(const frontend::SwitchStatement& switchStatement, @@ -2527,6 +2667,8 @@ class OpenQASMToQCEmitter { const auto slots = mutatedState(nestedStatements); const auto initialValues = stateValues(slots); const auto savedScalars = scalarValues; + const auto savedConditionCache = conditionCache; + const auto savedClassicalStateGeneration = classicalStateGeneration; auto control = emitExpression(builder, switchStatement.control, {}); auto selector = @@ -2540,6 +2682,7 @@ class OpenQASMToQCEmitter { auto& block = region.emplaceBlock(); builder.setInsertionPointToEnd(&block); scalarValues = savedScalars; + conditionCache.clear(); for (const auto statement : statements) { emitStatement(statement, gateParameters, gateQubits); } @@ -2554,6 +2697,11 @@ class OpenQASMToQCEmitter { emitBranch(switchOp.getDefaultRegion(), switchStatement.defaultStatements); scalarValues = savedScalars; assignState(slots, switchOp.getResults()); + if (classicalStateGeneration == savedClassicalStateGeneration) { + conditionCache = savedConditionCache; + } else { + conditionCache.clear(); + } } }; diff --git a/mlir/lib/Dialect/QC/Translation/TranslateQCToOpenQASM3.cpp b/mlir/lib/Dialect/QC/Translation/TranslateQCToOpenQASM3.cpp index 7b6815948b..ac5c338de2 100644 --- a/mlir/lib/Dialect/QC/Translation/TranslateQCToOpenQASM3.cpp +++ b/mlir/lib/Dialect/QC/Translation/TranslateQCToOpenQASM3.cpp @@ -89,6 +89,26 @@ struct GateCall { SmallVector qubits; }; +struct RegisterBitConstraint { + Value reg; + int64_t index; + bool expected; + Operation* observation; +}; + +struct RegisterEquality { + Value reg; + APInt expected; + SmallVector expressionIfs; + SmallVector expressionOperations; +}; + +struct RegisterEqualityCandidate { + SmallVector constraints; + SmallVector expressionIfs; + DenseSet expressionOperations; +}; + } // namespace [[nodiscard]] static bool isOpenQASMIdentifier(const StringRef value) { @@ -171,6 +191,11 @@ class OpenQASMEmitter { SmallVector resourceOrder; DenseMap valueNames; DenseSet returnedRegisters; + DenseMap registerEqualities; + DenseSet foldedConditionIfs; + DenseSet foldedRegisterExpressionOperations; + DenseMap fusedMeasurementStores; + DenseSet foldedMeasurementStores; SmallVector scalarOutputs; llvm::StringSet<> usedNames; llvm::StringSet<> fixedHelpers; @@ -303,13 +328,16 @@ class OpenQASMEmitter { if (auto alloc = dyn_cast(&operation)) { const auto type = alloc.getResult().getType(); const auto width = type.getWidth(); - if (width <= 0 || static_cast(width) > - MAX_CLASSICAL_BITS - numClassicalBits) { + if (width <= 0) { + return fail(alloc, "classical register width must be positive"); + } + const auto bitWidth = static_cast(width); + if (bitWidth > MAX_CLASSICAL_BITS - numClassicalBits) { return fail(alloc, "total classical register width exceeds the " "supported limit of " + Twine(MAX_CLASSICAL_BITS) + " bits"); } - numClassicalBits += static_cast(width); + numClassicalBits += bitWidth; const bool isOutput = returnedRegisters.contains(alloc.getResult()); const auto name = alloc->getAttrOfType( mqt::MQTDialect::RegisterNameAttrHelper::getNameStr()); @@ -355,6 +383,7 @@ class OpenQASMEmitter { "allocations"); } } + collectCompatibilityPatterns(); return success(); } @@ -383,6 +412,531 @@ class OpenQASMEmitter { return {}; } + [[nodiscard]] static std::optional + getBooleanConstant(Value value, RegisterEqualityCandidate& candidate) { + auto constant = value.getDefiningOp(); + auto integer = + constant ? dyn_cast(constant.getValue()) : IntegerAttr{}; + if (!integer || !integer.getType().isInteger(1)) { + return std::nullopt; + } + candidate.expressionOperations.insert(constant); + return !integer.getValue().isZero(); + } + + [[nodiscard]] bool + matchRegisterConjunction(Value value, bool positive, + RegisterEqualityCandidate& candidate) const { + SmallVector> pending{{value, positive}}; + DenseSet visitedPositive; + DenseSet visitedNegative; + + while (!pending.empty()) { + auto [current, expected] = pending.pop_back_val(); + auto& visited = expected ? visitedPositive : visitedNegative; + if (!visited.insert(current).second) { + return false; + } + + if (const auto constant = getBooleanConstant(current, candidate)) { + if (*constant != expected) { + return false; + } + continue; + } + + if (auto xorOp = current.getDefiningOp()) { + if (const auto lhs = getBooleanConstant(xorOp.getLhs(), candidate)) { + candidate.expressionOperations.insert(xorOp); + pending.emplace_back(xorOp.getRhs(), expected != *lhs); + continue; + } + if (const auto rhs = getBooleanConstant(xorOp.getRhs(), candidate)) { + candidate.expressionOperations.insert(xorOp); + pending.emplace_back(xorOp.getLhs(), expected != *rhs); + continue; + } + return false; + } + + if (auto load = current.getDefiningOp()) { + const auto index = getConstantInteger(load.getIndex()); + if (!index) { + return false; + } + candidate.expressionOperations.insert(load); + candidate.constraints.push_back({.reg = load.getReg(), + .index = *index, + .expected = expected, + .observation = load}); + continue; + } + + // QC cleanup may forward a static CBit load to the value written by its + // latest store. Recover the register provenance only through one + // unambiguous store; snapshot validation below still proves its ordering. + cbit::StoreOp storedBit; + for (Operation* user : current.getUsers()) { + auto store = dyn_cast(user); + if (!store || store.getValue() != current) { + continue; + } + if (storedBit) { + return false; + } + storedBit = store; + } + if (storedBit) { + const auto index = getConstantInteger(storedBit.getIndex()); + auto* definition = current.getDefiningOp(); + if (!index || definition == nullptr) { + return false; + } + candidate.expressionOperations.insert(definition); + candidate.constraints.push_back({.reg = storedBit.getReg(), + .index = *index, + .expected = expected, + .observation = storedBit}); + continue; + } + + if (!expected) { + return false; + } + auto ifOp = current.getDefiningOp(); + if (!ifOp || ifOp.getNumResults() != 1 || + !ifOp.getResult(0).getType().isInteger(1) || + ifOp.getElseRegion().empty()) { + return false; + } + auto thenYield = + dyn_cast(ifOp.getThenRegion().front().getTerminator()); + auto elseYield = + dyn_cast(ifOp.getElseRegion().front().getTerminator()); + if (!thenYield || !elseYield || thenYield.getNumOperands() != 1 || + elseYield.getNumOperands() != 1) { + return false; + } + + const auto thenConstant = + getBooleanConstant(thenYield.getOperand(0), candidate); + const auto elseConstant = + getBooleanConstant(elseYield.getOperand(0), candidate); + candidate.expressionOperations.insert(ifOp); + candidate.expressionIfs.push_back(ifOp); + if (elseConstant && !*elseConstant) { + pending.emplace_back(ifOp.getCondition(), true); + pending.emplace_back(thenYield.getOperand(0), true); + continue; + } + if (thenConstant && !*thenConstant) { + pending.emplace_back(ifOp.getCondition(), false); + pending.emplace_back(elseYield.getOperand(0), true); + continue; + } + return false; + } + return true; + } + + [[nodiscard]] static bool containsOnlyMatchedExpressionOperations( + const RegisterEqualityCandidate& candidate) { + return llvm::all_of(candidate.expressionIfs, [&](Operation* operation) { + auto ifOp = cast(operation); + return llvm::all_of(ifOp.getThenRegion().front().without_terminator(), + [&](Operation& nested) { + return candidate.expressionOperations.contains( + &nested); + }) && + llvm::all_of(ifOp.getElseRegion().front().without_terminator(), + [&](Operation& nested) { + return candidate.expressionOperations.contains( + &nested); + }); + }); + } + + [[nodiscard]] static Operation* getTopLevelObservation(Operation* operation, + Block* consumerBlock) { + while (operation != nullptr && operation->getBlock() != consumerBlock) { + operation = operation->getParentOp(); + } + return operation; + } + + [[nodiscard]] static bool + hasOnlyRepresentedRegisterWrites(scf::IfOp consumer, + const RegisterEqualityCandidate& candidate, + Value reg) { + auto alloc = reg.getDefiningOp(); + auto* consumerBlock = consumer->getBlock(); + if (!alloc || alloc->getBlock() != consumerBlock || + !alloc->isBeforeInBlock(consumer)) { + return false; + } + + // Zero-initialized bits may be omitted from the reconstructed equality, + // but every explicit write before the consumer must be observed by one of + // its matched constraints. Otherwise an omitted bit could be stale. + for (Operation* operation = alloc->getNextNode(); + operation != consumer.getOperation(); + operation = operation->getNextNode()) { + if (operation == nullptr) { + return false; + } + if (auto store = dyn_cast(operation); + store && store.getReg() == reg) { + const auto index = getConstantInteger(store.getIndex()); + if (!index || + !llvm::any_of(candidate.constraints, [&](const auto& constraint) { + if (constraint.reg != reg || constraint.index != *index) { + return false; + } + auto* observation = + getTopLevelObservation(constraint.observation, consumerBlock); + return observation == operation || + (observation != nullptr && + operation->isBeforeInBlock(observation)); + })) { + return false; + } + continue; + } + + const auto effects = getEffectsRecursively(operation); + if (!effects) { + if (referencesValueRecursively(operation, reg)) { + return false; + } + continue; + } + if (llvm::any_of(*effects, [&](const auto& effect) { + if (!isa( + effect.getEffect())) { + return false; + } + const auto affected = effect.getValue(); + return affected == reg || + (!affected && referencesValueRecursively(operation, reg)); + })) { + return false; + } + } + return true; + } + + [[nodiscard]] std::optional + matchRegisterEquality(scf::IfOp consumer) const { + RegisterEqualityCandidate candidate; + if (!matchRegisterConjunction(consumer.getCondition(), true, candidate) || + candidate.constraints.empty() || + !containsOnlyMatchedExpressionOperations(candidate)) { + return std::nullopt; + } + + auto reg = candidate.constraints.front().reg; + const auto resource = resources.find(reg); + if (resource == resources.end() || + resource->second.kind != ResourceKind::Bit || + !std::in_range(resource->second.width) || + candidate.constraints.size() > + static_cast(resource->second.width)) { + return std::nullopt; + } + SmallVector> expectedBits( + static_cast(resource->second.width)); + if (resource->second.initialization == cbit::Initialization::Zero) { + llvm::fill(expectedBits, false); + } + SmallVector constrained(expectedBits.size(), false); + for (const auto& constraint : candidate.constraints) { + if (constraint.reg != reg || constraint.index < 0 || + constraint.index >= resource->second.width) { + return std::nullopt; + } + const auto index = static_cast(constraint.index); + if (constrained[index]) { + return std::nullopt; + } + constrained[index] = true; + expectedBits[index] = constraint.expected; + } + if (llvm::any_of(expectedBits, + [](const auto& value) { return !value.has_value(); })) { + return std::nullopt; + } + if (resource->second.initialization == cbit::Initialization::Zero && + candidate.constraints.size() < expectedBits.size() && + !hasOnlyRepresentedRegisterWrites(consumer, candidate, reg)) { + return std::nullopt; + } + if (!preservesRegisterSnapshot(consumer, candidate, reg)) { + return std::nullopt; + } + + APInt expected(static_cast(resource->second.width), 0); + for (const auto [index, bit] : llvm::enumerate(expectedBits)) { + if (*bit) { + expected.setBit(static_cast(index)); + } + } + return RegisterEquality{.reg = reg, + .expected = std::move(expected), + .expressionIfs = std::move(candidate.expressionIfs), + .expressionOperations = SmallVector( + candidate.expressionOperations.begin(), + candidate.expressionOperations.end())}; + } + + [[nodiscard]] bool + isDeadRegisterExpression(const RegisterEqualityCandidate& candidate) const { + if (candidate.constraints.empty() || + !containsOnlyMatchedExpressionOperations(candidate)) { + return false; + } + return llvm::all_of(candidate.constraints, [&](const auto& constraint) { + const auto resource = resources.find(constraint.reg); + return resource != resources.end() && + resource->second.kind == ResourceKind::Bit && + constraint.index >= 0 && constraint.index < resource->second.width; + }); + } + + [[nodiscard]] static bool + hasExternalExpressionUse(const ArrayRef expressionIfs, + const DenseSet& matchedOperations) { + return llvm::any_of(expressionIfs, [&](Operation* expressionIf) { + return llvm::any_of(expressionIf->getResults(), [&](Value result) { + return llvm::any_of(result.getUsers(), [&](Operation* user) { + return !matchedOperations.contains(user); + }); + }); + }); + } + + [[nodiscard]] static Operation* + getTopLevelEvaluationOperation(Operation* operation, Block* consumerBlock, + const RegisterEqualityCandidate& candidate) { + while (operation->getBlock() != consumerBlock) { + operation = operation->getParentOp(); + if (operation == nullptr || + !candidate.expressionOperations.contains(operation)) { + return nullptr; + } + } + if (candidate.expressionOperations.contains(operation) || + llvm::any_of(candidate.constraints, [&](const auto& constraint) { + return constraint.observation == operation; + })) { + return operation; + } + return nullptr; + } + + [[nodiscard]] static bool referencesValueRecursively(Operation* operation, + Value value) { + bool referencesValue = false; + operation->walk([&](Operation* nested) { + if (!llvm::is_contained(nested->getOperands(), value)) { + return WalkResult::advance(); + } + referencesValue = true; + return WalkResult::interrupt(); + }); + return referencesValue; + } + + [[nodiscard]] static bool + preservesRegisterSnapshot(scf::IfOp consumer, + const RegisterEqualityCandidate& candidate, + Value reg) { + auto* conditionOperation = consumer.getCondition().getDefiningOp(); + auto* consumerBlock = consumer->getBlock(); + if (conditionOperation == nullptr || + conditionOperation->getBlock() != consumerBlock || + !candidate.expressionOperations.contains(conditionOperation)) { + return false; + } + + Operation* earliestEvaluation = conditionOperation; + for (const auto& constraint : candidate.constraints) { + auto* evaluation = getTopLevelEvaluationOperation( + constraint.observation, consumerBlock, candidate); + if (evaluation == nullptr || !evaluation->isBeforeInBlock(consumer)) { + return false; + } + if (evaluation != earliestEvaluation && + evaluation->isBeforeInBlock(earliestEvaluation)) { + earliestEvaluation = evaluation; + } + } + + for (Operation* operation = earliestEvaluation; + operation != consumer.getOperation(); + operation = operation->getNextNode()) { + if (operation == nullptr) { + return false; + } + if (candidate.expressionOperations.contains(operation)) { + continue; + } + if (llvm::any_of(candidate.constraints, [&](const auto& constraint) { + return constraint.observation == operation; + })) { + continue; + } + const auto effects = getEffectsRecursively(operation); + if (!effects) { + if (referencesValueRecursively(operation, reg)) { + return false; + } + continue; + } + if (llvm::any_of(*effects, [&](const auto& effect) { + if (!isa( + effect.getEffect())) { + return false; + } + const auto affected = effect.getValue(); + return affected == reg || + (!affected && referencesValueRecursively(operation, reg)); + })) { + return false; + } + } + return true; + } + + void collectCompatibilityPatterns() { + SmallVector> candidates; + function.walk([&](scf::IfOp ifOp) { + if (ifOp.getNumResults() != 0) { + return; + } + auto equality = matchRegisterEquality(ifOp); + if (!equality) { + return; + } + candidates.emplace_back(ifOp, std::move(*equality)); + }); + + SmallVector deadExpressionCandidates; + function.walk([&](scf::IfOp ifOp) { + if (ifOp.getNumResults() != 1 || !ifOp.getResult(0).use_empty()) { + return; + } + RegisterEqualityCandidate candidate; + if (matchRegisterConjunction(ifOp.getResult(0), true, candidate) && + isDeadRegisterExpression(candidate)) { + deadExpressionCandidates.push_back(std::move(candidate)); + } + }); + + SmallVector active(candidates.size(), true); + SmallVector activeDead(deadExpressionCandidates.size(), true); + bool changed = true; + while (changed) { + changed = false; + DenseSet matchedOperations; + for (const auto [index, candidate] : llvm::enumerate(candidates)) { + if (!active[index]) { + continue; + } + matchedOperations.insert(candidate.first.getOperation()); + matchedOperations.insert(candidate.second.expressionOperations.begin(), + candidate.second.expressionOperations.end()); + } + for (const auto [index, candidate] : + llvm::enumerate(deadExpressionCandidates)) { + if (activeDead[index]) { + matchedOperations.insert(candidate.expressionOperations.begin(), + candidate.expressionOperations.end()); + } + } + for (const auto [index, candidate] : llvm::enumerate(candidates)) { + if (!active[index]) { + continue; + } + if (hasExternalExpressionUse(candidate.second.expressionIfs, + matchedOperations)) { + active[index] = false; + changed = true; + } + } + for (const auto [index, candidate] : + llvm::enumerate(deadExpressionCandidates)) { + if (activeDead[index] && + hasExternalExpressionUse(candidate.expressionIfs, + matchedOperations)) { + activeDead[index] = false; + changed = true; + } + } + } + + for (auto [index, candidate] : llvm::enumerate(candidates)) { + if (!active[index]) { + continue; + } + for (auto* expressionIf : candidate.second.expressionIfs) { + foldedConditionIfs.insert(expressionIf); + } + foldedRegisterExpressionOperations.insert( + candidate.second.expressionOperations.begin(), + candidate.second.expressionOperations.end()); + registerEqualities.try_emplace(candidate.first.getOperation(), + std::move(candidate.second)); + } + for (const auto [index, candidate] : + llvm::enumerate(deadExpressionCandidates)) { + if (!activeDead[index]) { + continue; + } + foldedConditionIfs.insert(candidate.expressionIfs.begin(), + candidate.expressionIfs.end()); + } + + function.walk([&](qc::MeasureOp measurement) { + cbit::StoreOp store; + for (Operation* user : measurement.getResult().getUsers()) { + if (auto candidateStore = dyn_cast(user); + candidateStore && + candidateStore.getValue() == measurement.getResult()) { + if (store) { + return; + } + store = candidateStore; + continue; + } + auto consumer = dyn_cast(user); + if (!foldedRegisterExpressionOperations.contains(user) && + (!consumer || + !registerEqualities.contains(consumer.getOperation()))) { + return; + } + } + if (!store || store->getBlock() != measurement->getBlock()) { + return; + } + for (Operation* operation = measurement->getNextNode(); + operation != store.getOperation(); + operation = operation->getNextNode()) { + if (operation == nullptr || !isa(operation)) { + return; + } + } + fusedMeasurementStores.try_emplace(measurement, store); + foldedMeasurementStores.insert(store); + }); + } + + [[nodiscard]] std::string + emitRegisterEquality(const RegisterEquality& equality) const { + llvm::SmallString<64> expected; + equality.expected.toString(expected, 10, false); + return (Twine(resources.at(equality.reg).name) + " == " + expected).str(); + } + [[nodiscard]] LogicalResult emitDeclarations() { for (auto value : resourceOrder) { const auto& resource = resources.at(value); @@ -432,6 +986,9 @@ class OpenQASMEmitter { return success(); } if (isInlineExpressionOperation(operation)) { + if (foldedRegisterExpressionOperations.contains(&operation)) { + return success(); + } return validateInlineExpressionOperation(operation); } if (isa(&operation) || @@ -458,6 +1015,10 @@ class OpenQASMEmitter { return success(); } if (auto ifOp = dyn_cast(&operation)) { + if (ifOp.getNumResults() != 0 && + foldedConditionIfs.contains(&operation)) { + return success(); + } return emitIf(ifOp); } if (auto forOp = dyn_cast(&operation)) { @@ -821,20 +1382,13 @@ class OpenQASMEmitter { } [[nodiscard]] LogicalResult emitStore(cbit::StoreOp store) { + if (foldedMeasurementStores.contains(store)) { + return success(); + } auto target = emitBitReference(store.getReg(), store.getIndex()); if (failed(target)) { return failure(); } - if (auto measurement = store.getValue().getDefiningOp(); - measurement && measurement.getResult().hasOneUse() && - measurement->getNextNode() == store.getOperation()) { - auto qubit = emitQubit(measurement.getQubit()); - if (failed(qubit)) { - return failure(); - } - *output << *target << " = measure " << *qubit << ";\n"; - return success(); - } auto value = emitExpression(store.getValue()); if (failed(value)) { return failure(); @@ -844,17 +1398,21 @@ class OpenQASMEmitter { } [[nodiscard]] LogicalResult emitMeasurement(qc::MeasureOp measurement) { - if (measurement.getResult().hasOneUse()) { - if (auto store = dyn_cast( - *measurement.getResult().getUsers().begin()); - store && measurement->getNextNode() == store.getOperation()) { - return success(); - } - } auto qubit = emitQubit(measurement.getQubit()); if (failed(qubit)) { return failure(); } + if (const auto found = + fusedMeasurementStores.find(measurement.getOperation()); + found != fusedMeasurementStores.end()) { + auto store = cast(found->second); + auto target = emitBitReference(store.getReg(), store.getIndex()); + if (failed(target)) { + return failure(); + } + *output << *target << " = measure " << *qubit << ";\n"; + return success(); + } const auto name = uniqueName("b", nextBit); valueNames.try_emplace(measurement.getResult(), name); *output << "bit " << name << " = measure " << *qubit << ";\n"; @@ -865,11 +1423,18 @@ class OpenQASMEmitter { if (ifOp.getNumResults() != 0) { return fail(ifOp, "scf.if results are not supported"); } - auto condition = emitExpression(ifOp.getCondition()); - if (failed(condition)) { - return failure(); + std::string condition; + if (const auto found = registerEqualities.find(ifOp.getOperation()); + found != registerEqualities.end()) { + condition = emitRegisterEquality(found->second); + } else { + auto expression = emitExpression(ifOp.getCondition()); + if (failed(expression)) { + return failure(); + } + condition = std::move(*expression); } - *output << "if (" << *condition << ") {\n"; + *output << "if (" << condition << ") {\n"; output->indent(); if (failed(emitBlock(ifOp.getThenRegion().front()))) { return failure(); diff --git a/mlir/lib/Target/OpenQASM/OpenQASMSemantics.cpp b/mlir/lib/Target/OpenQASM/OpenQASMSemantics.cpp index 8de5d8f5ea..1a30349c60 100644 --- a/mlir/lib/Target/OpenQASM/OpenQASMSemantics.cpp +++ b/mlir/lib/Target/OpenQASM/OpenQASMSemantics.cpp @@ -3711,16 +3711,23 @@ class SemanticAnalyzer { 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 && + if (condition.kind == Expr::Kind::Equal && lhsSymbol != nullptr && + lhsSymbol->kind == SymbolKind::Register && program.registers[lhsSymbol->id].kind == RegisterKind::Bit && isConstantExpression(*condition.rhs)) { const auto& rhsSyntax = syntax.expressions[*condition.rhs]; MQT_OQ3_TRY_ASSIGN(bits, resolveBits({.location = lhsSyntax.location, .identifier = lhsSyntax.identifier})); - // OpenQASM 2 classical bits default to 0, so partially written - // registers are valid in `if (c == k)` (e.g. mid-circuit feedback). + // OpenQASM 2 classical bits default to zero. OpenQASM 3 register + // comparisons require every bit to be initialized. + if (!program.openQASM2) { + for (const auto& bit : bits) { + if (failed(ensureBitInitialized(bit, condition.location))) { + return failure(); + } + } + } llvm::APInt expectedBits; if (rhsSyntax.kind == Expr::Kind::Int && !rhsSyntax.wideInteger.empty()) { @@ -3740,7 +3747,7 @@ class SemanticAnalyzer { std::get(expected.value) < 0)) { return fail( condition.location, - "OpenQASM 2 register conditions require an unsigned integer"); + "classical register conditions require an unsigned integer"); } const auto expectedValue = expected.type == ScalarType::Uint diff --git a/mlir/unittests/Dialect/QC/Translation/test_openqasm3_emission.cpp b/mlir/unittests/Dialect/QC/Translation/test_openqasm3_emission.cpp index 5773799a5b..73b5087ce4 100644 --- a/mlir/unittests/Dialect/QC/Translation/test_openqasm3_emission.cpp +++ b/mlir/unittests/Dialect/QC/Translation/test_openqasm3_emission.cpp @@ -212,7 +212,7 @@ TEST(OpenQASM3EmissionTest, RenamesOutputsThatCollideWithStandardGates) { return %bits : !cbit.reg<1> } })mlir"; - DialectRegistry registry = emissionDialects(); + const DialectRegistry registry = emissionDialects(); MLIRContext context(registry); auto moduleOp = parseSourceString(source, &context); ASSERT_TRUE(moduleOp); @@ -268,6 +268,380 @@ switch (selector) { << *emitted; } +TEST(OpenQASM3EmissionTest, + CompactsQASM2RegisterConditionsAndMeasurementStores) { + struct Fixture { + size_t width{}; + llvm::StringLiteral expected{""}; + }; + constexpr std::array fixtures{ + Fixture{.width = 1, .expected = "0"}, + Fixture{.width = 1, .expected = "1"}, + Fixture{.width = 64, .expected = "9223372036854775808"}, + Fixture{.width = 151, + .expected = "1427247692705959881058285969449495136382746624"}, + Fixture{.width = 301, + .expected = "2037035976334486086268445688409378161051468393665" + "936250636140449354381299763336706183397376"}, + }; + + for (const auto& fixture : fixtures) { + SCOPED_TRACE(fixture.width); + const auto lastBit = fixture.width - 1; + const auto source = + "OPENQASM 2.0;\ninclude \"qelib1.inc\";\nqreg q[1];\ncreg c[" + + std::to_string(fixture.width) + "];\nmeasure q[0] -> c[" + + std::to_string(lastBit) + "];\nif(c==" + fixture.expected.str() + + ") x q[0];\n"; + MLIRContext context; + auto moduleOp = qc::translateQASM3ToQC(source, &context); + ASSERT_TRUE(moduleOp); + ASSERT_TRUE(succeeded(runQCCleanupPipeline(*moduleOp))); + auto emitted = qc::translateQCToOpenQASM3(*moduleOp); + + ASSERT_TRUE(succeeded(emitted)); + EXPECT_NE( + emitted->find("c[" + std::to_string(lastBit) + "] = measure q[0];"), + std::string::npos) + << *emitted; + EXPECT_EQ(emitted->find("bit _mqt_b"), std::string::npos) << *emitted; + EXPECT_NE(emitted->find("if (c == " + fixture.expected.str() + ")"), + std::string::npos) + << *emitted; + EXPECT_TRUE(oq3::frontend::analyzeOpenQASM( + *emitted, {.gatePolicy = oq3::frontend::GatePolicy::Strict})) + << *emitted; + EXPECT_TRUE(qc::translateQASM3ToQC(*emitted, &context)) << *emitted; + } +} + +TEST(OpenQASM3EmissionTest, + CompactsRepeatedRegisterConditionsAcrossQuantumOperations) { + constexpr llvm::StringLiteral source = R"mlir( +module { + func.func @main() -> !cbit.reg<1> { + %bits = cbit.alloc(#cbit.init) {mqt.register_name = "c"} + : !cbit.reg<1> + %control = qc.alloc : !qc.qubit + %target = qc.alloc : !qc.qubit + %zero = arith.constant 0 : index + %true = arith.constant true + %bit = cbit.load %bits[%zero] : !cbit.reg<1> + %firstCondition = arith.xori %bit, %true : i1 + scf.if %firstCondition { + qc.ctrl(%control) targets(%arg = %target) { + qc.x %arg : !qc.qubit + qc.yield + } : {!qc.qubit}, {!qc.qubit} + } + qc.barrier %control, %target : !qc.qubit, !qc.qubit + %secondCondition = arith.xori %bit, %true : i1 + scf.if %secondCondition { + qc.h %control : !qc.qubit + } + qc.dealloc %control : !qc.qubit + qc.dealloc %target : !qc.qubit + return %bits : !cbit.reg<1> + } +} +)mlir"; + const DialectRegistry registry = emissionDialects(); + MLIRContext context(registry); + auto moduleOp = parseSourceString(source, &context); + ASSERT_TRUE(moduleOp); + + auto emitted = qc::translateQCToOpenQASM3(*moduleOp); + + ASSERT_TRUE(succeeded(emitted)); + const auto firstCondition = emitted->find("if (c == 0)"); + ASSERT_NE(firstCondition, std::string::npos) << *emitted; + EXPECT_NE(emitted->find("if (c == 0)", firstCondition + 1), + std::string::npos); + EXPECT_NE(emitted->find("ctrl @ x "), std::string::npos); + EXPECT_NE(emitted->find("barrier "), std::string::npos); + EXPECT_NE(emitted->find("h "), std::string::npos); + EXPECT_TRUE(oq3::frontend::analyzeOpenQASM( + *emitted, {.gatePolicy = oq3::frontend::GatePolicy::Strict})) + << *emitted; +} + +TEST(OpenQASM3EmissionTest, CompactsSharedRegisterConditionExpressionTrees) { + constexpr llvm::StringLiteral source = R"mlir( +module { + func.func @main() -> !cbit.reg<2> { + %bits = cbit.alloc(#cbit.init) {mqt.register_name = "c"} + : !cbit.reg<2> + %qubit = qc.alloc : !qc.qubit + %zero = arith.constant 0 : index + %one = arith.constant 1 : index + %false = arith.constant false + %true = arith.constant true + %first = cbit.load %bits[%zero] : !cbit.reg<2> + %condition = scf.if %first -> i1 { + scf.yield %false : i1 + } else { + %second = cbit.load %bits[%one] : !cbit.reg<2> + %inverted = arith.xori %second, %true : i1 + scf.yield %inverted : i1 + } + scf.if %condition { + qc.x %qubit : !qc.qubit + } + qc.barrier %qubit : !qc.qubit + scf.if %condition { + qc.h %qubit : !qc.qubit + } + qc.dealloc %qubit : !qc.qubit + return %bits : !cbit.reg<2> + } +} +)mlir"; + const DialectRegistry registry = emissionDialects(); + MLIRContext context(registry); + auto moduleOp = parseSourceString(source, &context); + ASSERT_TRUE(moduleOp); + + auto emitted = qc::translateQCToOpenQASM3(*moduleOp); + + ASSERT_TRUE(succeeded(emitted)); + const auto firstCondition = emitted->find("if (c == 0)"); + ASSERT_NE(firstCondition, std::string::npos) << *emitted; + EXPECT_NE(emitted->find("if (c == 0)", firstCondition + 1), + std::string::npos); + EXPECT_TRUE(oq3::frontend::analyzeOpenQASM( + *emitted, {.gatePolicy = oq3::frontend::GatePolicy::Strict})) + << *emitted; +} + +TEST(OpenQASM3EmissionTest, ElidesDeadRegisterConditionExpressionTrees) { + constexpr llvm::StringLiteral source = R"mlir( +module { + func.func @main() -> !cbit.reg<2> { + %bits = cbit.alloc(#cbit.init) {mqt.register_name = "c"} + : !cbit.reg<2> + %zero = arith.constant 0 : index + %one = arith.constant 1 : index + %false = arith.constant false + %first = cbit.load %bits[%zero] : !cbit.reg<2> + %unused = scf.if %first -> i1 { + scf.yield %false : i1 + } else { + %second = cbit.load %bits[%one] : !cbit.reg<2> + scf.yield %second : i1 + } + return %bits : !cbit.reg<2> + } +} +)mlir"; + const DialectRegistry registry = emissionDialects(); + MLIRContext context(registry); + auto moduleOp = parseSourceString(source, &context); + ASSERT_TRUE(moduleOp); + + auto emitted = qc::translateQCToOpenQASM3(*moduleOp); + + ASSERT_TRUE(succeeded(emitted)); + EXPECT_EQ(emitted->find("if ("), std::string::npos) << *emitted; + EXPECT_TRUE(oq3::frontend::analyzeOpenQASM( + *emitted, {.gatePolicy = oq3::frontend::GatePolicy::Strict})) + << *emitted; +} + +TEST(OpenQASM3EmissionTest, DoesNotFuseMeasurementsWithMultipleUses) { + constexpr llvm::StringLiteral source = R"mlir( +module { + func.func @main() -> (!cbit.reg<1>, i1) { + %bits = cbit.alloc(#cbit.init) {mqt.register_name = "bits"} + : !cbit.reg<1> + %qubit = qc.alloc : !qc.qubit + %zero = arith.constant 0 : index + %measured = qc.measure %qubit : !qc.qubit -> i1 + cbit.store %measured, %bits[%zero] : !cbit.reg<1> + qc.dealloc %qubit : !qc.qubit + return %bits, %measured : !cbit.reg<1>, i1 + } +} +)mlir"; + const DialectRegistry registry = emissionDialects(); + MLIRContext context(registry); + auto moduleOp = parseSourceString(source, &context); + ASSERT_TRUE(moduleOp); + + auto emitted = qc::translateQCToOpenQASM3(*moduleOp); + + ASSERT_TRUE(succeeded(emitted)); + EXPECT_NE(emitted->find("bit _mqt_b0 = measure"), std::string::npos); + EXPECT_EQ(emitted->find("bits[0] = measure"), std::string::npos); + EXPECT_TRUE(oq3::frontend::analyzeOpenQASM( + *emitted, {.gatePolicy = oq3::frontend::GatePolicy::Strict})) + << *emitted; +} + +TEST(OpenQASM3EmissionTest, + DoesNotInferZeroAcrossAnUnrepresentedRegisterWrite) { + constexpr llvm::StringLiteral source = R"mlir( +module { + func.func @main() -> !cbit.reg<2> { + %bits = cbit.alloc(#cbit.init) {mqt.register_name = "bits"} + : !cbit.reg<2> + %firstQubit = qc.alloc : !qc.qubit + %secondQubit = qc.alloc : !qc.qubit + %zero = arith.constant 0 : index + %one = arith.constant 1 : index + %first = qc.measure %firstQubit : !qc.qubit -> i1 + cbit.store %first, %bits[%zero] : !cbit.reg<2> + %second = qc.measure %secondQubit : !qc.qubit -> i1 + cbit.store %second, %bits[%one] : !cbit.reg<2> + scf.if %second { + qc.x %firstQubit : !qc.qubit + } + qc.dealloc %firstQubit : !qc.qubit + qc.dealloc %secondQubit : !qc.qubit + return %bits : !cbit.reg<2> + } +} +)mlir"; + const DialectRegistry registry = emissionDialects(); + MLIRContext context(registry); + auto moduleOp = parseSourceString(source, &context); + ASSERT_TRUE(moduleOp); + + auto emitted = qc::translateQCToOpenQASM3(*moduleOp); + + ASSERT_TRUE(succeeded(emitted)); + EXPECT_EQ(emitted->find("if (bits == 2)"), std::string::npos) << *emitted; + EXPECT_NE(emitted->find("if (_mqt_b"), std::string::npos) << *emitted; + EXPECT_TRUE(oq3::frontend::analyzeOpenQASM( + *emitted, {.gatePolicy = oq3::frontend::GatePolicy::Strict})) + << *emitted; +} + +TEST(OpenQASM3EmissionTest, + RejectsRegisterEqualityWhenTheRegisterChangesBeforeUse) { + constexpr llvm::StringLiteral source = R"mlir( +module { + func.func @main() { + %bits = cbit.alloc(#cbit.init) : !cbit.reg<2> + %qubit = qc.alloc : !qc.qubit + %zero = arith.constant 0 : index + %one = arith.constant 1 : index + %false = arith.constant false + %true = arith.constant true + cbit.store %false, %bits[%zero] : !cbit.reg<2> + cbit.store %true, %bits[%one] : !cbit.reg<2> + %first = cbit.load %bits[%zero] : !cbit.reg<2> + %condition = scf.if %first -> i1 { + scf.yield %false : i1 + } else { + %second = cbit.load %bits[%one] : !cbit.reg<2> + scf.yield %second : i1 + } + cbit.store %false, %bits[%one] : !cbit.reg<2> + scf.if %condition { + qc.x %qubit : !qc.qubit + } + qc.dealloc %qubit : !qc.qubit + return + } +} +)mlir"; + const DialectRegistry registry = emissionDialects(); + MLIRContext context(registry); + auto moduleOp = parseSourceString(source, &context); + ASSERT_TRUE(moduleOp); + + EXPECT_TRUE(failed(qc::translateQCToOpenQASM3(*moduleOp))); +} + +TEST(OpenQASM3EmissionTest, DoesNotReuseRegisterEqualityAfterInterveningStore) { + constexpr llvm::StringLiteral source = R"mlir( +module { + func.func @main() -> !cbit.reg<1> { + %bits = cbit.alloc(#cbit.init) {mqt.register_name = "bits"} + : !cbit.reg<1> + %qubit = qc.alloc : !qc.qubit + %zero = arith.constant 0 : index + %false = arith.constant false + %measured = qc.measure %qubit : !qc.qubit -> i1 + cbit.store %measured, %bits[%zero] : !cbit.reg<1> + scf.if %measured { + qc.x %qubit : !qc.qubit + } + cbit.store %false, %bits[%zero] : !cbit.reg<1> + scf.if %measured { + qc.h %qubit : !qc.qubit + } + qc.dealloc %qubit : !qc.qubit + return %bits : !cbit.reg<1> + } +} +)mlir"; + const DialectRegistry registry = emissionDialects(); + MLIRContext context(registry); + auto moduleOp = parseSourceString(source, &context); + ASSERT_TRUE(moduleOp); + + auto emitted = qc::translateQCToOpenQASM3(*moduleOp); + + ASSERT_TRUE(succeeded(emitted)); + const auto firstCondition = emitted->find("if (bits == 1)"); + const auto overwrite = emitted->find("bits[0] = false;"); + const auto secondCondition = emitted->find("if (_mqt_b0)"); + ASSERT_NE(firstCondition, std::string::npos) << *emitted; + ASSERT_NE(overwrite, std::string::npos) << *emitted; + ASSERT_NE(secondCondition, std::string::npos) << *emitted; + EXPECT_LT(firstCondition, overwrite); + EXPECT_LT(overwrite, secondCondition); + EXPECT_EQ(emitted->find("if (bits == 1)", firstCondition + 1), + std::string::npos) + << *emitted; + EXPECT_NE(emitted->find("bit _mqt_b0 = measure"), std::string::npos) + << *emitted; + EXPECT_TRUE(oq3::frontend::analyzeOpenQASM( + *emitted, {.gatePolicy = oq3::frontend::GatePolicy::Strict})) + << *emitted; +} + +TEST(OpenQASM3EmissionTest, DoesNotUseStoreAfterConsumerForRegisterEquality) { + constexpr llvm::StringLiteral source = R"mlir( +module { + func.func @main() -> !cbit.reg<1> { + %bits = cbit.alloc(#cbit.init) {mqt.register_name = "bits"} + : !cbit.reg<1> + %qubit = qc.alloc : !qc.qubit + %zero = arith.constant 0 : index + %measured = qc.measure %qubit : !qc.qubit -> i1 + scf.if %measured { + qc.x %qubit : !qc.qubit + } + cbit.store %measured, %bits[%zero] : !cbit.reg<1> + qc.dealloc %qubit : !qc.qubit + return %bits : !cbit.reg<1> + } +} +)mlir"; + const DialectRegistry registry = emissionDialects(); + MLIRContext context(registry); + auto moduleOp = parseSourceString(source, &context); + ASSERT_TRUE(moduleOp); + + auto emitted = qc::translateQCToOpenQASM3(*moduleOp); + + ASSERT_TRUE(succeeded(emitted)); + const auto measurement = emitted->find("bit _mqt_b0 = measure"); + const auto condition = emitted->find("if (_mqt_b0)"); + const auto store = emitted->find("bits[0] = _mqt_b0;"); + ASSERT_NE(measurement, std::string::npos) << *emitted; + ASSERT_NE(condition, std::string::npos) << *emitted; + ASSERT_NE(store, std::string::npos) << *emitted; + EXPECT_LT(measurement, condition); + EXPECT_LT(condition, store); + EXPECT_EQ(emitted->find("if (bits == 1)"), std::string::npos) << *emitted; + EXPECT_TRUE(oq3::frontend::analyzeOpenQASM( + *emitted, {.gatePolicy = oq3::frontend::GatePolicy::Strict})) + << *emitted; +} + TEST(OpenQASM3EmissionTest, EmitsNativeIndexSwitch) { constexpr llvm::StringLiteral source = R"mlir( module { @@ -938,6 +1312,50 @@ TEST(OpenQASM3EmissionTest, RejectsUnsupportedSubsetConcerns) { return %value : i64 } })mlir"}, + Fixture{.name = "partial-register-equality", .source = R"mlir(module { + func.func @main() { + %bits = cbit.alloc(#cbit.init) : !cbit.reg<3> + %qubit = qc.alloc : !qc.qubit + %zero = arith.constant 0 : index + %one = arith.constant 1 : index + %false = arith.constant false + %first = cbit.load %bits[%zero] : !cbit.reg<3> + %second = cbit.load %bits[%one] : !cbit.reg<3> + %condition = scf.if %first -> i1 { + scf.yield %second : i1 + } else { + scf.yield %false : i1 + } + scf.if %condition { + qc.x %qubit : !qc.qubit + } + qc.dealloc %qubit : !qc.qubit + return + } + })mlir"}, + Fixture{.name = "side-effecting-register-equality", + .source = R"mlir(module { + func.func @main() { + %bits = cbit.alloc(#cbit.init) : !cbit.reg<2> + %qubit = qc.alloc : !qc.qubit + %zero = arith.constant 0 : index + %one = arith.constant 1 : index + %false = arith.constant false + %first = cbit.load %bits[%zero] : !cbit.reg<2> + %second = cbit.load %bits[%one] : !cbit.reg<2> + %condition = scf.if %first -> i1 { + cbit.store %false, %bits[%one] : !cbit.reg<2> + scf.yield %second : i1 + } else { + scf.yield %false : i1 + } + scf.if %condition { + qc.x %qubit : !qc.qubit + } + qc.dealloc %qubit : !qc.qubit + return + } + })mlir"}, Fixture{.name = "for-iterated-state", .source = R"mlir(module { func.func @main() -> i64 { %zero = arith.constant 0 : index diff --git a/mlir/unittests/Target/OpenQASM/test_openqasm_emitter.cpp b/mlir/unittests/Target/OpenQASM/test_openqasm_emitter.cpp index 871c70245d..129bef964f 100644 --- a/mlir/unittests/Target/OpenQASM/test_openqasm_emitter.cpp +++ b/mlir/unittests/Target/OpenQASM/test_openqasm_emitter.cpp @@ -1264,6 +1264,104 @@ if (c == 1) x q[0]; EXPECT_EQ(conditionals, 2); } +TEST(OpenQASMTargetTest, + SharesOpenQASM2RegisterConditionsUntilClassicalMutation) { + constexpr llvm::StringLiteral source = R"qasm( +OPENQASM 2.0; +include "qelib1.inc"; +qreg q[2]; +creg c[2]; +measure q[0] -> c[0]; +if (c == 1) x q[1]; +if (c == 1) h q[1]; +measure q[1] -> c[1]; +if (c == 1) z q[0]; +)qasm"; + + MLIRContext context; + auto moduleOp = qc::translateQASM3ToQC(source, &context); + ASSERT_TRUE(moduleOp); + ASSERT_TRUE(succeeded(verify(*moduleOp))); + + SmallVector branchConditions; + size_t expressionConditionals = 0; + size_t classicalLoads = 0; + moduleOp->walk([&](scf::IfOp conditional) { + if (conditional.getNumResults() == 0) { + branchConditions.push_back(conditional.getCondition()); + } else { + ++expressionConditionals; + } + }); + moduleOp->walk([&](cbit::LoadOp) { ++classicalLoads; }); + + ASSERT_EQ(branchConditions.size(), 3); + EXPECT_EQ(branchConditions[0], branchConditions[1]); + EXPECT_NE(branchConditions[1], branchConditions[2]); + EXPECT_EQ(expressionConditionals, 4); + EXPECT_EQ(classicalLoads, 4); +} + +TEST(OpenQASMTargetTest, DoesNotReuseLoopLocalConditionAfterPositiveRangeLoop) { + constexpr llvm::StringLiteral source = R"qasm( +OPENQASM 3.1; +include "stdgates.inc"; +qubit[2] q; +bit c = measure q[0]; +for int i in [0:1] { + if (c) { x q[1]; } +} +if (c) { h q[1]; } +)qasm"; + + MLIRContext context; + auto moduleOp = qc::translateQASM3ToQC(source, &context); + ASSERT_TRUE(moduleOp); + ASSERT_TRUE(succeeded(verify(*moduleOp))); + + SmallVector branchConditions; + moduleOp->walk([&](scf::IfOp conditional) { + if (conditional.getNumResults() == 0) { + branchConditions.push_back(conditional.getCondition()); + } + }); + + ASSERT_EQ(branchConditions.size(), 2); + EXPECT_NE(branchConditions[0], branchConditions[1]); +} + +TEST(OpenQASMTargetTest, InvalidatesPositiveRangeLoopConditionsAcrossMutation) { + constexpr llvm::StringLiteral source = R"qasm( +OPENQASM 3.1; +include "stdgates.inc"; +qubit[2] q; +bit c = measure q[0]; +if (c) { x q[1]; } +for int i in [0:1] { + if (c) { h q[1]; } + c = false; +} +if (c) { z q[1]; } +)qasm"; + + MLIRContext context; + auto moduleOp = qc::translateQASM3ToQC(source, &context); + ASSERT_TRUE(moduleOp); + ASSERT_TRUE(succeeded(verify(*moduleOp))); + + SmallVector branchConditions; + moduleOp->walk([&](scf::IfOp conditional) { + if (conditional.getNumResults() == 0) { + branchConditions.push_back(conditional.getCondition()); + } + }); + + ASSERT_EQ(branchConditions.size(), 3); + EXPECT_NE(branchConditions[0], branchConditions[1]); + EXPECT_NE(branchConditions[1], branchConditions[2]); + EXPECT_NE(branchConditions[0], branchConditions[2]); +} + TEST(OpenQASMTargetTest, ZeroInitializesUnmeasuredOpenQASM2Registers) { constexpr llvm::StringLiteral source = R"qasm( OPENQASM 2.0; diff --git a/mlir/unittests/Target/OpenQASM/test_openqasm_semantics.cpp b/mlir/unittests/Target/OpenQASM/test_openqasm_semantics.cpp index c1011b47cc..9716285b46 100644 --- a/mlir/unittests/Target/OpenQASM/test_openqasm_semantics.cpp +++ b/mlir/unittests/Target/OpenQASM/test_openqasm_semantics.cpp @@ -1655,6 +1655,48 @@ if(c==1180591620717411303433) x q[0]; })); } +TEST(OpenQASMFrontendTest, AcceptsInitializedRegisterConditionInOpenQASM3) { + std::string source = R"qasm( +OPENQASM 3.1; +include "stdgates.inc"; +qubit q; +bit[80] c; +)qasm"; + for (size_t index = 0; index < 80; ++index) { + source += "c[" + std::to_string(index) + "] = false;\n"; + } + source += R"qasm( +c[70] = measure q; +if(c==1180591620717411303424) { x q; } +)qasm"; + + auto analyzed = oq3::frontend::analyzeOpenQASM(source); + + ASSERT_TRUE(analyzed) << analyzed.diagnostics.front().message; + EXPECT_TRUE(llvm::any_of(analyzed.program->conditions, [](const auto& c) { + return c.kind == oq3::frontend::ConditionKind::Bit && c.bit.index == 70; + })); +} + +TEST(OpenQASMFrontendTest, RejectsUninitializedRegisterConditionInOpenQASM3) { + constexpr llvm::StringLiteral source = R"qasm( +OPENQASM 3.1; +include "stdgates.inc"; +qubit q; +bit[2] c; +c[0] = measure q; +if(c==1) { x q; } +)qasm"; + + auto analyzed = oq3::frontend::analyzeOpenQASM(source); + + ASSERT_FALSE(analyzed); + ASSERT_FALSE(analyzed.diagnostics.empty()); + EXPECT_NE( + analyzed.diagnostics.front().message.find("has not been initialized"), + std::string::npos); +} + TEST(OpenQASMFrontendTest, AcceptsWideIntegerLiteralWithDigitSeparatorsInOpenQASM2If) { // Same value as above, spelled with grammar-legal digit separators. From 9064d0288d4011ef7f58cbf508fedbdace764b0c Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Wed, 2 Sep 2026 15:22:42 +0200 Subject: [PATCH 02/13] =?UTF-8?q?=F0=9F=90=9B=20Preserve=20reused=20measur?= =?UTF-8?q?ement=20expressions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reject measurement-store fusion when a folded expression also reaches an unmatched consumer. Emit constant boolean XORs canonically so named measurement bits remain valid conditions. Assisted-by: Codex Signed-off-by: Simon Hofmann --- .../QC/Translation/TranslateQCToOpenQASM3.cpp | 58 +++++++++++++++++-- .../Translation/test_openqasm3_emission.cpp | 45 ++++++++++++++ 2 files changed, 99 insertions(+), 4 deletions(-) diff --git a/mlir/lib/Dialect/QC/Translation/TranslateQCToOpenQASM3.cpp b/mlir/lib/Dialect/QC/Translation/TranslateQCToOpenQASM3.cpp index ac5c338de2..2de5180c8b 100644 --- a/mlir/lib/Dialect/QC/Translation/TranslateQCToOpenQASM3.cpp +++ b/mlir/lib/Dialect/QC/Translation/TranslateQCToOpenQASM3.cpp @@ -703,7 +703,7 @@ class OpenQASMEmitter { } [[nodiscard]] static bool - hasExternalExpressionUse(const ArrayRef expressionIfs, + hasExternalExpressionUse(ArrayRef expressionIfs, const DenseSet& matchedOperations) { return llvm::any_of(expressionIfs, [&](Operation* expressionIf) { return llvm::any_of(expressionIf->getResults(), [&](Value result) { @@ -714,6 +714,32 @@ class OpenQASMEmitter { }); } + [[nodiscard]] bool hasExternalFoldedUse(Operation* root) const { + SmallVector pending{root}; + DenseSet visited; + while (!pending.empty()) { + auto* operation = pending.pop_back_val(); + if (!visited.insert(operation).second) { + continue; + } + for (Value result : operation->getResults()) { + for (Operation* user : result.getUsers()) { + if (registerEqualities.contains(user)) { + continue; + } + Operation* continuation = + isa(user) ? user->getParentOp() : user; + if (!foldedRegisterExpressionOperations.contains(continuation) && + !foldedConditionIfs.contains(continuation)) { + return true; + } + pending.push_back(continuation); + } + } + } + return false; + } + [[nodiscard]] static Operation* getTopLevelEvaluationOperation(Operation* operation, Block* consumerBlock, const RegisterEqualityCandidate& candidate) { @@ -908,10 +934,15 @@ class OpenQASMEmitter { store = candidateStore; continue; } + if (foldedRegisterExpressionOperations.contains(user)) { + if (hasExternalFoldedUse(user)) { + return; + } + continue; + } auto consumer = dyn_cast(user); - if (!foldedRegisterExpressionOperations.contains(user) && - (!consumer || - !registerEqualities.contains(consumer.getOperation()))) { + if (!consumer || + !registerEqualities.contains(consumer.getOperation())) { return; } } @@ -1174,6 +1205,25 @@ class OpenQASMEmitter { return emitBinary(cmp.getLhs(), predicate, cmp.getRhs()); } const auto name = operation->getName().getStringRef(); + if (auto xorOp = dyn_cast(operation); + xorOp && value.getType().isInteger(1)) { + auto constant = getConstantInteger(xorOp.getLhs()); + Value operand = xorOp.getRhs(); + if (!constant) { + constant = getConstantInteger(xorOp.getRhs()); + operand = xorOp.getLhs(); + } + if (constant) { + auto expression = emitExpression(operand); + if (failed(expression)) { + return failure(); + } + if (*constant == 0) { + return std::move(*expression); + } + return (Twine("(!") + *expression + ")").str(); + } + } if (name == "arith.remf") { auto lhs = emitExpression(operation->getOperand(0)); if (failed(lhs)) { diff --git a/mlir/unittests/Dialect/QC/Translation/test_openqasm3_emission.cpp b/mlir/unittests/Dialect/QC/Translation/test_openqasm3_emission.cpp index 73b5087ce4..d2725abe8e 100644 --- a/mlir/unittests/Dialect/QC/Translation/test_openqasm3_emission.cpp +++ b/mlir/unittests/Dialect/QC/Translation/test_openqasm3_emission.cpp @@ -602,6 +602,51 @@ module { << *emitted; } +TEST(OpenQASM3EmissionTest, DoesNotFuseMeasurementsUsedByUnfoldedExpressions) { + constexpr llvm::StringLiteral source = R"mlir( +module { + func.func @main() -> !cbit.reg<1> { + %bits = cbit.alloc(#cbit.init) {mqt.register_name = "bits"} + : !cbit.reg<1> + %qubit = qc.alloc : !qc.qubit + %zero = arith.constant 0 : index + %false = arith.constant false + %true = arith.constant true + %measured = qc.measure %qubit : !qc.qubit -> i1 + cbit.store %measured, %bits[%zero] : !cbit.reg<1> + %negated = arith.xori %measured, %true : i1 + scf.if %negated { + qc.x %qubit : !qc.qubit + } + cbit.store %false, %bits[%zero] : !cbit.reg<1> + scf.if %negated { + qc.h %qubit : !qc.qubit + } + qc.dealloc %qubit : !qc.qubit + return %bits : !cbit.reg<1> + } +} +)mlir"; + const DialectRegistry registry = emissionDialects(); + MLIRContext context(registry); + auto moduleOp = parseSourceString(source, &context); + ASSERT_TRUE(moduleOp); + + auto emitted = qc::translateQCToOpenQASM3(*moduleOp); + + ASSERT_TRUE(succeeded(emitted)); + EXPECT_NE(emitted->find("bit _mqt_b0 = measure"), std::string::npos) + << *emitted; + EXPECT_NE(emitted->find("bits[0] = _mqt_b0;"), std::string::npos) << *emitted; + EXPECT_EQ(emitted->find("bits[0] = measure"), std::string::npos) << *emitted; + EXPECT_NE(emitted->find("if (bits == 0)"), std::string::npos) << *emitted; + EXPECT_NE(emitted->find("if ((!_mqt_b0))"), std::string::npos) << *emitted; + auto analyzed = oq3::frontend::analyzeOpenQASM( + *emitted, {.gatePolicy = oq3::frontend::GatePolicy::Strict}); + ASSERT_TRUE(analyzed) << analyzed.diagnostics.front().message << '\n' + << *emitted; +} + TEST(OpenQASM3EmissionTest, DoesNotUseStoreAfterConsumerForRegisterEquality) { constexpr llvm::StringLiteral source = R"mlir( module { From 6545b95dec707c3eaad6a25f43fc35e7b4b63ab3 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Wed, 2 Sep 2026 15:55:29 +0200 Subject: [PATCH 03/13] =?UTF-8?q?=F0=9F=A7=AA=20Cover=20mixed=20register?= =?UTF-8?q?=20constraints?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use different expected values for the two constrained bits so swapped mappings cannot pass the shared-expression regression. Assisted-by: Codex Signed-off-by: Simon Hofmann --- .../Dialect/QC/Translation/test_openqasm3_emission.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/mlir/unittests/Dialect/QC/Translation/test_openqasm3_emission.cpp b/mlir/unittests/Dialect/QC/Translation/test_openqasm3_emission.cpp index d2725abe8e..72b65a1121 100644 --- a/mlir/unittests/Dialect/QC/Translation/test_openqasm3_emission.cpp +++ b/mlir/unittests/Dialect/QC/Translation/test_openqasm3_emission.cpp @@ -378,11 +378,11 @@ module { %true = arith.constant true %first = cbit.load %bits[%zero] : !cbit.reg<2> %condition = scf.if %first -> i1 { - scf.yield %false : i1 - } else { %second = cbit.load %bits[%one] : !cbit.reg<2> %inverted = arith.xori %second, %true : i1 scf.yield %inverted : i1 + } else { + scf.yield %false : i1 } scf.if %condition { qc.x %qubit : !qc.qubit @@ -404,9 +404,9 @@ module { auto emitted = qc::translateQCToOpenQASM3(*moduleOp); ASSERT_TRUE(succeeded(emitted)); - const auto firstCondition = emitted->find("if (c == 0)"); + const auto firstCondition = emitted->find("if (c == 1)"); ASSERT_NE(firstCondition, std::string::npos) << *emitted; - EXPECT_NE(emitted->find("if (c == 0)", firstCondition + 1), + EXPECT_NE(emitted->find("if (c == 1)", firstCondition + 1), std::string::npos); EXPECT_TRUE(oq3::frontend::analyzeOpenQASM( *emitted, {.gatePolicy = oq3::frontend::GatePolicy::Strict})) From 466bf84aa52de21538d4a0eddf324232d148705f Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Wed, 2 Sep 2026 17:24:31 +0000 Subject: [PATCH 04/13] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Simplify=20OpenQASM?= =?UTF-8?q?=20register=20comparisons?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Represent whole-register conditions as a first-class cbit.cmp operation instead of reconstructing and sharing per-bit Boolean trees. Support the six unsigned OpenQASM 3 predicates, keep OpenQASM 2 equality, and lower the operation through supported backends. Export comparisons as Qiskit Uint expressions; the existing importer already handles them. Assisted-by: Codex Signed-off-by: Lukas Burgholzer --- bindings/mlir/qiskit/QiskitExport.cpp | 94 ++- docs/mlir/OpenQASM.md | 51 +- mlir/include/mlir/Dialect/CBit/IR/CBitOps.h | 6 + mlir/include/mlir/Dialect/CBit/IR/CBitOps.td | 33 + mlir/include/mlir/Target/OpenQASM/Frontend.h | 4 + .../Conversion/CBitToMemRef/CBitToMemRef.cpp | 24 +- mlir/lib/Conversion/QCOToJeff/QCOToJeff.cpp | 44 +- .../QCToQIR/QIRAdaptive/QCToQIRAdaptive.cpp | 73 +- .../QCToQIR/QIRBase/QCToQIRBase.cpp | 13 +- mlir/lib/Dialect/CBit/IR/CBitOps.cpp | 99 +++ .../QC/Translation/OpenQASMToQCEmitter.cpp | 206 ++---- .../QC/Translation/TranslateQCToOpenQASM3.cpp | 697 ++---------------- .../lib/Dialect/QCO/Utils/DDFunctionality.cpp | 43 ++ .../lib/Target/OpenQASM/OpenQASMSemantics.cpp | 56 +- .../CBitToMemRef/test_cbit_to_memref.cpp | 23 + .../test_qc_to_qir_adaptive.cpp | 26 + .../Dialect/CBit/IR/test_cbit_ir.cpp | 24 + .../Translation/test_openqasm3_emission.cpp | 456 +----------- .../QCO/Utils/test_dd_functionality.cpp | 29 + .../Target/OpenQASM/test_openqasm_emitter.cpp | 111 +-- .../OpenQASM/test_openqasm_semantics.cpp | 72 +- test/python/test_mlir_qiskit_translation.py | 28 +- 22 files changed, 736 insertions(+), 1476 deletions(-) diff --git a/bindings/mlir/qiskit/QiskitExport.cpp b/bindings/mlir/qiskit/QiskitExport.cpp index 4c2aacd1d2..4fb6ebb2e5 100644 --- a/bindings/mlir/qiskit/QiskitExport.cpp +++ b/bindings/mlir/qiskit/QiskitExport.cpp @@ -977,6 +977,53 @@ static void setExpressionType(Expression& expression, const mlir::Type type) { return checkedAdd(info->second.base, checked, "classical-bit"); } +[[nodiscard]] static Register classicalRegister(mlir::Value value, + const ExportState& state) { + const auto info = state.classicalRegisterInfo.find(value); + if (info == state.classicalRegisterInfo.end() || info->second.size == 0U || + info->second.size > 64U) { + throw std::runtime_error( + "Qiskit register comparisons require between 1 and 64 bits"); + } + if (info->second.initialization != mlir::cbit::Initialization::Zero) { + const auto written = state.unconditionalWrites.find(value); + if (written == state.unconditionalWrites.end() || + written->second.size() != info->second.size) { + throw std::runtime_error( + "Qiskit register comparison reads undefined classical bits"); + } + } + Register result; + result.bits.resize(info->second.size); + std::iota(result.bits.begin(), result.bits.end(), info->second.base); + for (const auto& candidate : state.classicalRegisters) { + if (candidate.bits == result.bits) { + result.name = candidate.name; + break; + } + } + return result; +} + +[[nodiscard]] static BinaryOperation +comparisonOperation(const mlir::cbit::ComparisonPredicate predicate) { + switch (predicate) { + case mlir::cbit::ComparisonPredicate::Equal: + return BinaryOperation::Equal; + case mlir::cbit::ComparisonPredicate::NotEqual: + return BinaryOperation::NotEqual; + case mlir::cbit::ComparisonPredicate::Less: + return BinaryOperation::Less; + case mlir::cbit::ComparisonPredicate::LessEqual: + return BinaryOperation::LessEqual; + case mlir::cbit::ComparisonPredicate::Greater: + return BinaryOperation::Greater; + case mlir::cbit::ComparisonPredicate::GreaterEqual: + return BinaryOperation::GreaterEqual; + } + llvm_unreachable("unknown CBit comparison predicate"); +} + [[noreturn]] static void throwClassicalExpressionSizeError() { throw std::runtime_error( "QC classical expression exceeds the size limit of 4096 nodes"); @@ -1073,6 +1120,31 @@ exportExpressionImpl(mlir::Value value, ExportState& state, state.expressionOperations.insert(operation); return result; } + if (auto comparison = llvm::dyn_cast(operation)) { + const auto width = comparison.getRhs().getBitWidth(); + if (width > 64U) { + throw std::runtime_error( + "Qiskit register comparisons support at most 64 bits"); + } + countExpressionNode(nodeCount); + auto left = std::make_unique(); + left->kind = ExpressionKind::ClassicalRegister; + left->type = ClassicalType::Uint; + left->width = width; + left->reg = classicalRegister(comparison.getReg(), state); + countExpressionNode(nodeCount); + auto right = std::make_unique(); + right->kind = ExpressionKind::Value; + right->type = ClassicalType::Uint; + right->width = width; + right->uintValue = comparison.getRhs().getZExtValue(); + result->kind = ExpressionKind::Binary; + result->binaryOperation = comparisonOperation(comparison.getPredicate()); + result->left = std::move(left); + result->right = std::move(right); + state.expressionOperations.insert(operation); + return result; + } if (auto ifOp = llvm::dyn_cast(operation)) { if (ifOp.getNumResults() != 1U || !value.getType().isInteger(1) || ifOp.getElseRegion().empty()) { @@ -1393,7 +1465,7 @@ static void acceptPackedRegister(PackedRegister& packed, ExportState& state) { static void validateClassicalSnapshot(mlir::Value expression, mlir::Operation& consumer) { llvm::DenseSet visited; - llvm::SmallVector loads; + llvm::SmallVector> reads; llvm::SmallVector worklist{expression}; while (!worklist.empty()) { auto value = worklist.pop_back_val(); @@ -1408,7 +1480,11 @@ static void validateClassicalSnapshot(mlir::Value expression, continue; } if (auto load = llvm::dyn_cast(operation)) { - loads.push_back(load); + reads.emplace_back(load, load.getReg()); + continue; + } + if (auto comparison = llvm::dyn_cast(operation)) { + reads.emplace_back(comparison, comparison.getReg()); continue; } if (auto ifOp = llvm::dyn_cast(operation)) { @@ -1422,9 +1498,9 @@ static void validateClassicalSnapshot(mlir::Value expression, } worklist.append(operation->operand_begin(), operation->operand_end()); } - for (auto load : loads) { - mlir::Operation* anchor = load; - auto* anchorBlock = load->getBlock(); + for (auto [read, reg] : reads) { + mlir::Operation* anchor = read; + auto* anchorBlock = read->getBlock(); while (anchorBlock != consumer.getBlock()) { auto* parent = anchorBlock->getParentOp(); auto parentIf = llvm::dyn_cast_if_present(parent); @@ -1448,13 +1524,13 @@ static void validateClassicalSnapshot(mlir::Value expression, "Qiskit control-flow expression does not dominate its consumer"); } if (auto store = llvm::dyn_cast(operation); - store && store.getReg() == load.getReg()) { + store && store.getReg() == reg) { throw std::runtime_error( "Qiskit control-flow export cannot preserve a stale classical " "snapshot"); } if (operation->getNumRegions() != 0U && - storesToValueRecursively(*operation, load.getReg())) { + storesToValueRecursively(*operation, reg)) { throw std::runtime_error( "Qiskit control-flow export cannot preserve a classical " "snapshot across nested control flow"); @@ -1866,6 +1942,10 @@ collectSwitch(mlir::scf::IndexSwitchOp switchOp, ExportState& state, deferredExpressions.push_back(&operation); continue; } + if (llvm::isa(operation)) { + deferredExpressions.push_back(&operation); + continue; + } if (auto dealloc = llvm::dyn_cast(operation)) { if (topLevel && state.quantumBases.contains(dealloc.getMemref())) { continue; diff --git a/docs/mlir/OpenQASM.md b/docs/mlir/OpenQASM.md index e196786b3e..62bc657e93 100644 --- a/docs/mlir/OpenQASM.md +++ b/docs/mlir/OpenQASM.md @@ -47,15 +47,15 @@ mqt-cc --input-format=qasm program.txt | Dynamic indexing | Classical bit indices can be dynamic and receive runtime bounds checks. A nonconstant qubit index must be a proven affine expression as described below. | | Unsupported language areas | Subroutines, `extern`, calibration and timing constructs, input declarations, arbitrary arrays, `break`, and `continue` are diagnosed. | -Bit-register equality accepts unsigned integer constants of arbitrary width. -OpenQASM 3 requires every compared bit to be initialized; OpenQASM 2 retains its -standard zero-initialized register behavior. - Syntax and semantic diagnostics retain source locations and include stacks. Runtime integer preconditions and classical-index bounds are represented explicitly in QC. This safety machinery is supported by the normal compiler and QIR paths, but it is intentionally outside the export subset described below. +OpenQASM 3 supports all six unsigned comparisons with a bit register on the left +and an integer constant on the right. OpenQASM 2 retains its equality-only +register condition. + Fixed-width angles are a compile-time input feature. An omitted angle width resolves to 52 bits. Both `const angle[N]` and initialized `angle[N]` declarations are accepted as write-once values. Initializers and angle casts @@ -87,10 +87,12 @@ do not index qubits keep their runtime behavior. Bit registers use `!cbit.reg` in QC. OpenQASM 2 initializes each register to zero. OpenQASM 3 leaves each register undefined until a statement writes it. -Explicit outputs and implicit global outputs are returned by the entry function; -internal CBit allocations are not outputs. Other scalar outputs use builtin MLIR -scalar types. A scalar `qubit` lowers to `qc.alloc`, while `qubit[1]` remains a -one-element qubit register. +Whole-register comparisons lower to `cbit.cmp` and keep their unsigned integer +meaning without expanding into per-bit expression trees. Explicit outputs and +implicit global outputs are returned by the entry function; internal CBit +allocations are not outputs. Other scalar outputs use builtin MLIR scalar types. +A scalar `qubit` lowers to `qc.alloc`, while `qubit[1]` remains a one-element +qubit register. ## Export OpenQASM @@ -149,25 +151,16 @@ QCO optimization pipeline, converts back to QC, and then exports. Calling {code}`mlir::QCProgram::toOpenQASM3` applies the QC cleanup pipeline but bypasses that QCO optimization round trip. -For measurement-conditioned programs, target compilation can expose the -frontend's bit-register equality as a classical SSA expression. The exporter -recognizes that exact unchanged expression, fuses eligible direct measurement -stores, and emits one register comparison. OpenQASM 3 input follows the same -path when every compared bit is initialized. The constant is not limited to a -machine integer, so this compatibility path also supports registers wider than -64 bits. Other expression shapes continue through the normal support checks -below. - ### Export and round-trip support -| QC or MLIR concept | Export support | -| ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Qubits and classical bits | Logical and physical qubits, scalar qubit allocations, static rank-one qubit memrefs, and CBit registers. Qubit memory indices must resolve statically. CBit indices can be dynamic. | -| Quantum operations | Measurement, reset, barrier, deallocation, global phase, and QC unitary operations. The exporter uses standard gates where available; for example, `sxdg` becomes `inv @ sx` and `u2` uses the standard compatibility alias. | -| Gate modifiers | Nested `ctrl`, `inv`, and `pow`. A multi-operation modifier body with target qubits becomes a private generated gate. | -| Scalar values | `i1`, `i64`, `f64`, and internal `index` values, including arithmetic, comparisons, Boolean operations, value-preserving casts, and supported math functions. | -| Structured control | Result-free `scf.if` and `scf.index_switch`, constant-range `scf.for` without iterated state, and zero-state expression-based `scf.while`. Complete register-equality conditions produced by the frontend are reconstructed as direct comparisons, including registers wider than 64 bits. Index switches use native `switch`, `case`, and `default` statements. | -| Results | Multiple scalar and bit-register outputs using the canonical type and naming rules below. | +| QC or MLIR concept | Export support | +| ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Qubits and classical bits | Logical and physical qubits, scalar qubit allocations, static rank-one qubit memrefs, and CBit registers. Qubit memory indices must resolve statically. CBit indices can be dynamic. | +| Quantum operations | Measurement, reset, barrier, deallocation, global phase, and QC unitary operations. The exporter uses standard gates where available; for example, `sxdg` becomes `inv @ sx` and `u2` uses the standard compatibility alias. | +| Gate modifiers | Nested `ctrl`, `inv`, and `pow`. A multi-operation modifier body with target qubits becomes a private generated gate. | +| Scalar values | `i1`, `i64`, `f64`, and internal `index` values, including arithmetic, comparisons, Boolean operations, value-preserving casts, and supported math functions. | +| Structured control | Result-free `scf.if` and `scf.index_switch`, constant-range `scf.for` without iterated state, and zero-state expression-based `scf.while`. Index switches use native `switch`, `case`, and `default` statements. | +| Results | Multiple scalar and bit-register outputs using the canonical type and naming rules below. | The exporter writes an OpenQASM 3.1 version declaration and includes `stdgates.inc`. Gates in MQT Core's compatibility catalog, such as `r`, `rzz`, @@ -200,6 +193,7 @@ Unsigned constants therefore normalize to `int`. Operations whose signedness affects their meaning, such as unsigned division, comparison, or conversion, are rejected instead of being approximated. Integer sign extension and truncation are also rejected because OpenQASM scalar casts have different value semantics. +Direct `cbit.cmp` operations retain their unsigned register semantics. Emitted scalar casts use standard OpenQASM conversion syntax. The MQT Core frontend does not yet parse that syntax, so cast-containing output is outside @@ -212,12 +206,7 @@ arbitrary CFGs, multi-block SCF regions, dynamic qubit indices or ranges, general memrefs, unsupported integer widths, packed bit-vector operations, unknown operations, and non-unitary content inside modifier regions. CBit loads, stores, and dynamic indices are supported. SCF results, loop-carried values, -nonempty `scf.yield`, and `arith.select` are outside the export subset. The sole -result-bearing SCF exception is an unchanged bit-register equality over CBit -storage produced by the frontend. The exporter emits this compatibility form as -one register comparison and rejects mixed, dynamically indexed, or modified -register conditions. A zero-initialized register can omit bits that no -intervening write changed. An uninitialized register must constrain every bit. +nonempty `scf.yield`, and `arith.select` are outside the export subset. Multi-operation modifier bodies must have a target qubit and cannot capture additional qubits from an enclosing scope. diff --git a/mlir/include/mlir/Dialect/CBit/IR/CBitOps.h b/mlir/include/mlir/Dialect/CBit/IR/CBitOps.h index a4e0bfe508..c0686ad430 100644 --- a/mlir/include/mlir/Dialect/CBit/IR/CBitOps.h +++ b/mlir/include/mlir/Dialect/CBit/IR/CBitOps.h @@ -14,6 +14,7 @@ #include "mlir/Dialect/CBit/IR/CBitDialect.h" #include +#include #include #include @@ -29,4 +30,9 @@ namespace mlir::cbit { void validateStaticRegisterIndex(Value reg, const std::variant& index); +/// Builds an equivalent comparison from individual register bits. +Value buildComparison(OpBuilder& builder, Location location, + ComparisonPredicate predicate, const llvm::APInt& rhs, + llvm::function_ref loadBit); + } // namespace mlir::cbit diff --git a/mlir/include/mlir/Dialect/CBit/IR/CBitOps.td b/mlir/include/mlir/Dialect/CBit/IR/CBitOps.td index 3d0450f78a..3b530490a7 100644 --- a/mlir/include/mlir/Dialect/CBit/IR/CBitOps.td +++ b/mlir/include/mlir/Dialect/CBit/IR/CBitOps.td @@ -33,6 +33,17 @@ def CBit_InitializationAttr let assemblyFormat = "`<` $value `>`"; } +def CBit_ComparisonPredicate + : I64EnumAttr<"ComparisonPredicate", "unsigned comparison predicate", + [I64EnumAttrCase<"Equal", 0, "eq">, + I64EnumAttrCase<"NotEqual", 1, "ne">, + I64EnumAttrCase<"Less", 2, "ult">, + I64EnumAttrCase<"LessEqual", 3, "ule">, + I64EnumAttrCase<"Greater", 4, "ugt">, + I64EnumAttrCase<"GreaterEqual", 5, "uge">]> { + let cppNamespace = "::mlir::cbit"; +} + def CBit_RegisterType : TypeDef { let mnemonic = "reg"; let summary = "A static classical-bit register"; @@ -96,6 +107,28 @@ def LoadOp : CBitOp<"load"> { let hasVerifier = 1; } +def CompareOp : CBitOp<"cmp"> { + let summary = "Compare a classical-bit register with an integer"; + let description = [{ + Compares the complete register with an unsigned integer of the same width. + The predicate must be `eq`, `ne`, `ult`, `ule`, `ugt`, or `uge`. + + Example: + ```mlir + %matches = cbit.cmp eq, %c, 1 : i2 : !cbit.reg<2> + ``` + }]; + + let arguments = (ins CBit_ComparisonPredicate:$predicate, + Arg:$reg, APIntAttr:$rhs); + let results = (outs I1:$result); + let assemblyFormat = [{ + $predicate `,` $reg `,` $rhs attr-dict `:` qualified(type($reg)) + }]; + let hasCanonicalizer = 1; + let hasVerifier = 1; +} + def StoreOp : CBitOp<"store"> { let summary = "Store a classical bit"; let description = [{ diff --git a/mlir/include/mlir/Target/OpenQASM/Frontend.h b/mlir/include/mlir/Target/OpenQASM/Frontend.h index 4bedb090a5..6b071cadb1 100644 --- a/mlir/include/mlir/Target/OpenQASM/Frontend.h +++ b/mlir/include/mlir/Target/OpenQASM/Frontend.h @@ -10,6 +10,7 @@ #pragma once +#include #include #include @@ -208,6 +209,7 @@ enum class ConditionKind : uint8_t { Not, And, Or, + RegisterComparison, Comparison, }; @@ -220,6 +222,8 @@ struct ConditionExpression { QubitReference measurement; ConditionId lhs = 0; ConditionId rhs = 0; + RegisterId reg = 0; + llvm::APInt expected = llvm::APInt(1, 0); ExpressionId comparisonLhs = 0; ExpressionId comparisonRhs = 0; ComparisonKind comparison = ComparisonKind::Equal; diff --git a/mlir/lib/Conversion/CBitToMemRef/CBitToMemRef.cpp b/mlir/lib/Conversion/CBitToMemRef/CBitToMemRef.cpp index dd4ffa5982..3840441811 100644 --- a/mlir/lib/Conversion/CBitToMemRef/CBitToMemRef.cpp +++ b/mlir/lib/Conversion/CBitToMemRef/CBitToMemRef.cpp @@ -88,6 +88,25 @@ struct ConvertLoadOp final : OpConversionPattern { } }; +struct ConvertCompareOp final : OpConversionPattern { + using OpConversionPattern::OpConversionPattern; + + LogicalResult + matchAndRewrite(cbit::CompareOp op, OpAdaptor adaptor, + ConversionPatternRewriter& rewriter) const override { + auto result = cbit::buildComparison( + rewriter, op.getLoc(), op.getPredicate(), op.getRhs(), + [&](const int64_t index) -> Value { + auto indexValue = + arith::ConstantIndexOp::create(rewriter, op.getLoc(), index); + return memref::LoadOp::create(rewriter, op.getLoc(), adaptor.getReg(), + ValueRange{indexValue}); + }); + rewriter.replaceOp(op, result); + return success(); + } +}; + struct ConvertStoreOp final : OpConversionPattern { using OpConversionPattern::OpConversionPattern; @@ -123,8 +142,9 @@ struct ConvertCBitToMemRef final target.addDynamicallyLegalOp( [&](Operation* op) { return typeConverter.isLegal(op); }); - patterns.add(typeConverter, - context); + patterns + .add( + typeConverter, context); populateFunctionOpInterfaceTypeConversionPattern( patterns, typeConverter); populateReturnOpTypeConversionPattern(patterns, typeConverter); diff --git a/mlir/lib/Conversion/QCOToJeff/QCOToJeff.cpp b/mlir/lib/Conversion/QCOToJeff/QCOToJeff.cpp index 505c54cd5c..97a77e0aab 100644 --- a/mlir/lib/Conversion/QCOToJeff/QCOToJeff.cpp +++ b/mlir/lib/Conversion/QCOToJeff/QCOToJeff.cpp @@ -631,6 +631,35 @@ struct ConvertCBitLoadOpToJeff final } }; +/// Converts a CBit register comparison to jeff array reads and Boolean logic. +struct ConvertCBitCompareOpToJeff final + : StatefulOpConversionPattern { + using StatefulOpConversionPattern::StatefulOpConversionPattern; + + LogicalResult + matchAndRewrite(cbit::CompareOp op, OpAdaptor /*adaptor*/, + ConversionPatternRewriter& rewriter) const override { + auto& state = getState().cbitState; + auto reg = state.resolveRegisterUse(op, op.getReg()); + auto array = state.getCurrentValue(reg, op); + if (!array) { + return rewriter.notifyMatchFailure(op, "unknown classical register"); + } + array = rewriter.getRemappedValue(array); + auto result = cbit::buildComparison( + rewriter, op.getLoc(), op.getPredicate(), op.getRhs(), + [&](const int64_t index) -> Value { + auto position = jeff::IntConst32Op::create( + rewriter, op.getLoc(), + rewriter.getI32IntegerAttr(static_cast(index))); + return jeff::IntArrayGetIndexOp::create( + rewriter, op.getLoc(), rewriter.getI1Type(), array, position); + }); + rewriter.replaceOp(op, result); + return success(); + } +}; + /** * @brief Converts qtensor.alloc to jeff.qureg_alloc * @@ -1879,13 +1908,14 @@ struct QCOToJeff final : impl::QCOToJeffBase { // Register operation conversion patterns jeff::populateNativeToJeffConversionPatterns(patterns); - patterns.add(typeConverter, context, &state); + patterns.add( + typeConverter, context, &state); using JK = JeffKind; using PP = PPRPaulis; diff --git a/mlir/lib/Conversion/QCToQIR/QIRAdaptive/QCToQIRAdaptive.cpp b/mlir/lib/Conversion/QCToQIR/QIRAdaptive/QCToQIRAdaptive.cpp index 809d724706..7cd7eaea2f 100644 --- a/mlir/lib/Conversion/QCToQIR/QIRAdaptive/QCToQIRAdaptive.cpp +++ b/mlir/lib/Conversion/QCToQIR/QIRAdaptive/QCToQIRAdaptive.cpp @@ -187,33 +187,60 @@ struct ConvertCBitAllocOp final : StatefulOpConversionPattern { } }; +} // namespace + +static Value loadCBit(Operation* op, Value reg, Value index, + ConversionPatternRewriter& rewriter, + LoweringState& state) { + const auto ptrType = LLVM::LLVMPointerType::get(rewriter.getContext()); + if (!state.resultArrays.contains(reg)) { + auto elementptr = + LLVM::GEPOp::create(rewriter, op->getLoc(), ptrType, + rewriter.getI1Type(), reg, ValueRange{index}); + return LLVM::LoadOp::create(rewriter, op->getLoc(), rewriter.getI1Type(), + elementptr); + } + auto elementptr = LLVM::GEPOp::create(rewriter, op->getLoc(), ptrType, + ptrType, reg, ValueRange{index}); + auto result = + LLVM::LoadOp::create(rewriter, op->getLoc(), ptrType, elementptr); + auto fnSig = LLVM::LLVMFunctionType::get(rewriter.getI1Type(), {ptrType}); + auto fnDec = + getOrCreateFunctionDeclaration(rewriter, op, QIR_READ_RESULT, fnSig); + return LLVM::CallOp::create(rewriter, op->getLoc(), fnDec, result.getResult()) + .getResult(); +} + +namespace { + struct ConvertCBitLoadOp final : StatefulOpConversionPattern { using StatefulOpConversionPattern::StatefulOpConversionPattern; LogicalResult matchAndRewrite(cbit::LoadOp op, OpAdaptor adaptor, ConversionPatternRewriter& rewriter) const override { - auto& state = getState(); - const auto ptrType = LLVM::LLVMPointerType::get(getContext()); - if (!state.resultArrays.contains(adaptor.getReg())) { - auto elementptr = LLVM::GEPOp::create( - rewriter, op.getLoc(), ptrType, rewriter.getI1Type(), - adaptor.getReg(), ValueRange{adaptor.getIndex()}); - rewriter.replaceOpWithNewOp(op, rewriter.getI1Type(), - elementptr); - return success(); - } - auto elementptr = - LLVM::GEPOp::create(rewriter, op.getLoc(), ptrType, ptrType, - adaptor.getReg(), ValueRange{adaptor.getIndex()}); - auto result = - LLVM::LoadOp::create(rewriter, op.getLoc(), ptrType, elementptr); - auto fnSig = LLVM::LLVMFunctionType::get(rewriter.getI1Type(), {ptrType}); - auto fnDec = - getOrCreateFunctionDeclaration(rewriter, op, QIR_READ_RESULT, fnSig); - auto readResult = - LLVM::CallOp::create(rewriter, op.getLoc(), fnDec, result.getResult()); - rewriter.replaceOp(op, readResult.getResult()); + rewriter.replaceOp(op, loadCBit(op, adaptor.getReg(), adaptor.getIndex(), + rewriter, getState())); + return success(); + } +}; + +struct ConvertCBitCompareOp final + : StatefulOpConversionPattern { + using StatefulOpConversionPattern::StatefulOpConversionPattern; + + LogicalResult + matchAndRewrite(cbit::CompareOp op, OpAdaptor adaptor, + ConversionPatternRewriter& rewriter) const override { + auto result = cbit::buildComparison( + rewriter, op.getLoc(), op.getPredicate(), op.getRhs(), + [&](const int64_t index) -> Value { + auto indexValue = LLVM::ConstantOp::create( + rewriter, op.getLoc(), rewriter.getI64Type(), index); + return loadCBit(op, adaptor.getReg(), indexValue, rewriter, + getState()); + }); + rewriter.replaceOp(op, result); return success(); } }; @@ -542,8 +569,8 @@ static void populateQCToQIRAdaptivePatterns(RewritePatternSet& patterns, MLIRContext* ctx, LoweringState& state) { populateQCToQIRPatterns(patterns, typeConverter, ctx, state); - patterns.add(typeConverter, ctx, &state); diff --git a/mlir/lib/Conversion/QCToQIR/QIRBase/QCToQIRBase.cpp b/mlir/lib/Conversion/QCToQIR/QIRBase/QCToQIRBase.cpp index a7f07325d0..e0ce975244 100644 --- a/mlir/lib/Conversion/QCToQIR/QIRBase/QCToQIRBase.cpp +++ b/mlir/lib/Conversion/QCToQIR/QIRBase/QCToQIRBase.cpp @@ -142,6 +142,17 @@ struct RejectCBitLoadOp final : OpConversionPattern { } }; +struct RejectCBitCompareOp final : OpConversionPattern { + using OpConversionPattern::OpConversionPattern; + + LogicalResult + matchAndRewrite(cbit::CompareOp op, OpAdaptor /*adaptor*/, + ConversionPatternRewriter& /*rewriter*/) const override { + return op.emitError( + "QIR Base Profile does not support classical-register comparisons"); + } +}; + struct ConvertMemRefAllocOp final : StatefulOpConversionPattern { using StatefulOpConversionPattern::StatefulOpConversionPattern; @@ -335,7 +346,7 @@ static void populateQCToQIRBasePatterns(RewritePatternSet& patterns, patterns.add(typeConverter, ctx, &state); - patterns.add(typeConverter, ctx); + patterns.add(typeConverter, ctx); } namespace { diff --git a/mlir/lib/Dialect/CBit/IR/CBitOps.cpp b/mlir/lib/Dialect/CBit/IR/CBitOps.cpp index 2268c55d2a..bb9a39f7db 100644 --- a/mlir/lib/Dialect/CBit/IR/CBitOps.cpp +++ b/mlir/lib/Dialect/CBit/IR/CBitOps.cpp @@ -168,17 +168,116 @@ struct ForwardKnownLoad final : OpRewritePattern { return success(); } }; + +struct FoldUntouchedZeroComparison final : OpRewritePattern { + using OpRewritePattern::OpRewritePattern; + + LogicalResult matchAndRewrite(CompareOp compare, + PatternRewriter& rewriter) const override { + auto alloc = compare.getReg().getDefiningOp(); + if (!alloc || alloc.getInitialization() != Initialization::Zero || + alloc->getBlock() != compare->getBlock()) { + return failure(); + } + /// ponytail: folds only untouched zero registers; track stores if broader + /// constant folding becomes performance-critical. + for (auto* operation = alloc->getNextNode(); operation != compare; + operation = operation->getNextNode()) { + if (operation == nullptr || operation->getNumRegions() != 0 || + (!isa(operation) && + llvm::is_contained(operation->getOperands(), compare.getReg()))) { + return failure(); + } + } + const auto zero = compare.getRhs().isZero(); + const auto result = [&] { + switch (compare.getPredicate()) { + case ComparisonPredicate::Equal: + case ComparisonPredicate::GreaterEqual: + return zero; + case ComparisonPredicate::NotEqual: + case ComparisonPredicate::Less: + return !zero; + case ComparisonPredicate::LessEqual: + return true; + case ComparisonPredicate::Greater: + return false; + } + llvm_unreachable("unknown CBit comparison predicate"); + }(); + rewriter.replaceOpWithNewOp(compare, result, 1); + return success(); + } +}; + } // namespace LogicalResult LoadOp::verify() { return verifyIndex(getOperation(), getReg(), getIndex()); } +LogicalResult CompareOp::verify() { + const auto width = static_cast(getReg().getType().getWidth()); + if (getRhs().getBitWidth() != width) { + return emitOpError("expected integer width must match register width"); + } + return success(); +} + +Value mlir::cbit::buildComparison( + OpBuilder& builder, const Location location, + const ComparisonPredicate predicate, const llvm::APInt& rhs, + const llvm::function_ref loadBit) { + auto one = arith::ConstantIntOp::create(builder, location, 1, 1); + Value equal = one; + Value less; + if (predicate != ComparisonPredicate::Equal && + predicate != ComparisonPredicate::NotEqual) { + less = arith::ConstantIntOp::create(builder, location, 0, 1); + } + for (int64_t index = static_cast(rhs.getBitWidth()) - 1; index >= 0; + --index) { + auto bit = loadBit(index); + Value matches = bit; + if (!rhs[static_cast(index)]) { + matches = arith::XOrIOp::create(builder, location, bit, one); + } else if (less) { + auto lower = arith::XOrIOp::create(builder, location, bit, one); + auto firstDifference = + arith::AndIOp::create(builder, location, equal, lower); + less = arith::OrIOp::create(builder, location, less, firstDifference); + } + equal = arith::AndIOp::create(builder, location, equal, matches); + } + switch (predicate) { + case ComparisonPredicate::Equal: + return equal; + case ComparisonPredicate::NotEqual: + return arith::XOrIOp::create(builder, location, equal, one); + case ComparisonPredicate::Less: + return less; + case ComparisonPredicate::LessEqual: + return arith::OrIOp::create(builder, location, less, equal); + case ComparisonPredicate::Greater: { + auto lessOrEqual = arith::OrIOp::create(builder, location, less, equal); + return arith::XOrIOp::create(builder, location, lessOrEqual, one); + } + case ComparisonPredicate::GreaterEqual: + return arith::XOrIOp::create(builder, location, less, one); + } + llvm_unreachable("unknown CBit comparison predicate"); +} + void LoadOp::getCanonicalizationPatterns(RewritePatternSet& results, MLIRContext* context) { results.add(context); } +void CompareOp::getCanonicalizationPatterns(RewritePatternSet& results, + MLIRContext* context) { + results.add(context); +} + LogicalResult StoreOp::verify() { return verifyIndex(getOperation(), getReg(), getIndex()); } diff --git a/mlir/lib/Dialect/QC/Translation/OpenQASMToQCEmitter.cpp b/mlir/lib/Dialect/QC/Translation/OpenQASMToQCEmitter.cpp index 9287fdd270..966f8c59e6 100644 --- a/mlir/lib/Dialect/QC/Translation/OpenQASMToQCEmitter.cpp +++ b/mlir/lib/Dialect/QC/Translation/OpenQASMToQCEmitter.cpp @@ -50,7 +50,6 @@ #include #include #include -#include #include #include #include @@ -120,8 +119,7 @@ class OpenQASMToQCEmitter { classicalRegisters(program.registers.size()), scalarValues(program.scalars.size()), expressionEmissionCosts(program.expressions.size()), - bitVectorExpressionEmissionCosts(program.bitVectorExpressions.size()), - canonicalConditions(program.conditions.size()) { + bitVectorExpressionEmissionCosts(program.bitVectorExpressions.size()) { context .loadDialect emit() { @@ -195,11 +192,6 @@ class OpenQASMToQCEmitter { llvm::DenseMap provenInductionValues; mutable std::vector> expressionEmissionCosts; mutable std::vector> bitVectorExpressionEmissionCosts; - // Canonical IDs exist only for pure, static classical-bit condition trees. - // Cached values are valid only in the current region and memory snapshot. - std::vector> canonicalConditions; - DenseMap conditionCache; - uint64_t classicalStateGeneration = 0; DenseMap structuredGateCapabilities; llvm::StringMap customGateIndex; @@ -207,72 +199,6 @@ class OpenQASMToQCEmitter { using StateSlot = frontend::ScalarId; - struct ConditionCacheKey { - frontend::ConditionKind kind = frontend::ConditionKind::Literal; - bool literal = false; - uint32_t first = 0; - uint32_t second = 0; - uint64_t index = 0; - - [[nodiscard]] bool operator<(const ConditionCacheKey& other) const { - return std::tie(kind, literal, first, second, index) < - std::tie(other.kind, other.literal, other.first, other.second, - other.index); - } - }; - - void initializeCanonicalConditions() { - std::map representatives; - for (const auto [id, condition] : llvm::enumerate(program.conditions)) { - std::optional key; - switch (condition.kind) { - case frontend::ConditionKind::Literal: - key = ConditionCacheKey{.kind = condition.kind, - .literal = condition.literal}; - break; - case frontend::ConditionKind::Bit: - if (!condition.bit.dynamicIndex) { - key = ConditionCacheKey{.kind = condition.kind, - .first = condition.bit.reg, - .index = condition.bit.index}; - } - break; - case frontend::ConditionKind::Not: - if (canonicalConditions.at(condition.lhs)) { - key = ConditionCacheKey{.kind = condition.kind, - .first = - *canonicalConditions.at(condition.lhs)}; - } - break; - case frontend::ConditionKind::And: - case frontend::ConditionKind::Or: - if (canonicalConditions.at(condition.lhs) && - canonicalConditions.at(condition.rhs)) { - key = ConditionCacheKey{ - .kind = condition.kind, - .first = *canonicalConditions.at(condition.lhs), - .second = *canonicalConditions.at(condition.rhs)}; - } - break; - case frontend::ConditionKind::Scalar: - case frontend::ConditionKind::Measurement: - case frontend::ConditionKind::Comparison: - break; - } - if (!key) { - continue; - } - const auto conditionId = static_cast(id); - const auto it = representatives.try_emplace(*key, conditionId).first; - canonicalConditions[conditionId] = it->second; - } - } - - void invalidateConditionCache() { - conditionCache.clear(); - ++classicalStateGeneration; - } - [[nodiscard]] Location getLocation(const frontend::SourceLocation& source) const { return getOpenQASMLocation(source, context); @@ -646,6 +572,9 @@ class OpenQASMToQCEmitter { return chargeDynamicBitRead(condition.bit, multiplicity, projectedEmission, source); } + if (condition.kind == frontend::ConditionKind::RegisterComparison) { + return chargeScaledEmission(1, multiplicity, projectedEmission, source); + } if (condition.kind == frontend::ConditionKind::Comparison) { return chargeExpressionEmission(condition.comparisonLhs, multiplicity, projectedEmission, source) && @@ -1945,6 +1874,25 @@ class OpenQASMToQCEmitter { return builder.loadClassicalBit(reg, registerIndex.getResult()); } + [[nodiscard]] static cbit::ComparisonPredicate + registerPredicate(const frontend::ComparisonKind comparison) { + switch (comparison) { + case frontend::ComparisonKind::Equal: + return cbit::ComparisonPredicate::Equal; + case frontend::ComparisonKind::NotEqual: + return cbit::ComparisonPredicate::NotEqual; + case frontend::ComparisonKind::Less: + return cbit::ComparisonPredicate::Less; + case frontend::ComparisonKind::LessEqual: + return cbit::ComparisonPredicate::LessEqual; + case frontend::ComparisonKind::Greater: + return cbit::ComparisonPredicate::Greater; + case frontend::ComparisonKind::GreaterEqual: + return cbit::ComparisonPredicate::GreaterEqual; + } + llvm_unreachable("unknown register comparison"); + } + [[nodiscard]] Value emitComparison(const frontend::ConditionExpression& condition, ValueRange gateParameters) { @@ -2001,9 +1949,9 @@ class OpenQASMToQCEmitter { return arith::CmpIOp::create(builder, predicate, lhs, rhs); } - [[nodiscard]] Value emitConditionUncached(const frontend::ConditionId id, - ValueRange gateParameters, - ValueRange gateQubits) { + [[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: @@ -2016,14 +1964,22 @@ class OpenQASMToQCEmitter { return emitQubitOperation( condition.measurement, gateQubits, [&](Value qubit) { return builder.measure(qubit); }); + case frontend::ConditionKind::RegisterComparison: { + auto reg = classicalRegisters.at(condition.reg); + assert(reg && "semantic analysis must declare bit registers before use"); + auto rhs = builder.getIntegerAttr( + builder.getIntegerType(condition.expected.getBitWidth()), + condition.expected); + return cbit::CompareOp::create(builder, builder.getI1Type(), + registerPredicate(condition.comparison), + reg, rhs); + } case frontend::ConditionKind::Not: return arith::XOrIOp::create( - builder, - emitConditionUncached(condition.lhs, gateParameters, gateQubits), + builder, emitCondition(condition.lhs, gateParameters, gateQubits), builder.boolConstant(true)); case frontend::ConditionKind::And: { - auto lhs = - emitConditionUncached(condition.lhs, gateParameters, gateQubits); + auto lhs = emitCondition(condition.lhs, gateParameters, gateQubits); auto ifOp = scf::IfOp::create(builder, builder.getI1Type(), lhs, true); OpBuilder::InsertionGuard guard(builder); auto& thenBlock = ifOp.getThenRegion().front(); @@ -2032,8 +1988,7 @@ class OpenQASMToQCEmitter { } builder.setInsertionPointToEnd(&thenBlock); scf::YieldOp::create( - builder, - emitConditionUncached(condition.rhs, gateParameters, gateQubits)); + builder, emitCondition(condition.rhs, gateParameters, gateQubits)); auto& elseBlock = ifOp.getElseRegion().front(); if (!elseBlock.empty()) { elseBlock.back().erase(); @@ -2043,8 +1998,7 @@ class OpenQASMToQCEmitter { return ifOp.getResult(0); } case frontend::ConditionKind::Or: { - auto lhs = - emitConditionUncached(condition.lhs, gateParameters, gateQubits); + auto lhs = emitCondition(condition.lhs, gateParameters, gateQubits); auto ifOp = scf::IfOp::create(builder, builder.getI1Type(), lhs, true); OpBuilder::InsertionGuard guard(builder); auto& thenBlock = ifOp.getThenRegion().front(); @@ -2059,8 +2013,7 @@ class OpenQASMToQCEmitter { } builder.setInsertionPointToEnd(&elseBlock); scf::YieldOp::create( - builder, - emitConditionUncached(condition.rhs, gateParameters, gateQubits)); + builder, emitCondition(condition.rhs, gateParameters, gateQubits)); return ifOp.getResult(0); } case frontend::ConditionKind::Comparison: @@ -2069,24 +2022,6 @@ class OpenQASMToQCEmitter { llvm_unreachable("unknown condition kind"); } - [[nodiscard]] Value emitCondition(const frontend::ConditionId id, - ValueRange gateParameters, - ValueRange gateQubits) { - // Cache only complete statement conditions. Short-circuit operands can be - // defined inside an scf.if region and cannot be reused in the parent block. - const auto canonical = canonicalConditions.at(id); - if (canonical) { - if (const auto cached = conditionCache.find(*canonical); - cached != conditionCache.end()) { - return cached->second; - } - } - auto value = emitConditionUncached(id, gateParameters, gateQubits); - if (canonical) { - conditionCache.try_emplace(*canonical, value); - } - return value; - } static void recordMutation(const StateSlot slot, llvm::DenseSet& mutationKeys, SmallVectorImpl& mutations) { @@ -2244,7 +2179,6 @@ class OpenQASMToQCEmitter { } else if (statement.conditionInitializer) { value = emitCondition(*statement.conditionInitializer, {}, gateQubits); } - invalidateConditionCache(); scalarValues.at(statement.scalar) = value; } @@ -2252,14 +2186,12 @@ class OpenQASMToQCEmitter { emitScalarAssignment(const frontend::ScalarAssignmentStatement& statement, ValueRange gateQubits) { if (statement.value) { - auto value = emitExpression(builder, *statement.value, {}); - invalidateConditionCache(); - scalarValues.at(statement.scalar) = value; + scalarValues.at(statement.scalar) = + emitExpression(builder, *statement.value, {}); return; } - auto value = emitCondition(*statement.condition, {}, gateQubits); - invalidateConditionCache(); - scalarValues.at(statement.scalar) = value; + scalarValues.at(statement.scalar) = + emitCondition(*statement.condition, {}, gateQubits); } void emitDeclaration(const frontend::DeclarationStatement& statement) { @@ -2287,11 +2219,9 @@ class OpenQASMToQCEmitter { static_cast(declaration.width), declaration.name, program.openQASM2 ? cbit::Initialization::Zero : cbit::Initialization::Undefined); - invalidateConditionCache(); } void assignBit(const frontend::BitReference& target, Value value) { - invalidateConditionCache(); auto reg = classicalRegisters[target.reg]; assert(reg && "semantic analysis must declare bit registers before use"); if (!target.dynamicIndex) { @@ -2317,7 +2247,6 @@ class OpenQASMToQCEmitter { const frontend::BitVectorAssignmentStatement& assignment) { auto value = emitBitVectorExpression(builder, assignment.value); const auto bits = ensureBits(builder, value); - invalidateConditionCache(); auto reg = classicalRegisters[assignment.target]; assert(reg && "semantic analysis must declare bit registers before use"); for (auto [index, bit] : llvm::enumerate(bits)) { @@ -2369,8 +2298,6 @@ class OpenQASMToQCEmitter { const auto slots = mutatedState(nestedStatements); const auto initialValues = stateValues(slots); const auto savedScalars = scalarValues; - const auto savedConditionCache = conditionCache; - const auto savedClassicalStateGeneration = classicalStateGeneration; const auto* thenStatements = &conditional.thenStatements; const auto* elseStatements = &conditional.elseStatements; if (slots.empty() && thenStatements->empty() && !elseStatements->empty()) { @@ -2385,7 +2312,6 @@ class OpenQASMToQCEmitter { const auto emitBranch = [&](Block& block, ArrayRef statements) { scalarValues = savedScalars; - conditionCache.clear(); if (!block.empty()) { block.back().erase(); } @@ -2401,11 +2327,6 @@ class OpenQASMToQCEmitter { } scalarValues = savedScalars; assignState(slots, ifOp.getResults()); - if (classicalStateGeneration == savedClassicalStateGeneration) { - conditionCache = savedConditionCache; - } else { - conditionCache.clear(); - } } [[nodiscard]] Value extendRangeValue(Value value, Type targetType, @@ -2471,8 +2392,6 @@ class OpenQASMToQCEmitter { const auto slots = mutatedState(loop.body); const auto initialValues = stateValues(slots); const auto savedScalars = scalarValues; - const auto savedConditionCache = conditionCache; - const auto savedClassicalStateGeneration = classicalStateGeneration; if (loop.provenPositiveRange) { auto start = emitProvenIndexExpression(builder, loop.start); @@ -2490,7 +2409,6 @@ class OpenQASMToQCEmitter { } builder.setInsertionPointToEnd(body); scalarValues = savedScalars; - conditionCache.clear(); assignState(slots, forOp.getRegionIterArgs()); provenInductionValues[loop.inductionVariable] = forOp.getInductionVar(); scalarValues.at(loop.inductionVariable) = arith::IndexCastOp::create( @@ -2503,11 +2421,6 @@ class OpenQASMToQCEmitter { scalarValues = savedScalars; provenInductionValues.erase(loop.inductionVariable); assignState(slots, forOp.getResults()); - if (classicalStateGeneration == savedClassicalStateGeneration) { - conditionCache = savedConditionCache; - } else { - conditionCache.clear(); - } return; } @@ -2538,7 +2451,6 @@ class OpenQASMToQCEmitter { } builder.setInsertionPointToEnd(body); scalarValues = savedScalars; - conditionCache.clear(); assignState(slots, forOp.getRegionIterArgs()); auto counter = arith::IndexCastOp::create(builder, builder.getI64Type(), forOp.getInductionVar()); @@ -2554,11 +2466,6 @@ class OpenQASMToQCEmitter { } scalarValues = savedScalars; assignState(slots, forOp.getResults()); - if (classicalStateGeneration == savedClassicalStateGeneration) { - conditionCache = savedConditionCache; - } else { - conditionCache.clear(); - } return; } @@ -2593,7 +2500,6 @@ class OpenQASMToQCEmitter { builder.setInsertionPoint(nested.getInsertionBlock(), nested.getInsertionPoint()); scalarValues = savedScalars; - conditionCache.clear(); assignState(slots, arguments.drop_front()); scalarValues.at(loop.inductionVariable) = arith::TruncIOp::create( builder, builder.getI64Type(), arguments.front()); @@ -2607,11 +2513,6 @@ class OpenQASMToQCEmitter { }); scalarValues = savedScalars; assignState(slots, whileOp.getResults().drop_front()); - if (classicalStateGeneration == savedClassicalStateGeneration) { - conditionCache = savedConditionCache; - } else { - conditionCache.clear(); - } } void emitWhile(const frontend::WhileStatement& loop, @@ -2619,8 +2520,6 @@ class OpenQASMToQCEmitter { const auto slots = mutatedState(loop.body); const auto initialValues = stateValues(slots); const auto savedScalars = scalarValues; - const auto savedConditionCache = conditionCache; - const auto savedClassicalStateGeneration = classicalStateGeneration; auto whileOp = scf::WhileOp::create( builder, ValueRange(initialValues).getTypes(), initialValues, [&](OpBuilder& nested, Location, ValueRange arguments) { @@ -2628,7 +2527,6 @@ class OpenQASMToQCEmitter { builder.setInsertionPoint(nested.getInsertionBlock(), nested.getInsertionPoint()); scalarValues = savedScalars; - conditionCache.clear(); assignState(slots, arguments); auto condition = emitCondition(loop.condition, gateParameters, gateQubits); @@ -2639,7 +2537,6 @@ class OpenQASMToQCEmitter { builder.setInsertionPoint(nested.getInsertionBlock(), nested.getInsertionPoint()); scalarValues = savedScalars; - conditionCache.clear(); assignState(slots, arguments); for (const auto statement : loop.body) { emitStatement(statement, gateParameters, gateQubits); @@ -2648,11 +2545,6 @@ class OpenQASMToQCEmitter { }); scalarValues = savedScalars; assignState(slots, whileOp.getResults()); - if (classicalStateGeneration == savedClassicalStateGeneration) { - conditionCache = savedConditionCache; - } else { - conditionCache.clear(); - } } void emitSwitch(const frontend::SwitchStatement& switchStatement, @@ -2667,8 +2559,6 @@ class OpenQASMToQCEmitter { const auto slots = mutatedState(nestedStatements); const auto initialValues = stateValues(slots); const auto savedScalars = scalarValues; - const auto savedConditionCache = conditionCache; - const auto savedClassicalStateGeneration = classicalStateGeneration; auto control = emitExpression(builder, switchStatement.control, {}); auto selector = @@ -2682,7 +2572,6 @@ class OpenQASMToQCEmitter { auto& block = region.emplaceBlock(); builder.setInsertionPointToEnd(&block); scalarValues = savedScalars; - conditionCache.clear(); for (const auto statement : statements) { emitStatement(statement, gateParameters, gateQubits); } @@ -2697,11 +2586,6 @@ class OpenQASMToQCEmitter { emitBranch(switchOp.getDefaultRegion(), switchStatement.defaultStatements); scalarValues = savedScalars; assignState(slots, switchOp.getResults()); - if (classicalStateGeneration == savedClassicalStateGeneration) { - conditionCache = savedConditionCache; - } else { - conditionCache.clear(); - } } }; diff --git a/mlir/lib/Dialect/QC/Translation/TranslateQCToOpenQASM3.cpp b/mlir/lib/Dialect/QC/Translation/TranslateQCToOpenQASM3.cpp index 2de5180c8b..e9a891b3c6 100644 --- a/mlir/lib/Dialect/QC/Translation/TranslateQCToOpenQASM3.cpp +++ b/mlir/lib/Dialect/QC/Translation/TranslateQCToOpenQASM3.cpp @@ -89,26 +89,6 @@ struct GateCall { SmallVector qubits; }; -struct RegisterBitConstraint { - Value reg; - int64_t index; - bool expected; - Operation* observation; -}; - -struct RegisterEquality { - Value reg; - APInt expected; - SmallVector expressionIfs; - SmallVector expressionOperations; -}; - -struct RegisterEqualityCandidate { - SmallVector constraints; - SmallVector expressionIfs; - DenseSet expressionOperations; -}; - } // namespace [[nodiscard]] static bool isOpenQASMIdentifier(const StringRef value) { @@ -191,11 +171,6 @@ class OpenQASMEmitter { SmallVector resourceOrder; DenseMap valueNames; DenseSet returnedRegisters; - DenseMap registerEqualities; - DenseSet foldedConditionIfs; - DenseSet foldedRegisterExpressionOperations; - DenseMap fusedMeasurementStores; - DenseSet foldedMeasurementStores; SmallVector scalarOutputs; llvm::StringSet<> usedNames; llvm::StringSet<> fixedHelpers; @@ -328,16 +303,13 @@ class OpenQASMEmitter { if (auto alloc = dyn_cast(&operation)) { const auto type = alloc.getResult().getType(); const auto width = type.getWidth(); - if (width <= 0) { - return fail(alloc, "classical register width must be positive"); - } - const auto bitWidth = static_cast(width); - if (bitWidth > MAX_CLASSICAL_BITS - numClassicalBits) { + if (width <= 0 || static_cast(width) > + MAX_CLASSICAL_BITS - numClassicalBits) { return fail(alloc, "total classical register width exceeds the " "supported limit of " + Twine(MAX_CLASSICAL_BITS) + " bits"); } - numClassicalBits += bitWidth; + numClassicalBits += static_cast(width); const bool isOutput = returnedRegisters.contains(alloc.getResult()); const auto name = alloc->getAttrOfType( mqt::MQTDialect::RegisterNameAttrHelper::getNameStr()); @@ -383,7 +355,6 @@ class OpenQASMEmitter { "allocations"); } } - collectCompatibilityPatterns(); return success(); } @@ -412,562 +383,6 @@ class OpenQASMEmitter { return {}; } - [[nodiscard]] static std::optional - getBooleanConstant(Value value, RegisterEqualityCandidate& candidate) { - auto constant = value.getDefiningOp(); - auto integer = - constant ? dyn_cast(constant.getValue()) : IntegerAttr{}; - if (!integer || !integer.getType().isInteger(1)) { - return std::nullopt; - } - candidate.expressionOperations.insert(constant); - return !integer.getValue().isZero(); - } - - [[nodiscard]] bool - matchRegisterConjunction(Value value, bool positive, - RegisterEqualityCandidate& candidate) const { - SmallVector> pending{{value, positive}}; - DenseSet visitedPositive; - DenseSet visitedNegative; - - while (!pending.empty()) { - auto [current, expected] = pending.pop_back_val(); - auto& visited = expected ? visitedPositive : visitedNegative; - if (!visited.insert(current).second) { - return false; - } - - if (const auto constant = getBooleanConstant(current, candidate)) { - if (*constant != expected) { - return false; - } - continue; - } - - if (auto xorOp = current.getDefiningOp()) { - if (const auto lhs = getBooleanConstant(xorOp.getLhs(), candidate)) { - candidate.expressionOperations.insert(xorOp); - pending.emplace_back(xorOp.getRhs(), expected != *lhs); - continue; - } - if (const auto rhs = getBooleanConstant(xorOp.getRhs(), candidate)) { - candidate.expressionOperations.insert(xorOp); - pending.emplace_back(xorOp.getLhs(), expected != *rhs); - continue; - } - return false; - } - - if (auto load = current.getDefiningOp()) { - const auto index = getConstantInteger(load.getIndex()); - if (!index) { - return false; - } - candidate.expressionOperations.insert(load); - candidate.constraints.push_back({.reg = load.getReg(), - .index = *index, - .expected = expected, - .observation = load}); - continue; - } - - // QC cleanup may forward a static CBit load to the value written by its - // latest store. Recover the register provenance only through one - // unambiguous store; snapshot validation below still proves its ordering. - cbit::StoreOp storedBit; - for (Operation* user : current.getUsers()) { - auto store = dyn_cast(user); - if (!store || store.getValue() != current) { - continue; - } - if (storedBit) { - return false; - } - storedBit = store; - } - if (storedBit) { - const auto index = getConstantInteger(storedBit.getIndex()); - auto* definition = current.getDefiningOp(); - if (!index || definition == nullptr) { - return false; - } - candidate.expressionOperations.insert(definition); - candidate.constraints.push_back({.reg = storedBit.getReg(), - .index = *index, - .expected = expected, - .observation = storedBit}); - continue; - } - - if (!expected) { - return false; - } - auto ifOp = current.getDefiningOp(); - if (!ifOp || ifOp.getNumResults() != 1 || - !ifOp.getResult(0).getType().isInteger(1) || - ifOp.getElseRegion().empty()) { - return false; - } - auto thenYield = - dyn_cast(ifOp.getThenRegion().front().getTerminator()); - auto elseYield = - dyn_cast(ifOp.getElseRegion().front().getTerminator()); - if (!thenYield || !elseYield || thenYield.getNumOperands() != 1 || - elseYield.getNumOperands() != 1) { - return false; - } - - const auto thenConstant = - getBooleanConstant(thenYield.getOperand(0), candidate); - const auto elseConstant = - getBooleanConstant(elseYield.getOperand(0), candidate); - candidate.expressionOperations.insert(ifOp); - candidate.expressionIfs.push_back(ifOp); - if (elseConstant && !*elseConstant) { - pending.emplace_back(ifOp.getCondition(), true); - pending.emplace_back(thenYield.getOperand(0), true); - continue; - } - if (thenConstant && !*thenConstant) { - pending.emplace_back(ifOp.getCondition(), false); - pending.emplace_back(elseYield.getOperand(0), true); - continue; - } - return false; - } - return true; - } - - [[nodiscard]] static bool containsOnlyMatchedExpressionOperations( - const RegisterEqualityCandidate& candidate) { - return llvm::all_of(candidate.expressionIfs, [&](Operation* operation) { - auto ifOp = cast(operation); - return llvm::all_of(ifOp.getThenRegion().front().without_terminator(), - [&](Operation& nested) { - return candidate.expressionOperations.contains( - &nested); - }) && - llvm::all_of(ifOp.getElseRegion().front().without_terminator(), - [&](Operation& nested) { - return candidate.expressionOperations.contains( - &nested); - }); - }); - } - - [[nodiscard]] static Operation* getTopLevelObservation(Operation* operation, - Block* consumerBlock) { - while (operation != nullptr && operation->getBlock() != consumerBlock) { - operation = operation->getParentOp(); - } - return operation; - } - - [[nodiscard]] static bool - hasOnlyRepresentedRegisterWrites(scf::IfOp consumer, - const RegisterEqualityCandidate& candidate, - Value reg) { - auto alloc = reg.getDefiningOp(); - auto* consumerBlock = consumer->getBlock(); - if (!alloc || alloc->getBlock() != consumerBlock || - !alloc->isBeforeInBlock(consumer)) { - return false; - } - - // Zero-initialized bits may be omitted from the reconstructed equality, - // but every explicit write before the consumer must be observed by one of - // its matched constraints. Otherwise an omitted bit could be stale. - for (Operation* operation = alloc->getNextNode(); - operation != consumer.getOperation(); - operation = operation->getNextNode()) { - if (operation == nullptr) { - return false; - } - if (auto store = dyn_cast(operation); - store && store.getReg() == reg) { - const auto index = getConstantInteger(store.getIndex()); - if (!index || - !llvm::any_of(candidate.constraints, [&](const auto& constraint) { - if (constraint.reg != reg || constraint.index != *index) { - return false; - } - auto* observation = - getTopLevelObservation(constraint.observation, consumerBlock); - return observation == operation || - (observation != nullptr && - operation->isBeforeInBlock(observation)); - })) { - return false; - } - continue; - } - - const auto effects = getEffectsRecursively(operation); - if (!effects) { - if (referencesValueRecursively(operation, reg)) { - return false; - } - continue; - } - if (llvm::any_of(*effects, [&](const auto& effect) { - if (!isa( - effect.getEffect())) { - return false; - } - const auto affected = effect.getValue(); - return affected == reg || - (!affected && referencesValueRecursively(operation, reg)); - })) { - return false; - } - } - return true; - } - - [[nodiscard]] std::optional - matchRegisterEquality(scf::IfOp consumer) const { - RegisterEqualityCandidate candidate; - if (!matchRegisterConjunction(consumer.getCondition(), true, candidate) || - candidate.constraints.empty() || - !containsOnlyMatchedExpressionOperations(candidate)) { - return std::nullopt; - } - - auto reg = candidate.constraints.front().reg; - const auto resource = resources.find(reg); - if (resource == resources.end() || - resource->second.kind != ResourceKind::Bit || - !std::in_range(resource->second.width) || - candidate.constraints.size() > - static_cast(resource->second.width)) { - return std::nullopt; - } - SmallVector> expectedBits( - static_cast(resource->second.width)); - if (resource->second.initialization == cbit::Initialization::Zero) { - llvm::fill(expectedBits, false); - } - SmallVector constrained(expectedBits.size(), false); - for (const auto& constraint : candidate.constraints) { - if (constraint.reg != reg || constraint.index < 0 || - constraint.index >= resource->second.width) { - return std::nullopt; - } - const auto index = static_cast(constraint.index); - if (constrained[index]) { - return std::nullopt; - } - constrained[index] = true; - expectedBits[index] = constraint.expected; - } - if (llvm::any_of(expectedBits, - [](const auto& value) { return !value.has_value(); })) { - return std::nullopt; - } - if (resource->second.initialization == cbit::Initialization::Zero && - candidate.constraints.size() < expectedBits.size() && - !hasOnlyRepresentedRegisterWrites(consumer, candidate, reg)) { - return std::nullopt; - } - if (!preservesRegisterSnapshot(consumer, candidate, reg)) { - return std::nullopt; - } - - APInt expected(static_cast(resource->second.width), 0); - for (const auto [index, bit] : llvm::enumerate(expectedBits)) { - if (*bit) { - expected.setBit(static_cast(index)); - } - } - return RegisterEquality{.reg = reg, - .expected = std::move(expected), - .expressionIfs = std::move(candidate.expressionIfs), - .expressionOperations = SmallVector( - candidate.expressionOperations.begin(), - candidate.expressionOperations.end())}; - } - - [[nodiscard]] bool - isDeadRegisterExpression(const RegisterEqualityCandidate& candidate) const { - if (candidate.constraints.empty() || - !containsOnlyMatchedExpressionOperations(candidate)) { - return false; - } - return llvm::all_of(candidate.constraints, [&](const auto& constraint) { - const auto resource = resources.find(constraint.reg); - return resource != resources.end() && - resource->second.kind == ResourceKind::Bit && - constraint.index >= 0 && constraint.index < resource->second.width; - }); - } - - [[nodiscard]] static bool - hasExternalExpressionUse(ArrayRef expressionIfs, - const DenseSet& matchedOperations) { - return llvm::any_of(expressionIfs, [&](Operation* expressionIf) { - return llvm::any_of(expressionIf->getResults(), [&](Value result) { - return llvm::any_of(result.getUsers(), [&](Operation* user) { - return !matchedOperations.contains(user); - }); - }); - }); - } - - [[nodiscard]] bool hasExternalFoldedUse(Operation* root) const { - SmallVector pending{root}; - DenseSet visited; - while (!pending.empty()) { - auto* operation = pending.pop_back_val(); - if (!visited.insert(operation).second) { - continue; - } - for (Value result : operation->getResults()) { - for (Operation* user : result.getUsers()) { - if (registerEqualities.contains(user)) { - continue; - } - Operation* continuation = - isa(user) ? user->getParentOp() : user; - if (!foldedRegisterExpressionOperations.contains(continuation) && - !foldedConditionIfs.contains(continuation)) { - return true; - } - pending.push_back(continuation); - } - } - } - return false; - } - - [[nodiscard]] static Operation* - getTopLevelEvaluationOperation(Operation* operation, Block* consumerBlock, - const RegisterEqualityCandidate& candidate) { - while (operation->getBlock() != consumerBlock) { - operation = operation->getParentOp(); - if (operation == nullptr || - !candidate.expressionOperations.contains(operation)) { - return nullptr; - } - } - if (candidate.expressionOperations.contains(operation) || - llvm::any_of(candidate.constraints, [&](const auto& constraint) { - return constraint.observation == operation; - })) { - return operation; - } - return nullptr; - } - - [[nodiscard]] static bool referencesValueRecursively(Operation* operation, - Value value) { - bool referencesValue = false; - operation->walk([&](Operation* nested) { - if (!llvm::is_contained(nested->getOperands(), value)) { - return WalkResult::advance(); - } - referencesValue = true; - return WalkResult::interrupt(); - }); - return referencesValue; - } - - [[nodiscard]] static bool - preservesRegisterSnapshot(scf::IfOp consumer, - const RegisterEqualityCandidate& candidate, - Value reg) { - auto* conditionOperation = consumer.getCondition().getDefiningOp(); - auto* consumerBlock = consumer->getBlock(); - if (conditionOperation == nullptr || - conditionOperation->getBlock() != consumerBlock || - !candidate.expressionOperations.contains(conditionOperation)) { - return false; - } - - Operation* earliestEvaluation = conditionOperation; - for (const auto& constraint : candidate.constraints) { - auto* evaluation = getTopLevelEvaluationOperation( - constraint.observation, consumerBlock, candidate); - if (evaluation == nullptr || !evaluation->isBeforeInBlock(consumer)) { - return false; - } - if (evaluation != earliestEvaluation && - evaluation->isBeforeInBlock(earliestEvaluation)) { - earliestEvaluation = evaluation; - } - } - - for (Operation* operation = earliestEvaluation; - operation != consumer.getOperation(); - operation = operation->getNextNode()) { - if (operation == nullptr) { - return false; - } - if (candidate.expressionOperations.contains(operation)) { - continue; - } - if (llvm::any_of(candidate.constraints, [&](const auto& constraint) { - return constraint.observation == operation; - })) { - continue; - } - const auto effects = getEffectsRecursively(operation); - if (!effects) { - if (referencesValueRecursively(operation, reg)) { - return false; - } - continue; - } - if (llvm::any_of(*effects, [&](const auto& effect) { - if (!isa( - effect.getEffect())) { - return false; - } - const auto affected = effect.getValue(); - return affected == reg || - (!affected && referencesValueRecursively(operation, reg)); - })) { - return false; - } - } - return true; - } - - void collectCompatibilityPatterns() { - SmallVector> candidates; - function.walk([&](scf::IfOp ifOp) { - if (ifOp.getNumResults() != 0) { - return; - } - auto equality = matchRegisterEquality(ifOp); - if (!equality) { - return; - } - candidates.emplace_back(ifOp, std::move(*equality)); - }); - - SmallVector deadExpressionCandidates; - function.walk([&](scf::IfOp ifOp) { - if (ifOp.getNumResults() != 1 || !ifOp.getResult(0).use_empty()) { - return; - } - RegisterEqualityCandidate candidate; - if (matchRegisterConjunction(ifOp.getResult(0), true, candidate) && - isDeadRegisterExpression(candidate)) { - deadExpressionCandidates.push_back(std::move(candidate)); - } - }); - - SmallVector active(candidates.size(), true); - SmallVector activeDead(deadExpressionCandidates.size(), true); - bool changed = true; - while (changed) { - changed = false; - DenseSet matchedOperations; - for (const auto [index, candidate] : llvm::enumerate(candidates)) { - if (!active[index]) { - continue; - } - matchedOperations.insert(candidate.first.getOperation()); - matchedOperations.insert(candidate.second.expressionOperations.begin(), - candidate.second.expressionOperations.end()); - } - for (const auto [index, candidate] : - llvm::enumerate(deadExpressionCandidates)) { - if (activeDead[index]) { - matchedOperations.insert(candidate.expressionOperations.begin(), - candidate.expressionOperations.end()); - } - } - for (const auto [index, candidate] : llvm::enumerate(candidates)) { - if (!active[index]) { - continue; - } - if (hasExternalExpressionUse(candidate.second.expressionIfs, - matchedOperations)) { - active[index] = false; - changed = true; - } - } - for (const auto [index, candidate] : - llvm::enumerate(deadExpressionCandidates)) { - if (activeDead[index] && - hasExternalExpressionUse(candidate.expressionIfs, - matchedOperations)) { - activeDead[index] = false; - changed = true; - } - } - } - - for (auto [index, candidate] : llvm::enumerate(candidates)) { - if (!active[index]) { - continue; - } - for (auto* expressionIf : candidate.second.expressionIfs) { - foldedConditionIfs.insert(expressionIf); - } - foldedRegisterExpressionOperations.insert( - candidate.second.expressionOperations.begin(), - candidate.second.expressionOperations.end()); - registerEqualities.try_emplace(candidate.first.getOperation(), - std::move(candidate.second)); - } - for (const auto [index, candidate] : - llvm::enumerate(deadExpressionCandidates)) { - if (!activeDead[index]) { - continue; - } - foldedConditionIfs.insert(candidate.expressionIfs.begin(), - candidate.expressionIfs.end()); - } - - function.walk([&](qc::MeasureOp measurement) { - cbit::StoreOp store; - for (Operation* user : measurement.getResult().getUsers()) { - if (auto candidateStore = dyn_cast(user); - candidateStore && - candidateStore.getValue() == measurement.getResult()) { - if (store) { - return; - } - store = candidateStore; - continue; - } - if (foldedRegisterExpressionOperations.contains(user)) { - if (hasExternalFoldedUse(user)) { - return; - } - continue; - } - auto consumer = dyn_cast(user); - if (!consumer || - !registerEqualities.contains(consumer.getOperation())) { - return; - } - } - if (!store || store->getBlock() != measurement->getBlock()) { - return; - } - for (Operation* operation = measurement->getNextNode(); - operation != store.getOperation(); - operation = operation->getNextNode()) { - if (operation == nullptr || !isa(operation)) { - return; - } - } - fusedMeasurementStores.try_emplace(measurement, store); - foldedMeasurementStores.insert(store); - }); - } - - [[nodiscard]] std::string - emitRegisterEquality(const RegisterEquality& equality) const { - llvm::SmallString<64> expected; - equality.expected.toString(expected, 10, false); - return (Twine(resources.at(equality.reg).name) + " == " + expected).str(); - } - [[nodiscard]] LogicalResult emitDeclarations() { for (auto value : resourceOrder) { const auto& resource = resources.at(value); @@ -1017,9 +432,6 @@ class OpenQASMEmitter { return success(); } if (isInlineExpressionOperation(operation)) { - if (foldedRegisterExpressionOperations.contains(&operation)) { - return success(); - } return validateInlineExpressionOperation(operation); } if (isa(&operation) || @@ -1046,10 +458,6 @@ class OpenQASMEmitter { return success(); } if (auto ifOp = dyn_cast(&operation)) { - if (ifOp.getNumResults() != 0 && - foldedConditionIfs.contains(&operation)) { - return success(); - } return emitIf(ifOp); } if (auto forOp = dyn_cast(&operation)) { @@ -1090,7 +498,8 @@ class OpenQASMEmitter { [[nodiscard]] static bool isInlineExpressionOperation(Operation& operation) { const auto name = operation.getName().getStringRef(); - return isa(&operation) || + return isa(&operation) || !binaryOperator(name).empty() || name == "arith.negf" || name == "arith.remf" || isScalarCast(name) || !mathFunction(name).empty(); @@ -1180,6 +589,37 @@ class OpenQASMEmitter { if (auto load = value.getDefiningOp()) { return emitBitReference(load.getReg(), load.getIndex()); } + if (auto comparison = value.getDefiningOp()) { + const auto resource = resources.find(comparison.getReg()); + if (resource == resources.end() || + resource->second.kind != ResourceKind::Bit) { + return failExpression(value, + "register comparison refers to unsupported " + "storage"); + } + const auto* predicate = [&] { + switch (comparison.getPredicate()) { + case cbit::ComparisonPredicate::Equal: + return "=="; + case cbit::ComparisonPredicate::NotEqual: + return "!="; + case cbit::ComparisonPredicate::Less: + return "<"; + case cbit::ComparisonPredicate::LessEqual: + return "<="; + case cbit::ComparisonPredicate::Greater: + return ">"; + case cbit::ComparisonPredicate::GreaterEqual: + return ">="; + } + llvm_unreachable("unknown CBit comparison predicate"); + }(); + llvm::SmallString<32> rhs; + comparison.getRhs().toString(rhs, 10, false); + return (Twine("(") + resource->second.name + " " + predicate + " " + rhs + + ")") + .str(); + } auto* operation = value.getDefiningOp(); if (operation == nullptr) { return failExpression(value, "unmapped block argument"); @@ -1205,25 +645,6 @@ class OpenQASMEmitter { return emitBinary(cmp.getLhs(), predicate, cmp.getRhs()); } const auto name = operation->getName().getStringRef(); - if (auto xorOp = dyn_cast(operation); - xorOp && value.getType().isInteger(1)) { - auto constant = getConstantInteger(xorOp.getLhs()); - Value operand = xorOp.getRhs(); - if (!constant) { - constant = getConstantInteger(xorOp.getRhs()); - operand = xorOp.getLhs(); - } - if (constant) { - auto expression = emitExpression(operand); - if (failed(expression)) { - return failure(); - } - if (*constant == 0) { - return std::move(*expression); - } - return (Twine("(!") + *expression + ")").str(); - } - } if (name == "arith.remf") { auto lhs = emitExpression(operation->getOperand(0)); if (failed(lhs)) { @@ -1432,13 +853,20 @@ class OpenQASMEmitter { } [[nodiscard]] LogicalResult emitStore(cbit::StoreOp store) { - if (foldedMeasurementStores.contains(store)) { - return success(); - } auto target = emitBitReference(store.getReg(), store.getIndex()); if (failed(target)) { return failure(); } + if (auto measurement = store.getValue().getDefiningOp(); + measurement && measurement.getResult().hasOneUse() && + measurement->getNextNode() == store.getOperation()) { + auto qubit = emitQubit(measurement.getQubit()); + if (failed(qubit)) { + return failure(); + } + *output << *target << " = measure " << *qubit << ";\n"; + return success(); + } auto value = emitExpression(store.getValue()); if (failed(value)) { return failure(); @@ -1448,21 +876,17 @@ class OpenQASMEmitter { } [[nodiscard]] LogicalResult emitMeasurement(qc::MeasureOp measurement) { + if (measurement.getResult().hasOneUse()) { + if (auto store = dyn_cast( + *measurement.getResult().getUsers().begin()); + store && measurement->getNextNode() == store.getOperation()) { + return success(); + } + } auto qubit = emitQubit(measurement.getQubit()); if (failed(qubit)) { return failure(); } - if (const auto found = - fusedMeasurementStores.find(measurement.getOperation()); - found != fusedMeasurementStores.end()) { - auto store = cast(found->second); - auto target = emitBitReference(store.getReg(), store.getIndex()); - if (failed(target)) { - return failure(); - } - *output << *target << " = measure " << *qubit << ";\n"; - return success(); - } const auto name = uniqueName("b", nextBit); valueNames.try_emplace(measurement.getResult(), name); *output << "bit " << name << " = measure " << *qubit << ";\n"; @@ -1473,18 +897,11 @@ class OpenQASMEmitter { if (ifOp.getNumResults() != 0) { return fail(ifOp, "scf.if results are not supported"); } - std::string condition; - if (const auto found = registerEqualities.find(ifOp.getOperation()); - found != registerEqualities.end()) { - condition = emitRegisterEquality(found->second); - } else { - auto expression = emitExpression(ifOp.getCondition()); - if (failed(expression)) { - return failure(); - } - condition = std::move(*expression); + auto condition = emitExpression(ifOp.getCondition()); + if (failed(condition)) { + return failure(); } - *output << "if (" << condition << ") {\n"; + *output << "if (" << *condition << ") {\n"; output->indent(); if (failed(emitBlock(ifOp.getThenRegion().front()))) { return failure(); diff --git a/mlir/lib/Dialect/QCO/Utils/DDFunctionality.cpp b/mlir/lib/Dialect/QCO/Utils/DDFunctionality.cpp index 4a8cfb300f..736ff84bb8 100644 --- a/mlir/lib/Dialect/QCO/Utils/DDFunctionality.cpp +++ b/mlir/lib/Dialect/QCO/Utils/DDFunctionality.cpp @@ -655,6 +655,46 @@ static LogicalResult loadRegister(cbit::LoadOp load, ClassicalEnv& classical) { classical); } +static LogicalResult compareRegister(cbit::CompareOp compare, + ClassicalEnv& classical) { + const auto regIt = classical.registers.find(compare.getReg()); + if (regIt == classical.registers.end()) { + return compare.emitError() + << "CBit register is not mapped for QCO DD simulation"; + } + llvm::APInt actual(compare.getRhs().getBitWidth(), 0); + for (const auto [index, cell] : llvm::enumerate(*regIt->second)) { + if (cell.deferredWire && classical.deferredMeasurementUse != nullptr) { + *classical.deferredMeasurementUse = compare.getOperation(); + return failure(); + } + if (!cell.value) { + return compare.emitError() + << "read from an undefined CBit register element"; + } + actual.setBitVal(static_cast(index), *cell.value); + } + const auto result = [&] { + switch (compare.getPredicate()) { + case cbit::ComparisonPredicate::Equal: + return actual.eq(compare.getRhs()); + case cbit::ComparisonPredicate::NotEqual: + return actual.ne(compare.getRhs()); + case cbit::ComparisonPredicate::Less: + return actual.ult(compare.getRhs()); + case cbit::ComparisonPredicate::LessEqual: + return actual.ule(compare.getRhs()); + case cbit::ComparisonPredicate::Greater: + return actual.ugt(compare.getRhs()); + case cbit::ComparisonPredicate::GreaterEqual: + return actual.uge(compare.getRhs()); + } + llvm_unreachable("unknown CBit comparison predicate"); + }(); + return bindInteger(compare.getResult(), + llvm::APInt(1, static_cast(result)), classical); +} + static FailureOr lookupMemRefSlot(Value memref, ValueRange indices, ClassicalEnv& classical, Operation* op) { @@ -1282,6 +1322,9 @@ static LogicalResult applyOp(Operation& op, WalkState& walk, StateDD& state) { .template Case([&](cbit::LoadOp load) { return loadRegister(load, *walk.classical); }) + .template Case([&](cbit::CompareOp compare) { + return compareRegister(compare, *walk.classical); + }) .template Case([&](cbit::StoreOp store) { return storeRegister(store, *walk.classical); }) diff --git a/mlir/lib/Target/OpenQASM/OpenQASMSemantics.cpp b/mlir/lib/Target/OpenQASM/OpenQASMSemantics.cpp index 1a30349c60..adb3564dd1 100644 --- a/mlir/lib/Target/OpenQASM/OpenQASMSemantics.cpp +++ b/mlir/lib/Target/OpenQASM/OpenQASMSemantics.cpp @@ -3711,16 +3711,14 @@ class SemanticAnalyzer { const auto* lhsSymbol = lhsSyntax.kind == Expr::Kind::Identifier ? lookup(lhsSyntax.identifier) : nullptr; - if (condition.kind == Expr::Kind::Equal && lhsSymbol != nullptr && - lhsSymbol->kind == SymbolKind::Register && + 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& rhsSyntax = syntax.expressions[*condition.rhs]; MQT_OQ3_TRY_ASSIGN(bits, resolveBits({.location = lhsSyntax.location, .identifier = lhsSyntax.identifier})); - // OpenQASM 2 classical bits default to zero. OpenQASM 3 register - // comparisons require every bit to be initialized. if (!program.openQASM2) { for (const auto& bit : bits) { if (failed(ensureBitInitialized(bit, condition.location))) { @@ -3756,39 +3754,41 @@ class SemanticAnalyzer { expectedBits = llvm::APInt(/*numBits=*/64, expectedValue); } if (expectedBits.getActiveBits() > bits.size()) { - // Value cannot equal the register contents. + const bool result = condition.kind == Expr::Kind::NotEqual || + condition.kind == Expr::Kind::Less || + condition.kind == Expr::Kind::LessEqual; return addCondition( {.kind = ConditionKind::Literal, .location = getSourceLocation(condition.location), - .literal = false}); + .literal = result}); } if (expectedBits.getBitWidth() < bits.size()) { expectedBits = expectedBits.zext(static_cast(bits.size())); } else if (expectedBits.getBitWidth() > bits.size()) { expectedBits = expectedBits.trunc(static_cast(bits.size())); } - auto result = - addCondition({.kind = ConditionKind::Literal, - .location = getSourceLocation(condition.location), - .literal = true}); - for (const auto [index, bit] : llvm::enumerate(bits)) { - auto bitCondition = - addCondition({.kind = ConditionKind::Bit, - .location = getSourceLocation(condition.location), - .bit = bit}); - if (!expectedBits[index]) { - bitCondition = - addCondition({.kind = ConditionKind::Not, - .location = getSourceLocation(condition.location), - .lhs = bitCondition}); - } - result = - addCondition({.kind = ConditionKind::And, - .location = getSourceLocation(condition.location), - .lhs = result, - .rhs = bitCondition}); - } - return result; + return addCondition({.kind = ConditionKind::RegisterComparison, + .location = getSourceLocation(condition.location), + .reg = lhsSymbol->id, + .expected = std::move(expectedBits), + .comparison = [&] { + switch (condition.kind) { + case Expr::Kind::Equal: + return ComparisonKind::Equal; + case Expr::Kind::NotEqual: + return ComparisonKind::NotEqual; + case Expr::Kind::Less: + return ComparisonKind::Less; + case Expr::Kind::LessEqual: + return ComparisonKind::LessEqual; + case Expr::Kind::Greater: + return ComparisonKind::Greater; + case Expr::Kind::GreaterEqual: + return ComparisonKind::GreaterEqual; + default: + llvm_unreachable("not a comparison"); + } + }()}); } typed.kind = ConditionKind::Comparison; MQT_OQ3_TRY_ASSIGN(comparisonLhs, analyzeExpression(*condition.lhs)); diff --git a/mlir/unittests/Conversion/CBitToMemRef/test_cbit_to_memref.cpp b/mlir/unittests/Conversion/CBitToMemRef/test_cbit_to_memref.cpp index a48ff2acd2..24feb52681 100644 --- a/mlir/unittests/Conversion/CBitToMemRef/test_cbit_to_memref.cpp +++ b/mlir/unittests/Conversion/CBitToMemRef/test_cbit_to_memref.cpp @@ -134,6 +134,29 @@ TEST_F(CBitToMemRefTest, LargeZeroInitializationProducesBoundedIR) { EXPECT_EQ(stores, 1); } +TEST_F(CBitToMemRefTest, LowersRegisterComparisons) { + auto moduleOp = convert(R"mlir( + module { + func.func @main() -> i1 { + %reg = cbit.alloc(#cbit.init) : !cbit.reg<3> + %result = cbit.cmp uge, %reg, 5 : i3 : !cbit.reg<3> + return %result : i1 + } + } + )mlir"); + ASSERT_TRUE(moduleOp); + EXPECT_TRUE(succeeded(verify(*moduleOp))); + + bool containsCBit = false; + moduleOp->walk([&](Operation* op) { + containsCBit |= op->getDialect() == context->getLoadedDialect("cbit"); + }); + EXPECT_FALSE(containsCBit); + size_t loads = 0; + moduleOp->walk([&](memref::LoadOp) { ++loads; }); + EXPECT_EQ(loads, 3); +} + TEST_F(CBitToMemRefTest, ConvertsFunctionSignaturesCallsAndReturns) { auto moduleOp = convert(R"mlir( module { 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 8dc77847ac..2559a630de 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 @@ -11,6 +11,7 @@ #include "Support/IRVerification.h" #include "TestCaseUtils.h" #include "mlir/Conversion/QCToQIR/QIRAdaptive/QCToQIRAdaptive.h" +#include "mlir/Dialect/CBit/IR/CBitOps.h" #include "mlir/Dialect/MQT/Transforms/Passes.h" #include "mlir/Dialect/QC/Builder/QCProgramBuilder.h" #include "mlir/Dialect/QC/IR/QCDialect.h" @@ -256,6 +257,31 @@ TEST(QCToQIRAdaptiveNativeTest, LowersZeroInitializedClassicalControlRegister) { EXPECT_FALSE(module->lookupSymbol(qir::QIR_READ_RESULT)); } +TEST(QCToQIRAdaptiveNativeTest, LowersClassicalRegisterComparison) { + MLIRContext context; + context + .loadDialect(); + qc::QCProgramBuilder builder(&context); + builder.initialize(); + auto q = builder.allocQubit(); + auto c = builder.allocClassicalBitRegister(3); + builder.measure(q, c, 0); + auto rhs = builder.getIntegerAttr(builder.getIntegerType(3), 2); + auto comparison = cbit::CompareOp::create( + builder, builder.getI1Type(), cbit::ComparisonPredicate::Less, c, rhs); + builder.scfIf(comparison, [&] { builder.x(q); }); + auto module = builder.finalize(); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(verify(*module))); + ASSERT_TRUE(succeeded(runQCToQIRAdaptiveConversion(*module))); + EXPECT_TRUE(succeeded(verify(*module))); + bool retainsComparison = false; + module->walk([&](cbit::CompareOp) { retainsComparison = true; }); + EXPECT_FALSE(retainsComparison); +} + TEST(QCToQIRAdaptiveNativeTest, RejectsMultipleRegisterDestinations) { MLIRContext context; context diff --git a/mlir/unittests/Dialect/CBit/IR/test_cbit_ir.cpp b/mlir/unittests/Dialect/CBit/IR/test_cbit_ir.cpp index 6bdb47a4f5..d8dcfe6408 100644 --- a/mlir/unittests/Dialect/CBit/IR/test_cbit_ir.cpp +++ b/mlir/unittests/Dialect/CBit/IR/test_cbit_ir.cpp @@ -66,6 +66,7 @@ TEST_F(CBitIRTest, ParsesAndPrintsRegisterOperations) { %reg = cbit.alloc(#cbit.init) {mqt.register_name = "c"} : !cbit.reg<2> cbit.store %false, %reg[%c0] : !cbit.reg<2> %bit = cbit.load %reg[%c0] : !cbit.reg<2> + %matches = cbit.cmp eq, %reg, 1 : i2 : !cbit.reg<2> return %reg : !cbit.reg<2> } } @@ -84,6 +85,19 @@ TEST_F(CBitIRTest, ParsesAndPrintsRegisterOperations) { EXPECT_NE(printed.find("!cbit.reg<2>"), std::string::npos); EXPECT_NE(printed.find("cbit.store"), std::string::npos); EXPECT_NE(printed.find("cbit.load"), std::string::npos); + EXPECT_NE(printed.find("cbit.cmp eq"), std::string::npos); +} + +TEST_F(CBitIRTest, RejectsComparisonWidthMismatch) { + EXPECT_FALSE(parse(R"mlir( + module { + func.func @main() { + %reg = cbit.alloc(#cbit.init) : !cbit.reg<2> + %matches = cbit.cmp eq, %reg, 1 : i3 : !cbit.reg<2> + return + } + } + )mlir")); } TEST_F(CBitIRTest, RejectsNonPositiveRegisterWidth) { @@ -146,6 +160,7 @@ TEST_F(CBitIRTest, ReportsMemoryEffects) { %reg = cbit.alloc(#cbit.init) : !cbit.reg<1> cbit.store %false, %reg[%c0] : !cbit.reg<1> %bit = cbit.load %reg[%c0] : !cbit.reg<1> + %matches = cbit.cmp eq, %reg, 0 : i1 : !cbit.reg<1> return } } @@ -153,13 +168,16 @@ TEST_F(CBitIRTest, ReportsMemoryEffects) { ASSERT_TRUE(moduleOp); cbit::AllocOp alloc; + cbit::CompareOp compare; cbit::LoadOp load; cbit::StoreOp store; moduleOp->walk([&](cbit::AllocOp op) { alloc = op; }); + moduleOp->walk([&](cbit::CompareOp op) { compare = op; }); moduleOp->walk([&](cbit::LoadOp op) { load = op; }); moduleOp->walk([&](cbit::StoreOp op) { store = op; }); ASSERT_NE(alloc.getOperation(), nullptr); + ASSERT_NE(compare.getOperation(), nullptr); ASSERT_NE(load.getOperation(), nullptr); ASSERT_NE(store.getOperation(), nullptr); @@ -174,6 +192,12 @@ TEST_F(CBitIRTest, ReportsMemoryEffects) { EXPECT_TRUE(isa(effects.front().getEffect())); EXPECT_EQ(effects.front().getValue(), load.getReg()); + effects.clear(); + compare.getEffects(effects); + ASSERT_EQ(effects.size(), 1); + EXPECT_TRUE(isa(effects.front().getEffect())); + EXPECT_EQ(effects.front().getValue(), compare.getReg()); + effects.clear(); store.getEffects(effects); ASSERT_EQ(effects.size(), 1); diff --git a/mlir/unittests/Dialect/QC/Translation/test_openqasm3_emission.cpp b/mlir/unittests/Dialect/QC/Translation/test_openqasm3_emission.cpp index 72b65a1121..2eeb419ba9 100644 --- a/mlir/unittests/Dialect/QC/Translation/test_openqasm3_emission.cpp +++ b/mlir/unittests/Dialect/QC/Translation/test_openqasm3_emission.cpp @@ -212,7 +212,7 @@ TEST(OpenQASM3EmissionTest, RenamesOutputsThatCollideWithStandardGates) { return %bits : !cbit.reg<1> } })mlir"; - const DialectRegistry registry = emissionDialects(); + DialectRegistry registry = emissionDialects(); MLIRContext context(registry); auto moduleOp = parseSourceString(source, &context); ASSERT_TRUE(moduleOp); @@ -268,404 +268,22 @@ switch (selector) { << *emitted; } -TEST(OpenQASM3EmissionTest, - CompactsQASM2RegisterConditionsAndMeasurementStores) { - struct Fixture { - size_t width{}; - llvm::StringLiteral expected{""}; - }; - constexpr std::array fixtures{ - Fixture{.width = 1, .expected = "0"}, - Fixture{.width = 1, .expected = "1"}, - Fixture{.width = 64, .expected = "9223372036854775808"}, - Fixture{.width = 151, - .expected = "1427247692705959881058285969449495136382746624"}, - Fixture{.width = 301, - .expected = "2037035976334486086268445688409378161051468393665" - "936250636140449354381299763336706183397376"}, - }; - - for (const auto& fixture : fixtures) { - SCOPED_TRACE(fixture.width); - const auto lastBit = fixture.width - 1; - const auto source = - "OPENQASM 2.0;\ninclude \"qelib1.inc\";\nqreg q[1];\ncreg c[" + - std::to_string(fixture.width) + "];\nmeasure q[0] -> c[" + - std::to_string(lastBit) + "];\nif(c==" + fixture.expected.str() + - ") x q[0];\n"; - MLIRContext context; - auto moduleOp = qc::translateQASM3ToQC(source, &context); - ASSERT_TRUE(moduleOp); - ASSERT_TRUE(succeeded(runQCCleanupPipeline(*moduleOp))); - auto emitted = qc::translateQCToOpenQASM3(*moduleOp); - - ASSERT_TRUE(succeeded(emitted)); - EXPECT_NE( - emitted->find("c[" + std::to_string(lastBit) + "] = measure q[0];"), - std::string::npos) - << *emitted; - EXPECT_EQ(emitted->find("bit _mqt_b"), std::string::npos) << *emitted; - EXPECT_NE(emitted->find("if (c == " + fixture.expected.str() + ")"), - std::string::npos) - << *emitted; - EXPECT_TRUE(oq3::frontend::analyzeOpenQASM( - *emitted, {.gatePolicy = oq3::frontend::GatePolicy::Strict})) - << *emitted; - EXPECT_TRUE(qc::translateQASM3ToQC(*emitted, &context)) << *emitted; - } -} - -TEST(OpenQASM3EmissionTest, - CompactsRepeatedRegisterConditionsAcrossQuantumOperations) { - constexpr llvm::StringLiteral source = R"mlir( -module { - func.func @main() -> !cbit.reg<1> { - %bits = cbit.alloc(#cbit.init) {mqt.register_name = "c"} - : !cbit.reg<1> - %control = qc.alloc : !qc.qubit - %target = qc.alloc : !qc.qubit - %zero = arith.constant 0 : index - %true = arith.constant true - %bit = cbit.load %bits[%zero] : !cbit.reg<1> - %firstCondition = arith.xori %bit, %true : i1 - scf.if %firstCondition { - qc.ctrl(%control) targets(%arg = %target) { - qc.x %arg : !qc.qubit - qc.yield - } : {!qc.qubit}, {!qc.qubit} - } - qc.barrier %control, %target : !qc.qubit, !qc.qubit - %secondCondition = arith.xori %bit, %true : i1 - scf.if %secondCondition { - qc.h %control : !qc.qubit - } - qc.dealloc %control : !qc.qubit - qc.dealloc %target : !qc.qubit - return %bits : !cbit.reg<1> - } -} -)mlir"; - const DialectRegistry registry = emissionDialects(); - MLIRContext context(registry); - auto moduleOp = parseSourceString(source, &context); - ASSERT_TRUE(moduleOp); - - auto emitted = qc::translateQCToOpenQASM3(*moduleOp); - - ASSERT_TRUE(succeeded(emitted)); - const auto firstCondition = emitted->find("if (c == 0)"); - ASSERT_NE(firstCondition, std::string::npos) << *emitted; - EXPECT_NE(emitted->find("if (c == 0)", firstCondition + 1), - std::string::npos); - EXPECT_NE(emitted->find("ctrl @ x "), std::string::npos); - EXPECT_NE(emitted->find("barrier "), std::string::npos); - EXPECT_NE(emitted->find("h "), std::string::npos); - EXPECT_TRUE(oq3::frontend::analyzeOpenQASM( - *emitted, {.gatePolicy = oq3::frontend::GatePolicy::Strict})) - << *emitted; -} - -TEST(OpenQASM3EmissionTest, CompactsSharedRegisterConditionExpressionTrees) { - constexpr llvm::StringLiteral source = R"mlir( -module { - func.func @main() -> !cbit.reg<2> { - %bits = cbit.alloc(#cbit.init) {mqt.register_name = "c"} - : !cbit.reg<2> - %qubit = qc.alloc : !qc.qubit - %zero = arith.constant 0 : index - %one = arith.constant 1 : index - %false = arith.constant false - %true = arith.constant true - %first = cbit.load %bits[%zero] : !cbit.reg<2> - %condition = scf.if %first -> i1 { - %second = cbit.load %bits[%one] : !cbit.reg<2> - %inverted = arith.xori %second, %true : i1 - scf.yield %inverted : i1 - } else { - scf.yield %false : i1 - } - scf.if %condition { - qc.x %qubit : !qc.qubit - } - qc.barrier %qubit : !qc.qubit - scf.if %condition { - qc.h %qubit : !qc.qubit - } - qc.dealloc %qubit : !qc.qubit - return %bits : !cbit.reg<2> - } -} -)mlir"; - const DialectRegistry registry = emissionDialects(); - MLIRContext context(registry); - auto moduleOp = parseSourceString(source, &context); - ASSERT_TRUE(moduleOp); - - auto emitted = qc::translateQCToOpenQASM3(*moduleOp); - - ASSERT_TRUE(succeeded(emitted)); - const auto firstCondition = emitted->find("if (c == 1)"); - ASSERT_NE(firstCondition, std::string::npos) << *emitted; - EXPECT_NE(emitted->find("if (c == 1)", firstCondition + 1), - std::string::npos); - EXPECT_TRUE(oq3::frontend::analyzeOpenQASM( - *emitted, {.gatePolicy = oq3::frontend::GatePolicy::Strict})) - << *emitted; -} - -TEST(OpenQASM3EmissionTest, ElidesDeadRegisterConditionExpressionTrees) { - constexpr llvm::StringLiteral source = R"mlir( -module { - func.func @main() -> !cbit.reg<2> { - %bits = cbit.alloc(#cbit.init) {mqt.register_name = "c"} - : !cbit.reg<2> - %zero = arith.constant 0 : index - %one = arith.constant 1 : index - %false = arith.constant false - %first = cbit.load %bits[%zero] : !cbit.reg<2> - %unused = scf.if %first -> i1 { - scf.yield %false : i1 - } else { - %second = cbit.load %bits[%one] : !cbit.reg<2> - scf.yield %second : i1 - } - return %bits : !cbit.reg<2> - } -} -)mlir"; - const DialectRegistry registry = emissionDialects(); - MLIRContext context(registry); - auto moduleOp = parseSourceString(source, &context); - ASSERT_TRUE(moduleOp); - - auto emitted = qc::translateQCToOpenQASM3(*moduleOp); - - ASSERT_TRUE(succeeded(emitted)); - EXPECT_EQ(emitted->find("if ("), std::string::npos) << *emitted; - EXPECT_TRUE(oq3::frontend::analyzeOpenQASM( - *emitted, {.gatePolicy = oq3::frontend::GatePolicy::Strict})) - << *emitted; -} - -TEST(OpenQASM3EmissionTest, DoesNotFuseMeasurementsWithMultipleUses) { - constexpr llvm::StringLiteral source = R"mlir( -module { - func.func @main() -> (!cbit.reg<1>, i1) { - %bits = cbit.alloc(#cbit.init) {mqt.register_name = "bits"} - : !cbit.reg<1> - %qubit = qc.alloc : !qc.qubit - %zero = arith.constant 0 : index - %measured = qc.measure %qubit : !qc.qubit -> i1 - cbit.store %measured, %bits[%zero] : !cbit.reg<1> - qc.dealloc %qubit : !qc.qubit - return %bits, %measured : !cbit.reg<1>, i1 - } -} -)mlir"; - const DialectRegistry registry = emissionDialects(); - MLIRContext context(registry); - auto moduleOp = parseSourceString(source, &context); - ASSERT_TRUE(moduleOp); - - auto emitted = qc::translateQCToOpenQASM3(*moduleOp); - - ASSERT_TRUE(succeeded(emitted)); - EXPECT_NE(emitted->find("bit _mqt_b0 = measure"), std::string::npos); - EXPECT_EQ(emitted->find("bits[0] = measure"), std::string::npos); - EXPECT_TRUE(oq3::frontend::analyzeOpenQASM( - *emitted, {.gatePolicy = oq3::frontend::GatePolicy::Strict})) - << *emitted; -} - -TEST(OpenQASM3EmissionTest, - DoesNotInferZeroAcrossAnUnrepresentedRegisterWrite) { - constexpr llvm::StringLiteral source = R"mlir( -module { - func.func @main() -> !cbit.reg<2> { - %bits = cbit.alloc(#cbit.init) {mqt.register_name = "bits"} - : !cbit.reg<2> - %firstQubit = qc.alloc : !qc.qubit - %secondQubit = qc.alloc : !qc.qubit - %zero = arith.constant 0 : index - %one = arith.constant 1 : index - %first = qc.measure %firstQubit : !qc.qubit -> i1 - cbit.store %first, %bits[%zero] : !cbit.reg<2> - %second = qc.measure %secondQubit : !qc.qubit -> i1 - cbit.store %second, %bits[%one] : !cbit.reg<2> - scf.if %second { - qc.x %firstQubit : !qc.qubit - } - qc.dealloc %firstQubit : !qc.qubit - qc.dealloc %secondQubit : !qc.qubit - return %bits : !cbit.reg<2> - } -} -)mlir"; - const DialectRegistry registry = emissionDialects(); - MLIRContext context(registry); - auto moduleOp = parseSourceString(source, &context); - ASSERT_TRUE(moduleOp); - - auto emitted = qc::translateQCToOpenQASM3(*moduleOp); - - ASSERT_TRUE(succeeded(emitted)); - EXPECT_EQ(emitted->find("if (bits == 2)"), std::string::npos) << *emitted; - EXPECT_NE(emitted->find("if (_mqt_b"), std::string::npos) << *emitted; - EXPECT_TRUE(oq3::frontend::analyzeOpenQASM( - *emitted, {.gatePolicy = oq3::frontend::GatePolicy::Strict})) - << *emitted; -} - -TEST(OpenQASM3EmissionTest, - RejectsRegisterEqualityWhenTheRegisterChangesBeforeUse) { +TEST(OpenQASM3EmissionTest, EmitsRegisterComparisonsDirectly) { constexpr llvm::StringLiteral source = R"mlir( module { - func.func @main() { - %bits = cbit.alloc(#cbit.init) : !cbit.reg<2> - %qubit = qc.alloc : !qc.qubit - %zero = arith.constant 0 : index - %one = arith.constant 1 : index - %false = arith.constant false - %true = arith.constant true - cbit.store %false, %bits[%zero] : !cbit.reg<2> - cbit.store %true, %bits[%one] : !cbit.reg<2> - %first = cbit.load %bits[%zero] : !cbit.reg<2> - %condition = scf.if %first -> i1 { - scf.yield %false : i1 - } else { - %second = cbit.load %bits[%one] : !cbit.reg<2> - scf.yield %second : i1 - } - cbit.store %false, %bits[%one] : !cbit.reg<2> + func.func @main() -> !cbit.reg<3> attributes {mqt.entry_point} { + %q = qc.alloc : !qc.qubit + %c = cbit.alloc(#cbit.init) {mqt.register_name = "c"} + : !cbit.reg<3> + %condition = cbit.cmp uge, %c, 5 : i3 : !cbit.reg<3> scf.if %condition { - qc.x %qubit : !qc.qubit + qc.x %q : !qc.qubit } - qc.dealloc %qubit : !qc.qubit - return + return %c : !cbit.reg<3> } } )mlir"; - const DialectRegistry registry = emissionDialects(); - MLIRContext context(registry); - auto moduleOp = parseSourceString(source, &context); - ASSERT_TRUE(moduleOp); - - EXPECT_TRUE(failed(qc::translateQCToOpenQASM3(*moduleOp))); -} - -TEST(OpenQASM3EmissionTest, DoesNotReuseRegisterEqualityAfterInterveningStore) { - constexpr llvm::StringLiteral source = R"mlir( -module { - func.func @main() -> !cbit.reg<1> { - %bits = cbit.alloc(#cbit.init) {mqt.register_name = "bits"} - : !cbit.reg<1> - %qubit = qc.alloc : !qc.qubit - %zero = arith.constant 0 : index - %false = arith.constant false - %measured = qc.measure %qubit : !qc.qubit -> i1 - cbit.store %measured, %bits[%zero] : !cbit.reg<1> - scf.if %measured { - qc.x %qubit : !qc.qubit - } - cbit.store %false, %bits[%zero] : !cbit.reg<1> - scf.if %measured { - qc.h %qubit : !qc.qubit - } - qc.dealloc %qubit : !qc.qubit - return %bits : !cbit.reg<1> - } -} -)mlir"; - const DialectRegistry registry = emissionDialects(); - MLIRContext context(registry); - auto moduleOp = parseSourceString(source, &context); - ASSERT_TRUE(moduleOp); - - auto emitted = qc::translateQCToOpenQASM3(*moduleOp); - - ASSERT_TRUE(succeeded(emitted)); - const auto firstCondition = emitted->find("if (bits == 1)"); - const auto overwrite = emitted->find("bits[0] = false;"); - const auto secondCondition = emitted->find("if (_mqt_b0)"); - ASSERT_NE(firstCondition, std::string::npos) << *emitted; - ASSERT_NE(overwrite, std::string::npos) << *emitted; - ASSERT_NE(secondCondition, std::string::npos) << *emitted; - EXPECT_LT(firstCondition, overwrite); - EXPECT_LT(overwrite, secondCondition); - EXPECT_EQ(emitted->find("if (bits == 1)", firstCondition + 1), - std::string::npos) - << *emitted; - EXPECT_NE(emitted->find("bit _mqt_b0 = measure"), std::string::npos) - << *emitted; - EXPECT_TRUE(oq3::frontend::analyzeOpenQASM( - *emitted, {.gatePolicy = oq3::frontend::GatePolicy::Strict})) - << *emitted; -} - -TEST(OpenQASM3EmissionTest, DoesNotFuseMeasurementsUsedByUnfoldedExpressions) { - constexpr llvm::StringLiteral source = R"mlir( -module { - func.func @main() -> !cbit.reg<1> { - %bits = cbit.alloc(#cbit.init) {mqt.register_name = "bits"} - : !cbit.reg<1> - %qubit = qc.alloc : !qc.qubit - %zero = arith.constant 0 : index - %false = arith.constant false - %true = arith.constant true - %measured = qc.measure %qubit : !qc.qubit -> i1 - cbit.store %measured, %bits[%zero] : !cbit.reg<1> - %negated = arith.xori %measured, %true : i1 - scf.if %negated { - qc.x %qubit : !qc.qubit - } - cbit.store %false, %bits[%zero] : !cbit.reg<1> - scf.if %negated { - qc.h %qubit : !qc.qubit - } - qc.dealloc %qubit : !qc.qubit - return %bits : !cbit.reg<1> - } -} -)mlir"; - const DialectRegistry registry = emissionDialects(); - MLIRContext context(registry); - auto moduleOp = parseSourceString(source, &context); - ASSERT_TRUE(moduleOp); - - auto emitted = qc::translateQCToOpenQASM3(*moduleOp); - - ASSERT_TRUE(succeeded(emitted)); - EXPECT_NE(emitted->find("bit _mqt_b0 = measure"), std::string::npos) - << *emitted; - EXPECT_NE(emitted->find("bits[0] = _mqt_b0;"), std::string::npos) << *emitted; - EXPECT_EQ(emitted->find("bits[0] = measure"), std::string::npos) << *emitted; - EXPECT_NE(emitted->find("if (bits == 0)"), std::string::npos) << *emitted; - EXPECT_NE(emitted->find("if ((!_mqt_b0))"), std::string::npos) << *emitted; - auto analyzed = oq3::frontend::analyzeOpenQASM( - *emitted, {.gatePolicy = oq3::frontend::GatePolicy::Strict}); - ASSERT_TRUE(analyzed) << analyzed.diagnostics.front().message << '\n' - << *emitted; -} - -TEST(OpenQASM3EmissionTest, DoesNotUseStoreAfterConsumerForRegisterEquality) { - constexpr llvm::StringLiteral source = R"mlir( -module { - func.func @main() -> !cbit.reg<1> { - %bits = cbit.alloc(#cbit.init) {mqt.register_name = "bits"} - : !cbit.reg<1> - %qubit = qc.alloc : !qc.qubit - %zero = arith.constant 0 : index - %measured = qc.measure %qubit : !qc.qubit -> i1 - scf.if %measured { - qc.x %qubit : !qc.qubit - } - cbit.store %measured, %bits[%zero] : !cbit.reg<1> - qc.dealloc %qubit : !qc.qubit - return %bits : !cbit.reg<1> - } -} -)mlir"; - const DialectRegistry registry = emissionDialects(); + DialectRegistry registry = emissionDialects(); MLIRContext context(registry); auto moduleOp = parseSourceString(source, &context); ASSERT_TRUE(moduleOp); @@ -673,15 +291,7 @@ module { auto emitted = qc::translateQCToOpenQASM3(*moduleOp); ASSERT_TRUE(succeeded(emitted)); - const auto measurement = emitted->find("bit _mqt_b0 = measure"); - const auto condition = emitted->find("if (_mqt_b0)"); - const auto store = emitted->find("bits[0] = _mqt_b0;"); - ASSERT_NE(measurement, std::string::npos) << *emitted; - ASSERT_NE(condition, std::string::npos) << *emitted; - ASSERT_NE(store, std::string::npos) << *emitted; - EXPECT_LT(measurement, condition); - EXPECT_LT(condition, store); - EXPECT_EQ(emitted->find("if (bits == 1)"), std::string::npos) << *emitted; + EXPECT_NE(emitted->find("if ((c >= 5))"), std::string::npos) << *emitted; EXPECT_TRUE(oq3::frontend::analyzeOpenQASM( *emitted, {.gatePolicy = oq3::frontend::GatePolicy::Strict})) << *emitted; @@ -1357,50 +967,6 @@ TEST(OpenQASM3EmissionTest, RejectsUnsupportedSubsetConcerns) { return %value : i64 } })mlir"}, - Fixture{.name = "partial-register-equality", .source = R"mlir(module { - func.func @main() { - %bits = cbit.alloc(#cbit.init) : !cbit.reg<3> - %qubit = qc.alloc : !qc.qubit - %zero = arith.constant 0 : index - %one = arith.constant 1 : index - %false = arith.constant false - %first = cbit.load %bits[%zero] : !cbit.reg<3> - %second = cbit.load %bits[%one] : !cbit.reg<3> - %condition = scf.if %first -> i1 { - scf.yield %second : i1 - } else { - scf.yield %false : i1 - } - scf.if %condition { - qc.x %qubit : !qc.qubit - } - qc.dealloc %qubit : !qc.qubit - return - } - })mlir"}, - Fixture{.name = "side-effecting-register-equality", - .source = R"mlir(module { - func.func @main() { - %bits = cbit.alloc(#cbit.init) : !cbit.reg<2> - %qubit = qc.alloc : !qc.qubit - %zero = arith.constant 0 : index - %one = arith.constant 1 : index - %false = arith.constant false - %first = cbit.load %bits[%zero] : !cbit.reg<2> - %second = cbit.load %bits[%one] : !cbit.reg<2> - %condition = scf.if %first -> i1 { - cbit.store %false, %bits[%one] : !cbit.reg<2> - scf.yield %second : i1 - } else { - scf.yield %false : i1 - } - scf.if %condition { - qc.x %qubit : !qc.qubit - } - qc.dealloc %qubit : !qc.qubit - return - } - })mlir"}, Fixture{.name = "for-iterated-state", .source = R"mlir(module { func.func @main() -> i64 { %zero = arith.constant 0 : index diff --git a/mlir/unittests/Dialect/QCO/Utils/test_dd_functionality.cpp b/mlir/unittests/Dialect/QCO/Utils/test_dd_functionality.cpp index 9256119ae7..789f88860c 100644 --- a/mlir/unittests/Dialect/QCO/Utils/test_dd_functionality.cpp +++ b/mlir/unittests/Dialect/QCO/Utils/test_dd_functionality.cpp @@ -16,6 +16,7 @@ #include "dd/StateGeneration.hpp" #include "mlir/Dialect/CBit/IR/CBitAttributes.h" #include "mlir/Dialect/CBit/IR/CBitDialect.h" +#include "mlir/Dialect/CBit/IR/CBitOps.h" #include "mlir/Dialect/QCO/Builder/QCOProgramBuilder.h" #include "mlir/Dialect/QCO/IR/QCODialect.h" #include "mlir/Dialect/QCO/Utils/DDFunctionality.h" @@ -39,6 +40,7 @@ #include #include +#include #include #include #include @@ -796,6 +798,33 @@ TEST_F(QCODDFunctionalityTest, SimulateCBitConditionAndMeasurementUpdate) { dd->decRef(one); } +TEST_F(QCODDFunctionalityTest, SimulateCBitRegisterComparisons) { + constexpr std::array comparisons{ + std::pair{cbit::ComparisonPredicate::Equal, false}, + std::pair{cbit::ComparisonPredicate::NotEqual, true}, + std::pair{cbit::ComparisonPredicate::Less, true}, + std::pair{cbit::ComparisonPredicate::LessEqual, true}, + std::pair{cbit::ComparisonPredicate::Greater, false}, + std::pair{cbit::ComparisonPredicate::GreaterEqual, false}, + }; + for (const auto [predicate, expected] : comparisons) { + auto mod = buildModule([&](QCOProgramBuilder& b) { + auto reg = b.allocClassicalBitRegister(2, "c"); + auto rhs = b.getIntegerAttr(b.getIntegerType(2), 1); + auto condition = + cbit::CompareOp::create(b, b.getI1Type(), predicate, reg, rhs); + auto q = b.staticQubit(0); + q = b.qcoIf( + condition, q, [&](Value arg) { return b.x(arg); }, + [&](Value arg) { return arg; }); + b.sink(q); + return b.intConstant(0); + }); + ASSERT_TRUE(mod); + expectSimulatesFromZero(mainFunc(*mod), expected); + } +} + TEST_F(QCODDFunctionalityTest, RejectsUndefinedCBitLoad) { auto mod = buildModule([](QCOProgramBuilder& b) { auto reg = diff --git a/mlir/unittests/Target/OpenQASM/test_openqasm_emitter.cpp b/mlir/unittests/Target/OpenQASM/test_openqasm_emitter.cpp index 129bef964f..841bd0ab70 100644 --- a/mlir/unittests/Target/OpenQASM/test_openqasm_emitter.cpp +++ b/mlir/unittests/Target/OpenQASM/test_openqasm_emitter.cpp @@ -1258,60 +1258,28 @@ if (c == 1) x q[0]; ASSERT_TRUE(moduleOp); ASSERT_TRUE(succeeded(verify(*moduleOp))); size_t conditionals = 0; + size_t comparisons = 0; moduleOp->walk([&](scf::IfOp) { ++conditionals; }); - // The register equality and the source-level branch each short-circuit - // through their own structured conditional. - EXPECT_EQ(conditionals, 2); -} - -TEST(OpenQASMTargetTest, - SharesOpenQASM2RegisterConditionsUntilClassicalMutation) { - constexpr llvm::StringLiteral source = R"qasm( -OPENQASM 2.0; -include "qelib1.inc"; -qreg q[2]; -creg c[2]; -measure q[0] -> c[0]; -if (c == 1) x q[1]; -if (c == 1) h q[1]; -measure q[1] -> c[1]; -if (c == 1) z q[0]; -)qasm"; - - MLIRContext context; - auto moduleOp = qc::translateQASM3ToQC(source, &context); - ASSERT_TRUE(moduleOp); - ASSERT_TRUE(succeeded(verify(*moduleOp))); - - SmallVector branchConditions; - size_t expressionConditionals = 0; - size_t classicalLoads = 0; - moduleOp->walk([&](scf::IfOp conditional) { - if (conditional.getNumResults() == 0) { - branchConditions.push_back(conditional.getCondition()); - } else { - ++expressionConditionals; - } - }); - moduleOp->walk([&](cbit::LoadOp) { ++classicalLoads; }); - - ASSERT_EQ(branchConditions.size(), 3); - EXPECT_EQ(branchConditions[0], branchConditions[1]); - EXPECT_NE(branchConditions[1], branchConditions[2]); - EXPECT_EQ(expressionConditionals, 4); - EXPECT_EQ(classicalLoads, 4); + moduleOp->walk([&](cbit::CompareOp) { ++comparisons; }); + EXPECT_EQ(conditionals, 1); + EXPECT_EQ(comparisons, 1); } -TEST(OpenQASMTargetTest, DoesNotReuseLoopLocalConditionAfterPositiveRangeLoop) { +TEST(OpenQASMTargetTest, EmitsAllRegisterComparisonPredicates) { constexpr llvm::StringLiteral source = R"qasm( OPENQASM 3.1; include "stdgates.inc"; -qubit[2] q; -bit c = measure q[0]; -for int i in [0:1] { - if (c) { x q[1]; } -} -if (c) { h q[1]; } +bit[3] c; +c[0] = true; +c[1] = false; +c[2] = true; +qubit q; +if (c == 5) { x q; } +if (c != 5) { x q; } +if (c < 5) { x q; } +if (c <= 5) { x q; } +if (c > 5) { x q; } +if (c >= 5) { x q; } )qasm"; MLIRContext context; @@ -1319,47 +1287,30 @@ if (c) { h q[1]; } ASSERT_TRUE(moduleOp); ASSERT_TRUE(succeeded(verify(*moduleOp))); - SmallVector branchConditions; - moduleOp->walk([&](scf::IfOp conditional) { - if (conditional.getNumResults() == 0) { - branchConditions.push_back(conditional.getCondition()); - } + std::array predicates{}; + moduleOp->walk([&](cbit::CompareOp comparison) { + predicates.at(static_cast(comparison.getPredicate())) = true; + EXPECT_EQ(comparison.getRhs(), llvm::APInt(3, 5)); }); - - ASSERT_EQ(branchConditions.size(), 2); - EXPECT_NE(branchConditions[0], branchConditions[1]); + EXPECT_TRUE(llvm::all_of(predicates, [](const bool value) { return value; })); } -TEST(OpenQASMTargetTest, InvalidatesPositiveRangeLoopConditionsAcrossMutation) { +TEST(OpenQASMTargetTest, PreservesWideRegisterComparisons) { constexpr llvm::StringLiteral source = R"qasm( -OPENQASM 3.1; -include "stdgates.inc"; -qubit[2] q; -bit c = measure q[0]; -if (c) { x q[1]; } -for int i in [0:1] { - if (c) { h q[1]; } - c = false; -} -if (c) { z q[1]; } +OPENQASM 2.0; +include "qelib1.inc"; +qreg q[1]; +creg c[65]; +if (c == 18446744073709551616) x q[0]; )qasm"; MLIRContext context; auto moduleOp = qc::translateQASM3ToQC(source, &context); ASSERT_TRUE(moduleOp); - ASSERT_TRUE(succeeded(verify(*moduleOp))); - - SmallVector branchConditions; - moduleOp->walk([&](scf::IfOp conditional) { - if (conditional.getNumResults() == 0) { - branchConditions.push_back(conditional.getCondition()); - } - }); - - ASSERT_EQ(branchConditions.size(), 3); - EXPECT_NE(branchConditions[0], branchConditions[1]); - EXPECT_NE(branchConditions[1], branchConditions[2]); - EXPECT_NE(branchConditions[0], branchConditions[2]); + cbit::CompareOp comparison; + moduleOp->walk([&](cbit::CompareOp op) { comparison = op; }); + ASSERT_TRUE(comparison); + EXPECT_EQ(comparison.getRhs(), llvm::APInt(65, 1).shl(64)); } TEST(OpenQASMTargetTest, ZeroInitializesUnmeasuredOpenQASM2Registers) { diff --git a/mlir/unittests/Target/OpenQASM/test_openqasm_semantics.cpp b/mlir/unittests/Target/OpenQASM/test_openqasm_semantics.cpp index 9716285b46..f138e1cee8 100644 --- a/mlir/unittests/Target/OpenQASM/test_openqasm_semantics.cpp +++ b/mlir/unittests/Target/OpenQASM/test_openqasm_semantics.cpp @@ -407,6 +407,12 @@ OPENQASM 3.1; qubit q; bit c; if (c) { x q; } +)qasm"; + constexpr llvm::StringLiteral unmeasuredRegisterCondition = R"qasm( +OPENQASM 3.1; +qubit q; +bit[2] c; +if (c >= 1) { x q; } )qasm"; auto uninitializedOutput = oq3::frontend::analyzeOpenQASM(unmeasuredOutput); @@ -423,6 +429,14 @@ if (c) { x q; } EXPECT_NE(uninitializedCondition.diagnostics.front().message.find( "has not been initialized"), std::string::npos); + + auto uninitializedRegister = + oq3::frontend::analyzeOpenQASM(unmeasuredRegisterCondition); + ASSERT_FALSE(uninitializedRegister); + ASSERT_FALSE(uninitializedRegister.diagnostics.empty()); + EXPECT_NE(uninitializedRegister.diagnostics.front().message.find( + "has not been initialized"), + std::string::npos); } TEST(OpenQASMFrontendTest, RejectsUninitializedScalarOutputs) { @@ -1651,52 +1665,11 @@ if(c==1180591620717411303433) x q[0]; auto analyzed = oq3::frontend::analyzeOpenQASM(source); ASSERT_TRUE(analyzed) << analyzed.diagnostics.front().message; EXPECT_TRUE(llvm::any_of(analyzed.program->conditions, [](const auto& c) { - return c.kind == oq3::frontend::ConditionKind::Bit && c.bit.index == 70; + return c.kind == oq3::frontend::ConditionKind::RegisterComparison && + c.expected[70]; })); } -TEST(OpenQASMFrontendTest, AcceptsInitializedRegisterConditionInOpenQASM3) { - std::string source = R"qasm( -OPENQASM 3.1; -include "stdgates.inc"; -qubit q; -bit[80] c; -)qasm"; - for (size_t index = 0; index < 80; ++index) { - source += "c[" + std::to_string(index) + "] = false;\n"; - } - source += R"qasm( -c[70] = measure q; -if(c==1180591620717411303424) { x q; } -)qasm"; - - auto analyzed = oq3::frontend::analyzeOpenQASM(source); - - ASSERT_TRUE(analyzed) << analyzed.diagnostics.front().message; - EXPECT_TRUE(llvm::any_of(analyzed.program->conditions, [](const auto& c) { - return c.kind == oq3::frontend::ConditionKind::Bit && c.bit.index == 70; - })); -} - -TEST(OpenQASMFrontendTest, RejectsUninitializedRegisterConditionInOpenQASM3) { - constexpr llvm::StringLiteral source = R"qasm( -OPENQASM 3.1; -include "stdgates.inc"; -qubit q; -bit[2] c; -c[0] = measure q; -if(c==1) { x q; } -)qasm"; - - auto analyzed = oq3::frontend::analyzeOpenQASM(source); - - ASSERT_FALSE(analyzed); - ASSERT_FALSE(analyzed.diagnostics.empty()); - EXPECT_NE( - analyzed.diagnostics.front().message.find("has not been initialized"), - std::string::npos); -} - TEST(OpenQASMFrontendTest, AcceptsWideIntegerLiteralWithDigitSeparatorsInOpenQASM2If) { // Same value as above, spelled with grammar-legal digit separators. @@ -1711,7 +1684,8 @@ if(c==1_180_591_620_717_411_303_433) x q[0]; auto analyzed = oq3::frontend::analyzeOpenQASM(source); ASSERT_TRUE(analyzed) << analyzed.diagnostics.front().message; EXPECT_TRUE(llvm::any_of(analyzed.program->conditions, [](const auto& c) { - return c.kind == oq3::frontend::ConditionKind::Bit && c.bit.index == 70; + return c.kind == oq3::frontend::ConditionKind::RegisterComparison && + c.expected[70]; })); } @@ -1753,12 +1727,10 @@ if(c==1) x q[0]; )qasm"; auto analyzed = oq3::frontend::analyzeOpenQASM(source); ASSERT_TRUE(analyzed) << analyzed.diagnostics.front().message; - // Truncating to 64 bits would omit Not(c[79]). - EXPECT_TRUE(llvm::any_of(analyzed.program->conditions, [&](const auto& c) { - return c.kind == oq3::frontend::ConditionKind::Not && - analyzed.program->conditions[c.lhs].kind == - oq3::frontend::ConditionKind::Bit && - analyzed.program->conditions[c.lhs].bit.index == 79; + /// Truncating to 64 bits would omit the leading zero bits. + EXPECT_TRUE(llvm::any_of(analyzed.program->conditions, [](const auto& c) { + return c.kind == oq3::frontend::ConditionKind::RegisterComparison && + c.expected.getBitWidth() == 80U && c.expected == 1U; })); } diff --git a/test/python/test_mlir_qiskit_translation.py b/test/python/test_mlir_qiskit_translation.py index 4cbf646df6..a5af649681 100644 --- a/test/python/test_mlir_qiskit_translation.py +++ b/test/python/test_mlir_qiskit_translation.py @@ -575,7 +575,33 @@ def test_cleanup_forwards_measurement_results_to_qiskit_condition() -> None: assert restored.data[2].operation.blocks[0].count_ops() == {"x": 1} condition = restored.data[2].operation.condition assert isinstance(condition, expr.Expr) - assert expr.structurally_equivalent(condition, expr.logic_and(*restored.clbits)) + assert expr.structurally_equivalent(condition, expr.equal(restored.cregs[0], 3)) + + +def test_openqasm_register_ordering_exports_to_qiskit_expression() -> None: + """Export first-class register ordering as a Qiskit Uint expression.""" + program = QCProgram.from_qasm_str( + """OPENQASM 3.1; +include "stdgates.inc"; +qubit[3] q; +bit[2] c; +c[0] = measure q[0]; +c[1] = measure q[1]; +if (c >= 1) { x q[2]; } +""" + ) + + restored = program.to_qiskit() + condition = restored.data[2].operation.condition + + assert "cbit.cmp uge" in program.ir + assert isinstance(condition, expr.Expr) + assert expr.structurally_equivalent(condition, expr.greater_equal(restored.cregs[0], 1)) + + reimported = QCProgram.from_qiskit(restored).to_qiskit() + reimported_condition = reimported.data[2].operation.condition + assert isinstance(reimported_condition, expr.Expr) + assert expr.structurally_equivalent(reimported_condition, expr.greater_equal(reimported.cregs[0], 1)) def test_openqasm_short_circuit_expression_exports_to_qiskit() -> None: From 1315ab2cf88eefccede297fc40ff8630af3cb699 Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Wed, 2 Sep 2026 18:15:43 +0000 Subject: [PATCH 05/13] =?UTF-8?q?=F0=9F=A7=AA=20Cover=20register=20compari?= =?UTF-8?q?son=20contracts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Exercise every comparison predicate through memref lowering and OpenQASM export. Cover the explicit QIR Base rejection and undefined-register DD failure. Assisted-by: Codex Signed-off-by: Lukas Burgholzer --- .../CBitToMemRef/test_cbit_to_memref.cpp | 13 ++++++--- .../QCToQIRBase/test_qc_to_qir_base.cpp | 28 ++++++++++++++++++ .../Translation/test_openqasm3_emission.cpp | 29 +++++++++++++++++-- .../QCO/Utils/test_dd_functionality.cpp | 22 ++++++++++++++ 4 files changed, 85 insertions(+), 7 deletions(-) diff --git a/mlir/unittests/Conversion/CBitToMemRef/test_cbit_to_memref.cpp b/mlir/unittests/Conversion/CBitToMemRef/test_cbit_to_memref.cpp index 24feb52681..8ed0ac25e2 100644 --- a/mlir/unittests/Conversion/CBitToMemRef/test_cbit_to_memref.cpp +++ b/mlir/unittests/Conversion/CBitToMemRef/test_cbit_to_memref.cpp @@ -137,10 +137,15 @@ TEST_F(CBitToMemRefTest, LargeZeroInitializationProducesBoundedIR) { TEST_F(CBitToMemRefTest, LowersRegisterComparisons) { auto moduleOp = convert(R"mlir( module { - func.func @main() -> i1 { + func.func @main() -> (i1, i1, i1, i1, i1, i1) { %reg = cbit.alloc(#cbit.init) : !cbit.reg<3> - %result = cbit.cmp uge, %reg, 5 : i3 : !cbit.reg<3> - return %result : i1 + %eq = cbit.cmp eq, %reg, 5 : i3 : !cbit.reg<3> + %ne = cbit.cmp ne, %reg, 5 : i3 : !cbit.reg<3> + %ult = cbit.cmp ult, %reg, 5 : i3 : !cbit.reg<3> + %ule = cbit.cmp ule, %reg, 5 : i3 : !cbit.reg<3> + %ugt = cbit.cmp ugt, %reg, 5 : i3 : !cbit.reg<3> + %uge = cbit.cmp uge, %reg, 5 : i3 : !cbit.reg<3> + return %eq, %ne, %ult, %ule, %ugt, %uge : i1, i1, i1, i1, i1, i1 } } )mlir"); @@ -154,7 +159,7 @@ TEST_F(CBitToMemRefTest, LowersRegisterComparisons) { EXPECT_FALSE(containsCBit); size_t loads = 0; moduleOp->walk([&](memref::LoadOp) { ++loads; }); - EXPECT_EQ(loads, 3); + EXPECT_EQ(loads, 18); } TEST_F(CBitToMemRefTest, ConvertsFunctionSignaturesCallsAndReturns) { diff --git a/mlir/unittests/Conversion/QCToQIR/QCToQIRBase/test_qc_to_qir_base.cpp b/mlir/unittests/Conversion/QCToQIR/QCToQIRBase/test_qc_to_qir_base.cpp index 41bfced7c1..e92df59f35 100644 --- a/mlir/unittests/Conversion/QCToQIR/QCToQIRBase/test_qc_to_qir_base.cpp +++ b/mlir/unittests/Conversion/QCToQIR/QCToQIRBase/test_qc_to_qir_base.cpp @@ -11,6 +11,8 @@ #include "Support/IRVerification.h" #include "TestCaseUtils.h" #include "mlir/Conversion/QCToQIR/QIRBase/QCToQIRBase.h" +#include "mlir/Dialect/CBit/IR/CBitDialect.h" +#include "mlir/Dialect/CBit/IR/CBitOps.h" #include "mlir/Dialect/MQT/Transforms/Passes.h" #include "mlir/Dialect/QC/Builder/QCProgramBuilder.h" #include "mlir/Dialect/QC/IR/QCDialect.h" @@ -157,6 +159,32 @@ TEST(QCToQIRBaseNativeTest, RejectsMultiBlockEntryFunctionWithoutMutation) { EXPECT_EQ(entryPoint.getBlocks().size(), 2); } +TEST(QCToQIRBaseNativeTest, RejectsClassicalRegisterComparisons) { + MLIRContext context; + context.loadDialect(); + qc::QCProgramBuilder builder(&context); + builder.initialize(); + auto reg = builder.allocClassicalBitRegister(1); + auto rhs = builder.getIntegerAttr(builder.getIntegerType(1), 0); + (void)cbit::CompareOp::create(builder, builder.getI1Type(), + cbit::ComparisonPredicate::Equal, reg, rhs); + auto module = builder.finalize(); + ASSERT_TRUE(module); + + bool sawExpectedDiagnostic = false; + ScopedDiagnosticHandler handler(&context, [&](Diagnostic& diagnostic) { + std::string message; + llvm::raw_string_ostream stream(message); + diagnostic.print(stream); + sawExpectedDiagnostic |= StringRef(message).contains( + "QIR Base Profile does not support classical-register comparisons"); + return success(); + }); + EXPECT_TRUE(failed(runQCToQIRBaseConversion(*module))); + EXPECT_TRUE(sawExpectedDiagnostic); +} + TEST(QCToQIRBaseNativeTest, ControlledBarrierDoesNotControlFollowingGate) { expectFollowingXIsUncontrolled( [](qc::QCProgramBuilder& builder, Value control, Value target) { diff --git a/mlir/unittests/Dialect/QC/Translation/test_openqasm3_emission.cpp b/mlir/unittests/Dialect/QC/Translation/test_openqasm3_emission.cpp index 2eeb419ba9..934e2a2e6d 100644 --- a/mlir/unittests/Dialect/QC/Translation/test_openqasm3_emission.cpp +++ b/mlir/unittests/Dialect/QC/Translation/test_openqasm3_emission.cpp @@ -275,8 +275,28 @@ module { %q = qc.alloc : !qc.qubit %c = cbit.alloc(#cbit.init) {mqt.register_name = "c"} : !cbit.reg<3> - %condition = cbit.cmp uge, %c, 5 : i3 : !cbit.reg<3> - scf.if %condition { + %eq = cbit.cmp eq, %c, 5 : i3 : !cbit.reg<3> + %ne = cbit.cmp ne, %c, 5 : i3 : !cbit.reg<3> + %ult = cbit.cmp ult, %c, 5 : i3 : !cbit.reg<3> + %ule = cbit.cmp ule, %c, 5 : i3 : !cbit.reg<3> + %ugt = cbit.cmp ugt, %c, 5 : i3 : !cbit.reg<3> + %uge = cbit.cmp uge, %c, 5 : i3 : !cbit.reg<3> + scf.if %eq { + qc.x %q : !qc.qubit + } + scf.if %ne { + qc.x %q : !qc.qubit + } + scf.if %ult { + qc.x %q : !qc.qubit + } + scf.if %ule { + qc.x %q : !qc.qubit + } + scf.if %ugt { + qc.x %q : !qc.qubit + } + scf.if %uge { qc.x %q : !qc.qubit } return %c : !cbit.reg<3> @@ -291,7 +311,10 @@ module { auto emitted = qc::translateQCToOpenQASM3(*moduleOp); ASSERT_TRUE(succeeded(emitted)); - EXPECT_NE(emitted->find("if ((c >= 5))"), std::string::npos) << *emitted; + for (const auto* comparison : + {"c == 5", "c != 5", "c < 5", "c <= 5", "c > 5", "c >= 5"}) { + EXPECT_NE(emitted->find(comparison), std::string::npos) << *emitted; + } EXPECT_TRUE(oq3::frontend::analyzeOpenQASM( *emitted, {.gatePolicy = oq3::frontend::GatePolicy::Strict})) << *emitted; diff --git a/mlir/unittests/Dialect/QCO/Utils/test_dd_functionality.cpp b/mlir/unittests/Dialect/QCO/Utils/test_dd_functionality.cpp index 789f88860c..d1c1f7a9e5 100644 --- a/mlir/unittests/Dialect/QCO/Utils/test_dd_functionality.cpp +++ b/mlir/unittests/Dialect/QCO/Utils/test_dd_functionality.cpp @@ -844,6 +844,28 @@ TEST_F(QCODDFunctionalityTest, RejectsUndefinedCBitLoad) { failed(simulate(mainFunc(*mod), dd::makeZeroState(1, *dd), *dd, rng))); } +TEST_F(QCODDFunctionalityTest, RejectsUndefinedCBitRegisterComparison) { + auto mod = buildModule([](QCOProgramBuilder& b) { + auto reg = + b.allocClassicalBitRegister(1, "c", cbit::Initialization::Undefined); + auto rhs = b.getIntegerAttr(b.getIntegerType(1), 0); + auto condition = cbit::CompareOp::create( + b, b.getI1Type(), cbit::ComparisonPredicate::Equal, reg, rhs); + auto q = b.staticQubit(0); + q = b.qcoIf( + condition, q, [&](Value arg) { return arg; }, + [&](Value arg) { return arg; }); + b.sink(q); + return b.intConstant(0); + }); + ASSERT_TRUE(mod); + + auto dd = std::make_unique(1); + std::mt19937_64 rng(1); + EXPECT_TRUE( + failed(simulate(mainFunc(*mod), dd::makeZeroState(1, *dd), *dd, rng))); +} + TEST_F(QCODDFunctionalityTest, SimulateMeasureFeedsIndexSwitch) { // |1> → measure → index_castui → index_switch case 1 applies X → |0>. auto mod = buildModule([](QCOProgramBuilder& b) { From 42b0fc85a8b245b7bf20115adb1d49864f76e24b Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Wed, 2 Sep 2026 19:11:34 +0000 Subject: [PATCH 06/13] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Canonicalize=20Qiski?= =?UTF-8?q?t=20register=20conditions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Normalize Qiskit tuple conditions at the Python boundary and emit typed expressions on export. Reuse cbit.cmp for complete-register comparisons, including reversed operands, and fold out-of-range equality to false. Simplify zero-register comparison folding to inspect actual users. Assisted-by: Codex Signed-off-by: Lukas Burgholzer --- bindings/mlir/qiskit/Qiskit2_5.cpp | 59 ++------ bindings/mlir/qiskit/QiskitExport.cpp | 33 ----- bindings/mlir/qiskit/QiskitImport.cpp | 140 +++++++++++++----- bindings/mlir/qiskit/QiskitTranslation.h | 2 - mlir/lib/Dialect/CBit/IR/CBitOps.cpp | 18 +-- .../Dialect/CBit/IR/test_cbit_ir.cpp | 15 +- test/python/test_mlir_qiskit_translation.py | 77 +++++++++- 7 files changed, 212 insertions(+), 132 deletions(-) diff --git a/bindings/mlir/qiskit/Qiskit2_5.cpp b/bindings/mlir/qiskit/Qiskit2_5.cpp index cecf5e9664..ccb66db6b5 100644 --- a/bindings/mlir/qiskit/Qiskit2_5.cpp +++ b/bindings/mlir/qiskit/Qiskit2_5.cpp @@ -1189,14 +1189,16 @@ class NativeControlFlowReader final : public ControlFlowReader { throw std::runtime_error( "Qiskit classical-bit condition must compare against zero or one"); } - result.expectedBit = expected != 0U; - return result; + return normalizePythonTarget(expressionModule.attr("equal")( + condition[0], nb::bool_(expected != 0U))); } if (result.kind == ClassicalTargetKind::ClassicalRegister) { - result.width = static_cast( - std::max(result.reg.bits.size(), std::bit_width(expected))); - result.expectedRegister = expected; - return result; + if (std::bit_width(expected) > result.reg.bits.size()) { + return normalizePythonTarget( + expressionModule.attr("lift")(nb::bool_(false))); + } + return normalizePythonTarget( + expressionModule.attr("equal")(condition[0], nb::int_(expected))); } throw std::runtime_error("Qiskit control flow has an unknown condition " "target"); @@ -1646,34 +1648,15 @@ class PythonClassicalBuilder final { } [[nodiscard]] nb::object condition(const ClassicalTarget& target) const { - switch (target.kind) { - case ClassicalTargetKind::ClassicalBit: - return nb::make_tuple(classicalBit(target.bit), - nb::bool_(target.expectedBit)); - case ClassicalTargetKind::ClassicalRegister: { - validateRegisterValue(target.reg, target.expectedRegister); - if (const auto reg = registeredClassicalRegister(target.reg)) { - return nb::make_tuple(*reg, nb::int_(target.expectedRegister)); - } - const auto packed = packedRegister(target.reg); - const auto expected = expressionModule_.attr("lift")( - nb::int_(target.expectedRegister), - classicalType(ClassicalType::Uint, - static_cast(target.reg.bits.size()))); - return expressionModule_.attr("equal")(packed, expected); + if (target.kind != ClassicalTargetKind::Expression || !target.expression) { + throw std::runtime_error( + "Qiskit control-flow condition has no expression"); } - case ClassicalTargetKind::Expression: - if (!target.expression) { - throw std::runtime_error( - "Qiskit control-flow condition has no expression"); - } - if (target.expression->type != ClassicalType::Bool) { - throw std::runtime_error( - "Qiskit control-flow condition expression must be Boolean"); - } - return expression(*target.expression); + if (target.expression->type != ClassicalType::Bool) { + throw std::runtime_error( + "Qiskit control-flow condition expression must be Boolean"); } - throw std::runtime_error("Qiskit control flow has an unknown condition"); + return expression(*target.expression); } [[nodiscard]] nb::object switchTarget(const ClassicalTarget& target) const { @@ -1752,18 +1735,6 @@ class PythonClassicalBuilder final { return std::nullopt; } - static void validateRegisterValue(const Register& reg, const uint64_t value) { - if (reg.bits.empty() || reg.bits.size() > 64U) { - throw std::runtime_error( - "Qiskit condition registers must contain between 1 and 64 bits"); - } - if (reg.bits.size() < std::numeric_limits::digits && - value >= (uint64_t{1} << reg.bits.size())) { - throw std::runtime_error( - "Qiskit register condition value exceeds its register width"); - } - } - [[nodiscard]] nb::object packedRegister(const Register& reg, const uint32_t expressionWidth = 0U) const { diff --git a/bindings/mlir/qiskit/QiskitExport.cpp b/bindings/mlir/qiskit/QiskitExport.cpp index 4fb6ebb2e5..da5280c355 100644 --- a/bindings/mlir/qiskit/QiskitExport.cpp +++ b/bindings/mlir/qiskit/QiskitExport.cpp @@ -1547,39 +1547,6 @@ exportCondition(mlir::Value value, ExportState& state, "Qiskit control-flow conditions must have Boolean type"); } validateClassicalSnapshot(value, consumer); - if (auto comparison = value.getDefiningOp(); - comparison && - comparison.getPredicate() == mlir::arith::CmpIPredicate::eq) { - for (auto [actual, expected] : - std::array{std::pair{comparison.getLhs(), comparison.getRhs()}, - std::pair{comparison.getRhs(), comparison.getLhs()}}) { - const auto constant = constantUnsignedInteger(expected); - if (!constant) { - continue; - } - if (auto load = actual.getDefiningOp(); - load && actual.getType().isInteger(1) && *constant <= 1U) { - state.expressionOperations.insert(comparison); - state.expressionOperations.insert(load); - return {.kind = ClassicalTargetKind::ClassicalBit, - .bit = classicalBitIndex(load, state), - .expectedBit = *constant != 0U}; - } - if (auto packed = matchPackedRegister(actual, state, evaluationBlock)) { - if (packed->reg.bits.size() != 64U && - *constant >= (uint64_t{1} << packed->reg.bits.size())) { - continue; - } - state.expressionOperations.insert(comparison); - acceptPackedRegister(*packed, state); - return {.kind = ClassicalTargetKind::ClassicalRegister, - .reg = std::move(packed->reg), - .expectedRegister = *constant, - .width = - llvm::cast(actual.getType()).getWidth()}; - } - } - } ClassicalTarget target{.kind = ClassicalTargetKind::Expression}; target.expression = exportExpression(value, state, evaluationBlock); return target; diff --git a/bindings/mlir/qiskit/QiskitImport.cpp b/bindings/mlir/qiskit/QiskitImport.cpp index 2fe44d3594..5f21e0d15a 100644 --- a/bindings/mlir/qiskit/QiskitImport.cpp +++ b/bindings/mlir/qiskit/QiskitImport.cpp @@ -15,6 +15,7 @@ #include "jeff/IR/JeffDialect.h" #include "mlir/Compiler/Programs.h" #include "mlir/Dialect/CBit/IR/CBitDialect.h" +#include "mlir/Dialect/CBit/IR/CBitOps.h" #include "mlir/Dialect/MQT/IR/MQTDialect.h" #include "mlir/Dialect/MQT/Utils/DenseUnitary.h" #include "mlir/Dialect/QC/Builder/QCProgramBuilder.h" @@ -620,20 +621,55 @@ struct ClassicalBitRef { }; } // namespace -[[nodiscard]] static mlir::Value -loadClassicalBit(mlir::qc::QCProgramBuilder& builder, - const llvm::ArrayRef classicalBits, - const llvm::ArrayRef rootClbitMap, - const uint32_t index) { +[[nodiscard]] static const ClassicalBitRef& +classicalBitRef(const llvm::ArrayRef classicalBits, + const llvm::ArrayRef rootClbitMap, uint32_t index) { if (index >= rootClbitMap.size() || rootClbitMap[index] >= classicalBits.size()) { throw std::runtime_error( "Qiskit control flow references an invalid classical bit"); } - const auto& bit = classicalBits[rootClbitMap[index]]; + return classicalBits[rootClbitMap[index]]; +} + +[[nodiscard]] static mlir::Value +loadClassicalBit(mlir::qc::QCProgramBuilder& builder, + const llvm::ArrayRef classicalBits, + const llvm::ArrayRef rootClbitMap, + const uint32_t index) { + const auto& bit = classicalBitRef(classicalBits, rootClbitMap, index); return builder.loadClassicalBit(bit.storage, bit.index); } +[[nodiscard]] static mlir::Value emitRegisterComparison( + mlir::qc::QCProgramBuilder& builder, + const llvm::ArrayRef classicalBits, + const llvm::ArrayRef rootClbitMap, const Register& reg, + mlir::cbit::ComparisonPredicate predicate, uint64_t expected) { + mlir::Value storage; + for (size_t index = 0U; index < reg.bits.size(); ++index) { + const auto& bit = + classicalBitRef(classicalBits, rootClbitMap, reg.bits[index]); + if (bit.index != static_cast(index) || + (storage && bit.storage != storage)) { + return {}; + } + storage = bit.storage; + } + if (!storage) { + return {}; + } + const auto width = static_cast(reg.bits.size()); + if (llvm::cast(storage.getType()).getWidth() != + width) { + return {}; + } + const auto rhs = builder.getIntegerAttr(builder.getIntegerType(width), + llvm::APInt(width, expected, false)); + return mlir::cbit::CompareOp::create(builder, builder.getI1Type(), predicate, + storage, rhs); +} + [[nodiscard]] static mlir::Value packRegister(mlir::qc::QCProgramBuilder& builder, const llvm::ArrayRef classicalBits, @@ -675,6 +711,31 @@ packRegister(mlir::qc::QCProgramBuilder& builder, return terms.front(); } +[[nodiscard]] static std::optional +registerComparisonPredicate(const BinaryOperation operation, + const bool reverse) { + switch (operation) { + case BinaryOperation::Equal: + return mlir::cbit::ComparisonPredicate::Equal; + case BinaryOperation::NotEqual: + return mlir::cbit::ComparisonPredicate::NotEqual; + case BinaryOperation::Less: + return reverse ? mlir::cbit::ComparisonPredicate::Greater + : mlir::cbit::ComparisonPredicate::Less; + case BinaryOperation::LessEqual: + return reverse ? mlir::cbit::ComparisonPredicate::GreaterEqual + : mlir::cbit::ComparisonPredicate::LessEqual; + case BinaryOperation::Greater: + return reverse ? mlir::cbit::ComparisonPredicate::Less + : mlir::cbit::ComparisonPredicate::Greater; + case BinaryOperation::GreaterEqual: + return reverse ? mlir::cbit::ComparisonPredicate::LessEqual + : mlir::cbit::ComparisonPredicate::GreaterEqual; + default: + return std::nullopt; + } +} + [[nodiscard]] static mlir::Value emitExpression(mlir::qc::QCProgramBuilder& builder, const Expression& expression, @@ -708,6 +769,15 @@ emitExpression(mlir::qc::QCProgramBuilder& builder, target); } case ExpressionKind::Cast: { + if (expression.type == ClassicalType::Bool && + expression.left->kind == ExpressionKind::ClassicalRegister && + expression.left->width == expression.left->reg.bits.size()) { + if (auto comparison = emitRegisterComparison( + builder, classicalBits, rootClbitMap, expression.left->reg, + mlir::cbit::ComparisonPredicate::NotEqual, 0U)) { + return comparison; + } + } auto operand = emitExpression(builder, *expression.left, classicalBits, rootClbitMap); if (operand.getType() == resultType) { @@ -784,6 +854,27 @@ emitExpression(mlir::qc::QCProgramBuilder& builder, break; } case ExpressionKind::Binary: { + const auto reverse = + expression.left->kind == ExpressionKind::Value && + expression.right->kind == ExpressionKind::ClassicalRegister; + const auto& registerExpression = + reverse ? *expression.right : *expression.left; + const auto& expected = reverse ? *expression.left : *expression.right; + if (const auto predicate = + registerComparisonPredicate(expression.binaryOperation, reverse); + predicate && + registerExpression.kind == ExpressionKind::ClassicalRegister && + registerExpression.type == ClassicalType::Uint && + registerExpression.width == registerExpression.reg.bits.size() && + expected.kind == ExpressionKind::Value && + expected.type == ClassicalType::Uint && + expected.width == registerExpression.width) { + if (auto comparison = emitRegisterComparison( + builder, classicalBits, rootClbitMap, registerExpression.reg, + *predicate, expected.uintValue)) { + return comparison; + } + } auto left = emitExpression(builder, *expression.left, classicalBits, rootClbitMap); if (expression.binaryOperation == BinaryOperation::LogicAnd || @@ -956,36 +1047,17 @@ emitCondition(mlir::qc::QCProgramBuilder& builder, const ClassicalTarget& target, const llvm::ArrayRef classicalBits, const llvm::ArrayRef rootClbitMap) { - switch (target.kind) { - case ClassicalTargetKind::ClassicalBit: { - auto actual = - loadClassicalBit(builder, classicalBits, rootClbitMap, target.bit); - return mlir::arith::CmpIOp::create(builder, mlir::arith::CmpIPredicate::eq, - actual, - builder.boolConstant(target.expectedBit)) - .getResult(); - } - case ClassicalTargetKind::ClassicalRegister: { - auto actual = castInteger( - builder, packRegister(builder, classicalBits, rootClbitMap, target.reg), - builder.getIntegerType(target.width)); - auto expected = - integerConstant(builder, target.width, target.expectedRegister); - return mlir::arith::CmpIOp::create(builder, mlir::arith::CmpIPredicate::eq, - actual, expected) - .getResult(); - } - case ClassicalTargetKind::Expression: { - auto condition = emitExpression(builder, *target.expression, classicalBits, - rootClbitMap); - if (!condition.getType().isInteger(1)) { - throw std::runtime_error( - "Qiskit control-flow condition expression must have Boolean type"); - } - return condition; + if (target.kind != ClassicalTargetKind::Expression || !target.expression) { + throw std::runtime_error( + "Qiskit control-flow condition has no classical expression"); } + auto condition = + emitExpression(builder, *target.expression, classicalBits, rootClbitMap); + if (!condition.getType().isInteger(1)) { + throw std::runtime_error( + "Qiskit control-flow condition expression must have Boolean type"); } - throw std::runtime_error("unknown normalized Qiskit condition type"); + return condition; } [[nodiscard]] static mlir::Value diff --git a/bindings/mlir/qiskit/QiskitTranslation.h b/bindings/mlir/qiskit/QiskitTranslation.h index a66a20e5d3..fa5e442073 100644 --- a/bindings/mlir/qiskit/QiskitTranslation.h +++ b/bindings/mlir/qiskit/QiskitTranslation.h @@ -277,9 +277,7 @@ enum class ClassicalTargetKind : uint8_t { struct ClassicalTarget { ClassicalTargetKind kind = ClassicalTargetKind::ClassicalBit; uint32_t bit = 0; - bool expectedBit = false; Register reg; - uint64_t expectedRegister = 0; uint32_t width = 1; std::unique_ptr expression; }; diff --git a/mlir/lib/Dialect/CBit/IR/CBitOps.cpp b/mlir/lib/Dialect/CBit/IR/CBitOps.cpp index bb9a39f7db..00e510cb2d 100644 --- a/mlir/lib/Dialect/CBit/IR/CBitOps.cpp +++ b/mlir/lib/Dialect/CBit/IR/CBitOps.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include #include // IWYU pragma: keep #include @@ -176,17 +177,16 @@ struct FoldUntouchedZeroComparison final : OpRewritePattern { PatternRewriter& rewriter) const override { auto alloc = compare.getReg().getDefiningOp(); if (!alloc || alloc.getInitialization() != Initialization::Zero || - alloc->getBlock() != compare->getBlock()) { + alloc->getBlock() != compare->getBlock() || + !alloc->isBeforeInBlock(compare)) { return failure(); } - /// ponytail: folds only untouched zero registers; track stores if broader - /// constant folding becomes performance-critical. - for (auto* operation = alloc->getNextNode(); operation != compare; - operation = operation->getNextNode()) { - if (operation == nullptr || operation->getNumRegions() != 0 || - (!isa(operation) && - llvm::is_contained(operation->getOperands(), compare.getReg()))) { - return failure(); + for (auto* user : compare.getReg().getUsers()) { + if (!isa(user)) { + auto* ancestor = compare->getBlock()->findAncestorOpInBlock(*user); + if (ancestor != nullptr && ancestor->isBeforeInBlock(compare)) { + return failure(); + } } } const auto zero = compare.getRhs().isZero(); diff --git a/mlir/unittests/Dialect/CBit/IR/test_cbit_ir.cpp b/mlir/unittests/Dialect/CBit/IR/test_cbit_ir.cpp index d8dcfe6408..88ec68a32d 100644 --- a/mlir/unittests/Dialect/CBit/IR/test_cbit_ir.cpp +++ b/mlir/unittests/Dialect/CBit/IR/test_cbit_ir.cpp @@ -208,15 +208,16 @@ TEST_F(CBitIRTest, ReportsMemoryEffects) { TEST_F(CBitIRTest, ForwardsStraightLineStoresAndZeroInitialization) { auto moduleOp = parse(R"mlir( module { - func.func @main() -> (i1, i1) { + func.func @main() -> (i1, i1, i1) { %c0 = arith.constant 0 : index %c1 = arith.constant 1 : index %true = arith.constant true %reg = cbit.alloc(#cbit.init) : !cbit.reg<2> %zero = cbit.load %reg[%c0] : !cbit.reg<2> + %matches = cbit.cmp eq, %reg, 0 : i2 : !cbit.reg<2> cbit.store %true, %reg[%c1] : !cbit.reg<2> %stored = cbit.load %reg[%c1] : !cbit.reg<2> - return %zero, %stored : i1, i1 + return %zero, %stored, %matches : i1, i1, i1 } } )mlir"); @@ -233,24 +234,29 @@ TEST_F(CBitIRTest, ForwardsStraightLineStoresAndZeroInitialization) { moduleOp->print(canonicalizedStream); APInt zero; APInt stored; + APInt matches; EXPECT_TRUE(matchPattern(returnOp.getOperand(0), m_ConstantInt(&zero))) << canonicalized; EXPECT_TRUE(matchPattern(returnOp.getOperand(1), m_ConstantInt(&stored))) << canonicalized; + EXPECT_TRUE(matchPattern(returnOp.getOperand(2), m_ConstantInt(&matches))) + << canonicalized; EXPECT_TRUE(zero.isZero()); EXPECT_TRUE(stored.isOne()); + EXPECT_TRUE(matches.isOne()); } TEST_F(CBitIRTest, DoesNotForwardAcrossAnAmbiguousStore) { auto moduleOp = parse(R"mlir( module { - func.func @main(%dynamic: index) -> i1 { + func.func @main(%dynamic: index) -> (i1, i1) { %c0 = arith.constant 0 : index %true = arith.constant true %reg = cbit.alloc(#cbit.init) : !cbit.reg<2> cbit.store %true, %reg[%dynamic] : !cbit.reg<2> %value = cbit.load %reg[%c0] : !cbit.reg<2> - return %value : i1 + %matches = cbit.cmp eq, %reg, 0 : i2 : !cbit.reg<2> + return %value, %matches : i1, i1 } } )mlir"); @@ -263,5 +269,6 @@ TEST_F(CBitIRTest, DoesNotForwardAcrossAnAmbiguousStore) { auto funcOp = *moduleOp->getOps().begin(); auto returnOp = *funcOp.getOps().begin(); EXPECT_TRUE(returnOp.getOperand(0).getDefiningOp()); + EXPECT_TRUE(returnOp.getOperand(1).getDefiningOp()); } } // namespace diff --git a/test/python/test_mlir_qiskit_translation.py b/test/python/test_mlir_qiskit_translation.py index a5af649681..4eea8934c2 100644 --- a/test/python/test_mlir_qiskit_translation.py +++ b/test/python/test_mlir_qiskit_translation.py @@ -598,10 +598,75 @@ def test_openqasm_register_ordering_exports_to_qiskit_expression() -> None: assert isinstance(condition, expr.Expr) assert expr.structurally_equivalent(condition, expr.greater_equal(restored.cregs[0], 1)) - reimported = QCProgram.from_qiskit(restored).to_qiskit() - reimported_condition = reimported.data[2].operation.condition + reimported = QCProgram.from_qiskit(restored) + assert "cbit.cmp uge" in reimported.ir + reimported_circuit = reimported.to_qiskit() + reimported_condition = reimported_circuit.data[2].operation.condition assert isinstance(reimported_condition, expr.Expr) - assert expr.structurally_equivalent(reimported_condition, expr.greater_equal(reimported.cregs[0], 1)) + assert expr.structurally_equivalent(reimported_condition, expr.greater_equal(reimported_circuit.cregs[0], 1)) + + +@pytest.mark.parametrize( + ("comparison", "predicate"), + [ + (None, "eq"), + ("equal", "eq"), + ("not_equal", "ne"), + ("less", "ult"), + ("less_equal", "ule"), + ("greater", "ugt"), + ("greater_equal", "uge"), + ], +) +def test_qiskit_register_conditions_import_canonically(comparison: str | None, predicate: str) -> None: + """Import tuple and typed register conditions as first-class comparisons.""" + circuit = QuantumCircuit(1, 3) + condition = (circuit.cregs[0], 1) if comparison is None else getattr(expr, comparison)(circuit.cregs[0], 1) + with circuit.if_test(condition): + circuit.x(0) + + ir = QCProgram.from_qiskit(circuit).ir + + assert f"cbit.cmp {predicate}" in ir + assert "cbit.load" not in ir + + +@pytest.mark.parametrize( + ("comparison", "predicate"), + [ + ("equal", "eq"), + ("not_equal", "ne"), + ("less", "ugt"), + ("less_equal", "uge"), + ("greater", "ult"), + ("greater_equal", "ule"), + ], +) +def test_qiskit_reversed_register_conditions_import_canonically(comparison: str, predicate: str) -> None: + """Canonicalize Qiskit comparisons with the constant on the left.""" + circuit = QuantumCircuit(1, 3) + with circuit.if_test(getattr(expr, comparison)(1, circuit.cregs[0])): + circuit.x(0) + + ir = QCProgram.from_qiskit(circuit).ir + + assert f"cbit.cmp {predicate}" in ir + assert "cbit.load" not in ir + + +def test_qiskit_oversized_tuple_condition_is_false() -> None: + """Fold an impossible Qiskit register equality instead of widening it.""" + circuit = QuantumCircuit(1, 2) + with circuit.if_test((circuit.cregs[0], 4)): + circuit.x(0) + + program = QCProgram.from_qiskit(circuit) + + assert "arith.constant false" in program.ir + assert "cbit.cmp" not in program.ir + condition = program.to_qiskit().data[0].operation.condition + assert isinstance(condition, expr.Value) + assert condition.value == 0 def test_openqasm_short_circuit_expression_exports_to_qiskit() -> None: @@ -1932,12 +1997,12 @@ def test_uint_register_cast_to_bool_tests_all_bits() -> None: program = QCProgram.from_qiskit(circuit) ir = program.ir - assert "arith.cmpi ne" in ir + assert "cbit.cmp ne" in ir assert "arith.trunci" not in ir restored = program.to_qiskit() round_trip_ir = QCProgram.from_qiskit(restored).ir - assert "arith.cmpi ne" in round_trip_ir + assert "cbit.cmp ne" in round_trip_ir assert "arith.trunci" not in round_trip_ir @@ -2129,7 +2194,7 @@ def test_nested_condition_only_expression_uses_parent_capture_map() -> None: assert ir.count("scf.if") == 2 -def test_nested_legacy_clbit_condition_uses_root_index() -> None: +def test_nested_tuple_clbit_condition_uses_root_index() -> None: """Resolve a nested tuple condition through its enclosing Clbit map.""" circuit = QuantumCircuit(2, 2) with circuit.for_loop(range(2), None, None, None, None, label=None) as iteration: From c8aa3cb05c3d991e5dd526d51321646300f0abab Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Wed, 2 Sep 2026 19:21:59 +0000 Subject: [PATCH 07/13] =?UTF-8?q?=F0=9F=92=9A=20Fix=20Qiskit=20register=20?= =?UTF-8?q?comparison=20lint?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use std::cmp_not_equal for the signed register index and unsigned iteration index so clang-tidy accepts the canonical-register check. Assisted-by: Codex Signed-off-by: Lukas Burgholzer --- bindings/mlir/qiskit/QiskitImport.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bindings/mlir/qiskit/QiskitImport.cpp b/bindings/mlir/qiskit/QiskitImport.cpp index 5f21e0d15a..40d0a8d61b 100644 --- a/bindings/mlir/qiskit/QiskitImport.cpp +++ b/bindings/mlir/qiskit/QiskitImport.cpp @@ -650,7 +650,7 @@ loadClassicalBit(mlir::qc::QCProgramBuilder& builder, for (size_t index = 0U; index < reg.bits.size(); ++index) { const auto& bit = classicalBitRef(classicalBits, rootClbitMap, reg.bits[index]); - if (bit.index != static_cast(index) || + if (std::cmp_not_equal(bit.index, index) || (storage && bit.storage != storage)) { return {}; } From 0821559c31d2bbc3fe4daf4f51eb5da4a63223f8 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Thu, 3 Sep 2026 10:40:06 +0200 Subject: [PATCH 08/13] =?UTF-8?q?=F0=9F=90=9B=20Reject=20comparisons=20in?= =?UTF-8?q?=20modifiers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Treat cbit.cmp as register access in QC and QCO modifier verification and the QC-to-QCO preflight. Extend the exhaustive modifier tests. Assisted-by: GPT-5.6 via Codex --- mlir/lib/Conversion/QCToQCO/QCToQCO.cpp | 6 +++--- .../lib/Dialect/QC/IR/Modifiers/ModifierUtils.cpp | 6 +++--- .../Dialect/QCO/IR/Modifiers/ModifierUtils.cpp | 5 +++-- .../Conversion/QCToQCO/test_qc_to_qco.cpp | 15 +++++++++++---- mlir/unittests/Dialect/QC/IR/test_qc_ir.cpp | 9 +++++++++ mlir/unittests/Dialect/QCO/IR/test_qco_ir.cpp | 11 ++++++++++- 6 files changed, 39 insertions(+), 13 deletions(-) diff --git a/mlir/lib/Conversion/QCToQCO/QCToQCO.cpp b/mlir/lib/Conversion/QCToQCO/QCToQCO.cpp index f00d37b32c..0bd7f02c9c 100644 --- a/mlir/lib/Conversion/QCToQCO/QCToQCO.cpp +++ b/mlir/lib/Conversion/QCToQCO/QCToQCO.cpp @@ -649,9 +649,9 @@ collectRegisterAccesses(Operation* root, LoweringState& state) { } } - if (!isa(operation)) { + if (!isa(operation)) { return WalkResult::advance(); } diff --git a/mlir/lib/Dialect/QC/IR/Modifiers/ModifierUtils.cpp b/mlir/lib/Dialect/QC/IR/Modifiers/ModifierUtils.cpp index fd8d89c8e7..7d96a687cb 100644 --- a/mlir/lib/Dialect/QC/IR/Modifiers/ModifierUtils.cpp +++ b/mlir/lib/Dialect/QC/IR/Modifiers/ModifierUtils.cpp @@ -34,9 +34,9 @@ namespace mlir::qc::detail { LogicalResult verifyModifierBody(Operation* modifierOp, Block& body) { const auto hasNonUnitaryOperation = body.walk([](Operation* operation) { - return isa(operation) + return isa(operation) ? WalkResult::interrupt() : WalkResult::advance(); }) diff --git a/mlir/lib/Dialect/QCO/IR/Modifiers/ModifierUtils.cpp b/mlir/lib/Dialect/QCO/IR/Modifiers/ModifierUtils.cpp index bcae0c576e..9e0d5db824 100644 --- a/mlir/lib/Dialect/QCO/IR/Modifiers/ModifierUtils.cpp +++ b/mlir/lib/Dialect/QCO/IR/Modifiers/ModifierUtils.cpp @@ -33,8 +33,9 @@ namespace mlir::qco::detail { LogicalResult verifyModifierBody(Operation* modifierOp, Block& body) { const auto hasNonUnitaryOperation = body.walk([](Operation* operation) { - return isa(operation) + return isa(operation) ? WalkResult::interrupt() : WalkResult::advance(); }) diff --git a/mlir/unittests/Conversion/QCToQCO/test_qc_to_qco.cpp b/mlir/unittests/Conversion/QCToQCO/test_qc_to_qco.cpp index 05494a9418..31f4783e52 100644 --- a/mlir/unittests/Conversion/QCToQCO/test_qc_to_qco.cpp +++ b/mlir/unittests/Conversion/QCToQCO/test_qc_to_qco.cpp @@ -1200,13 +1200,15 @@ TEST_F(QCToQCORegressionTest, } namespace { -enum class CBitModifierBodyOp : std::uint8_t { Alloc, Load, Store }; +enum class CBitModifierBodyOp : std::uint8_t { Alloc, Compare, Load, Store }; } // namespace static StringRef cbitOperationName(CBitModifierBodyOp operation) { switch (operation) { case CBitModifierBodyOp::Alloc: return "cbit.alloc"; + case CBitModifierBodyOp::Compare: + return "cbit.cmp"; case CBitModifierBodyOp::Load: return "cbit.load"; case CBitModifierBodyOp::Store: @@ -1233,6 +1235,11 @@ buildInvalidCBitModifierProgram(MLIRContext* context, cbit::RegisterType::get(builder.getContext(), 1), cbit::Initialization::Zero); break; + case CBitModifierBodyOp::Compare: + cbit::CompareOp::create(builder, builder.getI1Type(), + cbit::ComparisonPredicate::Equal, reg, + builder.getIntegerAttr(builder.getI1Type(), 0)); + break; case CBitModifierBodyOp::Load: cbit::LoadOp::create(builder, builder.getI1Type(), reg, index.getResult()); @@ -1262,9 +1269,9 @@ TEST_F(QCToQCORegressionTest, PreflightRejectsEveryCBitOperationInEveryModifier) { constexpr std::array modifiers{ModifierKind::Inv, ModifierKind::Ctrl, ModifierKind::Pow}; - constexpr std::array operations{CBitModifierBodyOp::Alloc, - CBitModifierBodyOp::Load, - CBitModifierBodyOp::Store}; + constexpr std::array operations{ + CBitModifierBodyOp::Alloc, CBitModifierBodyOp::Compare, + CBitModifierBodyOp::Load, CBitModifierBodyOp::Store}; for (const auto modifier : modifiers) { for (const auto operation : operations) { diff --git a/mlir/unittests/Dialect/QC/IR/test_qc_ir.cpp b/mlir/unittests/Dialect/QC/IR/test_qc_ir.cpp index fa17cf9476..f3561243bd 100644 --- a/mlir/unittests/Dialect/QC/IR/test_qc_ir.cpp +++ b/mlir/unittests/Dialect/QC/IR/test_qc_ir.cpp @@ -622,6 +622,7 @@ enum class ForbiddenModifierBodyOp : std::uint8_t { QubitRegisterLoad, QubitRegisterStore, CBitAlloc, + CBitCompare, CBitLoad, CBitStore }; @@ -656,6 +657,8 @@ static StringRef forbiddenOperationName(ForbiddenModifierBodyOp kind) { return "qubit-register-store"; case ForbiddenModifierBodyOp::CBitAlloc: return "cbit.alloc"; + case ForbiddenModifierBodyOp::CBitCompare: + return "cbit.cmp"; case ForbiddenModifierBodyOp::CBitLoad: return "cbit.load"; case ForbiddenModifierBodyOp::CBitStore: @@ -693,6 +696,11 @@ static void emitForbiddenModifierBodyOperation(QCProgramBuilder& builder, cbit::RegisterType::get(builder.getContext(), 1), cbit::Initialization::Zero); return; + case ForbiddenModifierBodyOp::CBitCompare: + cbit::CompareOp::create(builder, builder.getI1Type(), + cbit::ComparisonPredicate::Equal, cbitReg, + builder.getIntegerAttr(builder.getI1Type(), 0)); + return; case ForbiddenModifierBodyOp::CBitLoad: cbit::LoadOp::create(builder, builder.getI1Type(), cbitReg, index); return; @@ -749,6 +757,7 @@ TEST_F(QCTest, ModifiersRecursivelyRejectEveryForbiddenOperation) { ForbiddenModifierBodyOp::QubitRegisterLoad, ForbiddenModifierBodyOp::QubitRegisterStore, ForbiddenModifierBodyOp::CBitAlloc, + ForbiddenModifierBodyOp::CBitCompare, ForbiddenModifierBodyOp::CBitLoad, ForbiddenModifierBodyOp::CBitStore}; diff --git a/mlir/unittests/Dialect/QCO/IR/test_qco_ir.cpp b/mlir/unittests/Dialect/QCO/IR/test_qco_ir.cpp index f174874843..37c2cde61c 100644 --- a/mlir/unittests/Dialect/QCO/IR/test_qco_ir.cpp +++ b/mlir/unittests/Dialect/QCO/IR/test_qco_ir.cpp @@ -394,6 +394,7 @@ enum class VerifierModifierKind : uint8_t { Inv, Ctrl, Pow }; enum class ForbiddenModifierBodyOp : uint8_t { Measure, CBitAlloc, + CBitCompare, CBitLoad, CBitStore }; @@ -418,6 +419,8 @@ static StringRef forbiddenOperationName(ForbiddenModifierBodyOp kind) { return "measure"; case ForbiddenModifierBodyOp::CBitAlloc: return "cbit.alloc"; + case ForbiddenModifierBodyOp::CBitCompare: + return "cbit.cmp"; case ForbiddenModifierBodyOp::CBitLoad: return "cbit.load"; case ForbiddenModifierBodyOp::CBitStore: @@ -480,6 +483,11 @@ buildInvalidNestedModifierBody(QCOProgramBuilder& builder, builder, cbit::RegisterType::get(builder.getContext(), 1), cbit::Initialization::Zero); break; + case ForbiddenModifierBodyOp::CBitCompare: + cbit::CompareOp::create( + builder, builder.getI1Type(), cbit::ComparisonPredicate::Equal, + cbitReg, builder.getIntegerAttr(builder.getI1Type(), 0)); + break; case ForbiddenModifierBodyOp::CBitLoad: cbit::LoadOp::create(builder, builder.getI1Type(), cbitReg, index.getResult()); @@ -512,7 +520,8 @@ TEST_F(QCOTest, ModifiersRecursivelyRejectNonUnitaryOperations) { VerifierModifierKind::Pow}; constexpr std::array forbiddenOperations{ ForbiddenModifierBodyOp::Measure, ForbiddenModifierBodyOp::CBitAlloc, - ForbiddenModifierBodyOp::CBitLoad, ForbiddenModifierBodyOp::CBitStore}; + ForbiddenModifierBodyOp::CBitCompare, ForbiddenModifierBodyOp::CBitLoad, + ForbiddenModifierBodyOp::CBitStore}; for (const auto modifier : modifiers) { for (const auto forbiddenOperation : forbiddenOperations) { From 9214ba1faca437aefa92982031fb182e912c2f91 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Thu, 3 Sep 2026 10:49:51 +0200 Subject: [PATCH 09/13] =?UTF-8?q?=F0=9F=90=9B=20Reject=20stale=20register?= =?UTF-8?q?=20comparisons?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reject OpenQASM export when an inlined cbit.cmp crosses a write to the same register. Follow transitive inline-expression users and cover the stale snapshot regression. Assisted-by: GPT-5.6 via Codex --- .../QC/Translation/TranslateQCToOpenQASM3.cpp | 76 ++++++++++++++++++- .../Translation/test_openqasm3_emission.cpp | 28 +++++++ 2 files changed, 103 insertions(+), 1 deletion(-) diff --git a/mlir/lib/Dialect/QC/Translation/TranslateQCToOpenQASM3.cpp b/mlir/lib/Dialect/QC/Translation/TranslateQCToOpenQASM3.cpp index e9a891b3c6..507fdecc23 100644 --- a/mlir/lib/Dialect/QC/Translation/TranslateQCToOpenQASM3.cpp +++ b/mlir/lib/Dialect/QC/Translation/TranslateQCToOpenQASM3.cpp @@ -21,6 +21,7 @@ #include #include +#include #include #include #include @@ -224,6 +225,79 @@ class OpenQASMEmitter { return uniqueName("q", nextQubit); } + [[nodiscard]] static bool storesToRegister(Operation& operation, Value reg) { + return operation + .walk([&](cbit::StoreOp store) { + return store.getReg() == reg ? WalkResult::interrupt() + : WalkResult::advance(); + }) + .wasInterrupted(); + } + + [[nodiscard]] static bool + hasInterveningRegisterWrite(cbit::CompareOp comparison, Operation& consumer) { + Operation* anchor = &consumer; + Block* block = consumer.getBlock(); + Block* comparisonBlock = comparison->getBlock(); + while (block != comparisonBlock) { + for (Operation& operation : *block) { + if (&operation == anchor) { + break; + } + if (storesToRegister(operation, comparison.getReg())) { + return true; + } + } + Operation* parent = block->getParentOp(); + if (parent == nullptr) { + return true; + } + if (isa(parent) && + storesToRegister(*parent, comparison.getReg())) { + return true; + } + anchor = parent; + block = parent->getBlock(); + } + + for (Operation* operation = comparison->getNextNode(); operation != anchor; + operation = operation->getNextNode()) { + if (operation == nullptr || + storesToRegister(*operation, comparison.getReg())) { + return true; + } + } + return false; + } + + [[nodiscard]] LogicalResult validateRegisterComparisonSnapshots() { + const auto walkResult = function.walk([&](cbit::CompareOp comparison) { + SmallVector consumers; + llvm::append_range(consumers, comparison.getResult().getUsers()); + DenseSet visited; + while (!consumers.empty()) { + Operation* consumer = consumers.pop_back_val(); + if (!visited.insert(consumer).second) { + continue; + } + if (hasInterveningRegisterWrite(comparison, *consumer)) { + std::ignore = + fail(comparison, + "register comparison crosses an intervening register write"); + return WalkResult::interrupt(); + } + if (!isInlineExpressionOperation(*consumer)) { + continue; + } + for (Value result : consumer->getResults()) { + llvm::append_range(consumers, result.getUsers()); + } + } + return WalkResult::advance(); + }); + return walkResult.wasInterrupted() ? failure() : success(); + } + [[nodiscard]] LogicalResult preflight() { SmallVector functions(moduleOp.getOps()); if (functions.size() != 1) { @@ -261,7 +335,7 @@ class OpenQASMEmitter { "scope"); } } - return success(); + return validateRegisterComparisonSnapshots(); } [[nodiscard]] LogicalResult collectProgramShape() { diff --git a/mlir/unittests/Dialect/QC/Translation/test_openqasm3_emission.cpp b/mlir/unittests/Dialect/QC/Translation/test_openqasm3_emission.cpp index 934e2a2e6d..be6ee5e029 100644 --- a/mlir/unittests/Dialect/QC/Translation/test_openqasm3_emission.cpp +++ b/mlir/unittests/Dialect/QC/Translation/test_openqasm3_emission.cpp @@ -320,6 +320,34 @@ module { << *emitted; } +TEST(OpenQASM3EmissionTest, RejectsComparisonAfterInterveningRegisterWrite) { + constexpr llvm::StringLiteral source = R"mlir( +module { + func.func @main() -> !cbit.reg<3> attributes {mqt.entry_point} { + %zero = arith.constant 0 : index + %true = arith.constant true + %q = qc.alloc : !qc.qubit + %c = cbit.alloc(#cbit.init) {mqt.register_name = "c"} + : !cbit.reg<3> + %condition = cbit.cmp eq, %c, 0 : i3 : !cbit.reg<3> + %false = arith.constant false + %forwarded = arith.xori %condition, %false : i1 + cbit.store %true, %c[%zero] : !cbit.reg<3> + scf.if %forwarded { + qc.x %q : !qc.qubit + } + return %c : !cbit.reg<3> + } +} +)mlir"; + DialectRegistry registry = emissionDialects(); + MLIRContext context(registry); + auto moduleOp = parseSourceString(source, &context); + ASSERT_TRUE(moduleOp); + + EXPECT_TRUE(failed(qc::translateQCToOpenQASM3(*moduleOp))); +} + TEST(OpenQASM3EmissionTest, EmitsNativeIndexSwitch) { constexpr llvm::StringLiteral source = R"mlir( module { From 1e6fadc81804d3cb0fdd5bc8db571e68e6edbdcc Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Thu, 3 Sep 2026 10:50:07 +0200 Subject: [PATCH 10/13] =?UTF-8?q?=F0=9F=90=9B=20Export=20comparisons=20in?= =?UTF-8?q?=20while=20loops?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Allow cbit.cmp reads in supported scf.while condition regions. Cover an OpenQASM register-comparison round trip. Assisted-by: GPT-5.6 via Codex --- .../QC/Translation/TranslateQCToOpenQASM3.cpp | 4 ++-- .../Translation/test_openqasm3_emission.cpp | 23 +++++++++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/mlir/lib/Dialect/QC/Translation/TranslateQCToOpenQASM3.cpp b/mlir/lib/Dialect/QC/Translation/TranslateQCToOpenQASM3.cpp index 507fdecc23..bd229f1e68 100644 --- a/mlir/lib/Dialect/QC/Translation/TranslateQCToOpenQASM3.cpp +++ b/mlir/lib/Dialect/QC/Translation/TranslateQCToOpenQASM3.cpp @@ -1042,8 +1042,8 @@ class OpenQASMEmitter { return fail(whileOp, "scf.while loop-carried values are not supported"); } for (Operation& operation : before.without_terminator()) { - if (auto load = dyn_cast(operation)) { - if (failed(emitExpression(load.getResult()))) { + if (isa(operation)) { + if (failed(emitExpression(operation.getResult(0)))) { return failure(); } continue; diff --git a/mlir/unittests/Dialect/QC/Translation/test_openqasm3_emission.cpp b/mlir/unittests/Dialect/QC/Translation/test_openqasm3_emission.cpp index be6ee5e029..46eac456bd 100644 --- a/mlir/unittests/Dialect/QC/Translation/test_openqasm3_emission.cpp +++ b/mlir/unittests/Dialect/QC/Translation/test_openqasm3_emission.cpp @@ -268,6 +268,29 @@ switch (selector) { << *emitted; } +TEST(OpenQASM3EmissionTest, RoundTripsRegisterComparisonInWhileCondition) { + constexpr llvm::StringLiteral source = R"qasm(OPENQASM 3.1; +include "stdgates.inc"; +qubit q; +bit[1] c = measure q; +while (c == 1) { + c[0] = measure q; +} +)qasm"; + MLIRContext context; + auto moduleOp = qc::translateQASM3ToQC(source, &context); + ASSERT_TRUE(moduleOp); + + auto emitted = qc::translateQCToOpenQASM3(*moduleOp); + + ASSERT_TRUE(succeeded(emitted)); + EXPECT_NE(emitted->find("while ("), std::string::npos) << *emitted; + EXPECT_NE(emitted->find("c == 1"), std::string::npos) << *emitted; + EXPECT_TRUE(oq3::frontend::analyzeOpenQASM( + *emitted, {.gatePolicy = oq3::frontend::GatePolicy::Strict})) + << *emitted; +} + TEST(OpenQASM3EmissionTest, EmitsRegisterComparisonsDirectly) { constexpr llvm::StringLiteral source = R"mlir( module { From ac6c5c8ec0ac963f1a291bd3b21d1208bdc69b24 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Thu, 3 Sep 2026 10:55:02 +0200 Subject: [PATCH 11/13] =?UTF-8?q?=F0=9F=90=9B=20Reject=20oversized=20CBit?= =?UTF-8?q?=20comparisons?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Compare register and APInt widths without narrowing the register width. Cover widths above the APInt range. Assisted-by: GPT-5.6 via Codex --- mlir/lib/Dialect/CBit/IR/CBitOps.cpp | 5 +++-- mlir/unittests/Dialect/CBit/IR/test_cbit_ir.cpp | 12 ++++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/mlir/lib/Dialect/CBit/IR/CBitOps.cpp b/mlir/lib/Dialect/CBit/IR/CBitOps.cpp index 00e510cb2d..f25a82b7c7 100644 --- a/mlir/lib/Dialect/CBit/IR/CBitOps.cpp +++ b/mlir/lib/Dialect/CBit/IR/CBitOps.cpp @@ -27,6 +27,7 @@ #include #include +#include #include using namespace mlir; @@ -217,8 +218,8 @@ LogicalResult LoadOp::verify() { } LogicalResult CompareOp::verify() { - const auto width = static_cast(getReg().getType().getWidth()); - if (getRhs().getBitWidth() != width) { + if (std::cmp_not_equal(getRhs().getBitWidth(), + getReg().getType().getWidth())) { return emitOpError("expected integer width must match register width"); } return success(); diff --git a/mlir/unittests/Dialect/CBit/IR/test_cbit_ir.cpp b/mlir/unittests/Dialect/CBit/IR/test_cbit_ir.cpp index 88ec68a32d..6baa759b85 100644 --- a/mlir/unittests/Dialect/CBit/IR/test_cbit_ir.cpp +++ b/mlir/unittests/Dialect/CBit/IR/test_cbit_ir.cpp @@ -100,6 +100,18 @@ TEST_F(CBitIRTest, RejectsComparisonWidthMismatch) { )mlir")); } +TEST_F(CBitIRTest, RejectsUnsupportedComparisonWidth) { + EXPECT_FALSE(parse(R"mlir( + module { + func.func @main() { + %reg = cbit.alloc(#cbit.init) : !cbit.reg<4294967297> + %matches = cbit.cmp eq, %reg, 0 : i1 : !cbit.reg<4294967297> + return + } + } + )mlir")); +} + TEST_F(CBitIRTest, RejectsNonPositiveRegisterWidth) { EXPECT_FALSE(parse(R"mlir( module { From f7ae4840592c7606dbeaff32cb7c27e148bc70dc Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Thu, 3 Sep 2026 10:56:51 +0200 Subject: [PATCH 12/13] =?UTF-8?q?=F0=9F=90=9B=20Reject=20mixed=20QIR=20reg?= =?UTF-8?q?ister=20storage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Track CBit register storage through control-flow forwarding before adaptive QIR lowering. Reject returned/local merges and preserve reads from same-representation merges. Assisted-by: GPT-5.6 via Codex --- .../Conversion/QCToQIR/QIRCommon/QIRCommon.h | 3 + .../QCToQIR/QIRAdaptive/QCToQIRAdaptive.cpp | 120 +++++++++++++++++- .../test_qc_to_qir_adaptive.cpp | 79 ++++++++++++ 3 files changed, 198 insertions(+), 4 deletions(-) diff --git a/mlir/include/mlir/Conversion/QCToQIR/QIRCommon/QIRCommon.h b/mlir/include/mlir/Conversion/QCToQIR/QIRCommon/QIRCommon.h index 34fb68d5f1..63dcda4035 100644 --- a/mlir/include/mlir/Conversion/QCToQIR/QIRCommon/QIRCommon.h +++ b/mlir/include/mlir/Conversion/QCToQIR/QIRCommon/QIRCommon.h @@ -40,6 +40,9 @@ struct LoweringState { /// Result-array pointers to be deallocated at the end of the program DenseSet resultArrays; + /// CBit read operations whose register is backed by a result array. + DenseSet returnedCBitReads; + /// Cache static qubit pointers for reuse DenseMap staticQubits; diff --git a/mlir/lib/Conversion/QCToQIR/QIRAdaptive/QCToQIRAdaptive.cpp b/mlir/lib/Conversion/QCToQIR/QIRAdaptive/QCToQIRAdaptive.cpp index 7cd7eaea2f..f234c04b64 100644 --- a/mlir/lib/Conversion/QCToQIR/QIRAdaptive/QCToQIRAdaptive.cpp +++ b/mlir/lib/Conversion/QCToQIR/QIRAdaptive/QCToQIRAdaptive.cpp @@ -40,6 +40,7 @@ #include #include #include +#include #include #include #include @@ -56,6 +57,109 @@ using namespace qir; #define GEN_PASS_DEF_QCTOQIRADAPTIVE #include "mlir/Conversion/QCToQIR/QIRAdaptive/QCToQIRAdaptive.h.inc" +namespace { + +constexpr unsigned LOCAL_CBIT_REGISTER = 1U; +constexpr unsigned RETURNED_CBIT_REGISTER = 2U; +constexpr unsigned MIXED_CBIT_REGISTER = + LOCAL_CBIT_REGISTER | RETURNED_CBIT_REGISTER; + +} // namespace + +static LogicalResult prepareCBitRegisterReads(Operation* moduleOp, + LoweringState& state) { + DenseMap> forwardedRegisters; + moduleOp->walk([&](Operation* operation) { + for (auto& region : operation->getRegions()) { + for (auto& block : region) { + for (auto argument : block.getArguments()) { + if (!isa(argument.getType())) { + continue; + } + for (auto* predecessor : block.getPredecessors()) { + auto branch = + dyn_cast(predecessor->getTerminator()); + if (!branch) { + continue; + } + for (unsigned successorIndex = 0; + successorIndex < branch->getNumSuccessors(); + ++successorIndex) { + if (branch->getSuccessor(successorIndex) != &block) { + continue; + } + auto operands = branch.getSuccessorOperands(successorIndex); + if (argument.getArgNumber() >= operands.size() || + operands.isOperandProduced(argument.getArgNumber())) { + continue; + } + if (auto incoming = operands[argument.getArgNumber()]) { + forwardedRegisters[incoming].push_back(argument); + } + } + } + } + } + } + }); + moduleOp->walk([&](arith::SelectOp selectOp) { + if (isa(selectOp.getType())) { + forwardedRegisters[selectOp.getTrueValue()].push_back( + selectOp.getResult()); + forwardedRegisters[selectOp.getFalseValue()].push_back( + selectOp.getResult()); + } + }); + + DenseMap representations; + SmallVector worklist; + moduleOp->walk([&](cbit::AllocOp allocOp) { + const auto it = state.cregIndices.find(allocOp.getOperation()); + if (it == state.cregIndices.end()) { + return; + } + representations[allocOp.getResult()] = state.cregs[it->second].record + ? RETURNED_CBIT_REGISTER + : LOCAL_CBIT_REGISTER; + worklist.push_back(allocOp.getResult()); + }); + + while (!worklist.empty()) { + auto source = worklist.pop_back_val(); + const auto it = forwardedRegisters.find(source); + if (it == forwardedRegisters.end()) { + continue; + } + for (auto destination : it->second) { + auto& representation = representations[destination]; + const auto merged = representation | representations.lookup(source); + if (merged != representation) { + representation = merged; + worklist.push_back(destination); + } + } + } + + bool hasMixedRepresentation = false; + const auto prepareRead = [&](Operation* operation, Value reg) { + const auto representation = representations.lookup(reg); + if (representation == MIXED_CBIT_REGISTER) { + operation->emitOpError( + "adaptive QIR conversion cannot merge returned and local CBit " + "registers"); + hasMixedRepresentation = true; + } else if (representation == RETURNED_CBIT_REGISTER) { + state.returnedCBitReads.insert(operation); + } + }; + moduleOp->walk( + [&](cbit::LoadOp loadOp) { prepareRead(loadOp, loadOp.getReg()); }); + moduleOp->walk([&](cbit::CompareOp compareOp) { + prepareRead(compareOp, compareOp.getReg()); + }); + return success(!hasMixedRepresentation); +} + /** * @brief Returns the result pointer the `qc::MeasureOp` @p op writes to, or * `nullptr` if it does not write into a classical register. @@ -191,9 +295,9 @@ struct ConvertCBitAllocOp final : StatefulOpConversionPattern { static Value loadCBit(Operation* op, Value reg, Value index, ConversionPatternRewriter& rewriter, - LoweringState& state) { + bool returnedRegister) { const auto ptrType = LLVM::LLVMPointerType::get(rewriter.getContext()); - if (!state.resultArrays.contains(reg)) { + if (!returnedRegister) { auto elementptr = LLVM::GEPOp::create(rewriter, op->getLoc(), ptrType, rewriter.getI1Type(), reg, ValueRange{index}); @@ -219,8 +323,10 @@ struct ConvertCBitLoadOp final : StatefulOpConversionPattern { LogicalResult matchAndRewrite(cbit::LoadOp op, OpAdaptor adaptor, ConversionPatternRewriter& rewriter) const override { + const auto returnedRegister = + getState().returnedCBitReads.contains(op.getOperation()); rewriter.replaceOp(op, loadCBit(op, adaptor.getReg(), adaptor.getIndex(), - rewriter, getState())); + rewriter, returnedRegister)); return success(); } }; @@ -232,13 +338,15 @@ struct ConvertCBitCompareOp final LogicalResult matchAndRewrite(cbit::CompareOp op, OpAdaptor adaptor, ConversionPatternRewriter& rewriter) const override { + const auto returnedRegister = + getState().returnedCBitReads.contains(op.getOperation()); auto result = cbit::buildComparison( rewriter, op.getLoc(), op.getPredicate(), op.getRhs(), [&](const int64_t index) -> Value { auto indexValue = LLVM::ConstantOp::create( rewriter, op.getLoc(), rewriter.getI64Type(), index); return loadCBit(op, adaptor.getReg(), indexValue, rewriter, - getState()); + returnedRegister); }); rewriter.replaceOp(op, result); return success(); @@ -750,6 +858,10 @@ struct QCToQIRAdaptive final : impl::QCToQIRAdaptiveBase { signalPassFailure(); return; } + if (failed(prepareCBitRegisterReads(moduleOp, state))) { + signalPassFailure(); + return; + } // Stage 2.1: Convert func dialect to LLVM { 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 2559a630de..2947de4831 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 @@ -36,6 +36,7 @@ #include #include #include +#include #include #include #include @@ -282,6 +283,84 @@ TEST(QCToQIRAdaptiveNativeTest, LowersClassicalRegisterComparison) { EXPECT_FALSE(retainsComparison); } +TEST(QCToQIRAdaptiveNativeTest, RejectsMixedClassicalRegisterRepresentations) { + MLIRContext context; + context.loadDialect(); + auto module = parseSourceString(R"mlir( + module { + func.func @main() -> (i1, i1, !cbit.reg<1>) attributes {mqt.entry_point} { + %true = arith.constant true + %c0 = arith.constant 0 : index + %returned = cbit.alloc(#cbit.init) : !cbit.reg<1> + %local = cbit.alloc(#cbit.init) : !cbit.reg<1> + %selected = scf.if %true -> (!cbit.reg<1>) { + scf.yield %returned : !cbit.reg<1> + } else { + scf.yield %local : !cbit.reg<1> + } + %bit = cbit.load %selected[%c0] : !cbit.reg<1> + %matches = cbit.cmp eq, %selected, 0 : i1 : !cbit.reg<1> + return %bit, %matches, %returned : i1, i1, !cbit.reg<1> + } + } + )mlir", + &context); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(verify(*module))); + + size_t mixedRepresentationDiagnostics = 0; + const ScopedDiagnosticHandler handler(&context, [&](Diagnostic& diagnostic) { + std::string message; + llvm::raw_string_ostream stream(message); + diagnostic.print(stream); + mixedRepresentationDiagnostics += StringRef(message).contains( + "adaptive QIR conversion cannot merge returned and local CBit " + "registers"); + return success(); + }); + EXPECT_TRUE(failed(runQCToQIRAdaptiveConversionSimple(*module))); + EXPECT_EQ(mixedRepresentationDiagnostics, 2); +} + +TEST(QCToQIRAdaptiveNativeTest, LowersReturnedRegisterMerge) { + MLIRContext context; + context.loadDialect(); + auto module = parseSourceString(R"mlir( + module { + func.func @main() -> (i1, i1, !cbit.reg<1>, !cbit.reg<1>) + attributes {mqt.entry_point} { + %true = arith.constant true + %c0 = arith.constant 0 : index + %first = cbit.alloc(#cbit.init) : !cbit.reg<1> + %second = cbit.alloc(#cbit.init) : !cbit.reg<1> + %selected = scf.if %true -> (!cbit.reg<1>) { + scf.yield %first : !cbit.reg<1> + } else { + scf.yield %second : !cbit.reg<1> + } + %bit = cbit.load %selected[%c0] : !cbit.reg<1> + %matches = cbit.cmp eq, %selected, 0 : i1 : !cbit.reg<1> + return %bit, %matches, %first, %second + : i1, i1, !cbit.reg<1>, !cbit.reg<1> + } + } + )mlir", + &context); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(verify(*module))); + EXPECT_TRUE(succeeded(runQCToQIRAdaptiveConversionSimple(*module))); + EXPECT_TRUE(succeeded(verify(*module))); + size_t resultReads = 0; + module->walk([&](LLVM::CallOp call) { + resultReads += call.getCallee() == qir::QIR_READ_RESULT; + }); + EXPECT_EQ(resultReads, 2); +} + TEST(QCToQIRAdaptiveNativeTest, RejectsMultipleRegisterDestinations) { MLIRContext context; context From e6f80357432064fb83a73ab111eb738020db77fb Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Thu, 3 Sep 2026 11:20:45 +0200 Subject: [PATCH 13/13] =?UTF-8?q?=F0=9F=8E=A8=20Fix=20C++=20lint=20warning?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use safe signed/unsigned comparisons for register widths and unitary dimensions. Assisted-by: GPT-5.6 via Codex --- mlir/lib/Dialect/QC/Translation/TranslateQCToOpenQASM3.cpp | 4 ++-- mlir/lib/Dialect/QCO/Utils/DDFunctionality.cpp | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/mlir/lib/Dialect/QC/Translation/TranslateQCToOpenQASM3.cpp b/mlir/lib/Dialect/QC/Translation/TranslateQCToOpenQASM3.cpp index bd229f1e68..01808553f0 100644 --- a/mlir/lib/Dialect/QC/Translation/TranslateQCToOpenQASM3.cpp +++ b/mlir/lib/Dialect/QC/Translation/TranslateQCToOpenQASM3.cpp @@ -377,8 +377,8 @@ class OpenQASMEmitter { if (auto alloc = dyn_cast(&operation)) { const auto type = alloc.getResult().getType(); const auto width = type.getWidth(); - if (width <= 0 || static_cast(width) > - MAX_CLASSICAL_BITS - numClassicalBits) { + if (width <= 0 || + std::cmp_greater(width, MAX_CLASSICAL_BITS - numClassicalBits)) { return fail(alloc, "total classical register width exceeds the " "supported limit of " + Twine(MAX_CLASSICAL_BITS) + " bits"); diff --git a/mlir/lib/Dialect/QCO/Utils/DDFunctionality.cpp b/mlir/lib/Dialect/QCO/Utils/DDFunctionality.cpp index 736ff84bb8..4655a76090 100644 --- a/mlir/lib/Dialect/QCO/Utils/DDFunctionality.cpp +++ b/mlir/lib/Dialect/QCO/Utils/DDFunctionality.cpp @@ -360,7 +360,7 @@ static LogicalResult applyUnitaryMatrix(UnitaryOpInterface unitary, } ArrayRef wires = *wiresOr; if (wires.size() >= 63 || - local.rows() != static_cast(size_t{1} << wires.size())) { + std::cmp_not_equal(local.rows(), uint64_t{1} << wires.size())) { return unitary.emitError() << "unitary matrix dimension does not match its target count"; }