Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions .agent/plans/wide-cbit-qiskit-comparisons.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Support wide CBit comparisons in Qiskit

Status: complete.

## Goal and scope

Support unsigned comparisons between one complete classical register and one
same-width literal beyond 64 bits, including both operand orders and all six
comparison predicates. Qiskit expression conditions and tuple equalities use the
same normalized expression path. Out-of-range tuple equalities become false.
Computed, packed, and signed wide expressions remain unsupported.

## Decisions

- Store unsigned literals in `llvm::APInt`; transfer Python integers through
hexadecimal text to preserve values without Python's decimal digit limit.
- Import validation owns the supported-expression boundary. Only the immediate
register and literal leaves of a direct comparison may exceed 64 bits. Generic
emission handles constants, reads, and comparison predicates.
- Emission checks mapped register storage. A wide read requires one complete
storage object in bit order; packing remains limited to 64 bits.
- Preserve operand order during import. MLIR comparison folding normalizes
constant-left predicates where needed; jeff already enables this folding.
- Export retains the direct `cbit.read` and `arith.cmpi` recognition path and
the shared initialization and snapshot checks.

## Validation

The LLVM/MLIR 23.1 binding build passed. All 281 Qiskit translation tests
passed, including 27 focused wide-comparison cases. The 12 predicate/order cases
also verify conversion through QCO to jeff. Stub generation produced no tracked
changes. C++ lint passed with zero findings. Repository lint passed.

Commands from the repository root:

```console
uv run --no-sync pytest -n 0 test/python/test_mlir_qiskit_translation.py
uvx nox -s stubs
uvx nox -s cpp-lint
uvx nox -s lint
```
102 changes: 70 additions & 32 deletions bindings/mlir/qiskit/Qiskit2_5.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@
#include "mlir/Dialect/QC/Translation/StandardGate.h"

#include <llvm/ADT/STLFunctionalExtras.h>
#include <llvm/ADT/StringExtras.h>
#include <llvm/ADT/StringMap.h>
#include <llvm/ADT/StringRef.h>
#include <llvm/ADT/StringSwitch.h>

// Qiskit requires its umbrella header before the extension function table.
Expand All @@ -27,7 +29,6 @@

#include <algorithm>
#include <array>
#include <bit>
#include <cmath>
#include <complex>
#include <cstddef>
Expand Down Expand Up @@ -96,6 +97,16 @@ constexpr size_t MAX_ANNOTATED_OPERATION_DEPTH = 64U;
}
}

[[nodiscard]] static std::string pythonHex(const nb::handle object,
const std::string_view error) {
try {
return nb::cast<std::string>(
nb::module_::import_("builtins").attr("hex")(object));
} catch (const nb::python_error&) {
throw std::runtime_error(std::string(error));
}
}

[[nodiscard]] static std::string
pythonStringAttribute(const nb::handle object, const char* name,
const std::string_view error) {
Expand All @@ -113,6 +124,33 @@ pythonUnsignedAttribute(const nb::handle object, const char* name,
return result;
}

[[nodiscard]] static llvm::APInt
pythonUnsignedValue(const nb::handle object, const uint32_t width,
const std::string_view error) {
if (!nb::isinstance<nb::int_>(object)) {
throw std::runtime_error(std::string(error));
}
const auto text = pythonHex(object, error);
auto value = llvm::StringRef(text);
llvm::APInt result;
if (!value.consume_front("0x") || value.getAsInteger(16, result) ||
result.getActiveBits() > width) {
throw std::runtime_error(std::string(error));
}
return result;
}

[[nodiscard]] static nb::object pythonInteger(const llvm::APInt& value,
const std::string_view error) {
const auto text = llvm::toString(value, 16, false);
try {
return nb::module_::import_("builtins")
.attr("int")(nb::str(text.c_str()), nb::int_(16));
} catch (const nb::python_error&) {
throw std::runtime_error(std::string(error));
}
}

[[noreturn]] static void throwPythonError(const std::string_view message) {
const nb::python_error error;
throw std::runtime_error(std::string(message) + ": " + error.what());
Expand Down Expand Up @@ -1106,9 +1144,9 @@ static void setPythonExpressionType(Expression& result,
if (typeName == "Uint") {
const auto width = pythonUnsignedAttribute(
type, "width", "Qiskit Uint expression has no width");
if (width == 0U || width > 64U) {
if (width == 0U || width > std::numeric_limits<uint32_t>::max()) {
throw std::runtime_error(
"Qiskit unsigned classical values must be between 1 and 64 bits");
"Qiskit unsigned classical value width is out of range");
}
result.type = ClassicalType::Uint;
result.width = static_cast<uint32_t>(width);
Expand Down Expand Up @@ -1186,7 +1224,7 @@ static void normalizePythonVariable(Expression& result,
}
if (nb::isinstance(variable, circuitModule.attr("ClassicalRegister"))) {
if (result.type != ClassicalType::Uint || nb::len(variable) == 0U ||
nb::len(variable) > 64U || result.width < nb::len(variable)) {
result.width < nb::len(variable)) {
throw std::runtime_error(
"Qiskit classical-register variable has an invalid type");
}
Expand Down Expand Up @@ -1241,12 +1279,9 @@ static void normalizePythonVariable(Expression& result,
break;
}
case ClassicalType::Uint:
if (!nb::try_cast(value, result->uintValue) ||
(result->width < 64U &&
result->uintValue >= (uint64_t{1} << result->width))) {
throw std::runtime_error(
"Qiskit Uint literal does not fit its declared width");
}
result->uintValue = pythonUnsignedValue(
value, result->width,
"Qiskit Uint literal does not fit its declared width");
break;
case ClassicalType::Float:
if (!nb::try_cast(value, result->floatValue) ||
Expand Down Expand Up @@ -1455,28 +1490,29 @@ class NativeControlFlowReader final : public ControlFlowReader {
throw std::runtime_error("Qiskit control-flow condition has an invalid "
"shape");
}
uint64_t expected = 0U;
if (!nb::try_cast(condition[1], expected)) {
throw std::runtime_error(
"Qiskit control-flow condition has an invalid value");
}

auto result = normalizePythonTarget(condition[0]);
if (result.kind == ClassicalTargetKind::ClassicalBit) {
if (expected > 1U) {
const auto expected = pythonUnsignedValue(
condition[1], std::numeric_limits<uint32_t>::max(),
"Qiskit control-flow condition has an invalid value");
auto result =
normalizePythonTarget(expressionModule.attr("lift")(condition[0]));
const auto& target = *result.expression;
if (target.kind == ExpressionKind::ClassicalBit) {
if (expected.getActiveBits() > 1U) {
throw std::runtime_error(
"Qiskit classical-bit condition must compare against zero or one");
}
return normalizePythonTarget(expressionModule.attr("equal")(
condition[0], nb::bool_(expected != 0U)));
condition[0], nb::bool_(!expected.isZero())));
}
if (result.kind == ClassicalTargetKind::ClassicalRegister) {
if (std::bit_width(expected) > result.reg.bits.size()) {
if (target.kind == ExpressionKind::ClassicalRegister) {
if (expected.getActiveBits() > target.reg.bits.size()) {
return normalizePythonTarget(
expressionModule.attr("lift")(nb::bool_(false)));
}
return normalizePythonTarget(
expressionModule.attr("equal")(condition[0], nb::int_(expected)));
return normalizePythonTarget(expressionModule.attr("equal")(
condition[0],
pythonInteger(expected,
"Qiskit control-flow condition has an invalid value")));
}
throw std::runtime_error("Qiskit control flow has an unknown condition "
"target");
Expand Down Expand Up @@ -1782,9 +1818,8 @@ class PythonClassicalBuilder final {
}
return typesModule_.attr("Bool")();
case ClassicalType::Uint:
if (width == 0U || width > 64U) {
throw std::runtime_error(
"Qiskit unsigned expressions require a width from 1 to 64");
if (width == 0U) {
throw std::runtime_error("Qiskit unsigned expressions require a width");
}
return typesModule_.attr("Uint")(width);
case ClassicalType::Float:
Expand Down Expand Up @@ -1892,13 +1927,16 @@ class PythonClassicalBuilder final {
switch (value.type) {
case ClassicalType::Bool:
return expressionModule_.attr("lift")(nb::bool_(value.boolValue), type);
case ClassicalType::Uint:
if (value.width < std::numeric_limits<uint64_t>::digits &&
value.uintValue >= (uint64_t{1} << value.width)) {
case ClassicalType::Uint: {
if (value.uintValue.getActiveBits() > value.width) {
throw std::runtime_error(
"Qiskit unsigned expression value exceeds its width");
}
return expressionModule_.attr("lift")(nb::int_(value.uintValue), type);
return expressionModule_.attr("lift")(
pythonInteger(value.uintValue,
"Qiskit failed to convert a Uint literal"),
type);
}
case ClassicalType::Float:
if (!std::isfinite(value.floatValue)) {
throw std::runtime_error(
Expand All @@ -1917,7 +1955,7 @@ class PythonClassicalBuilder final {
return expressionModule_.attr("lift")(classicalBit(value.bit));
case ExpressionKind::ClassicalRegister:
if (value.type != ClassicalType::Uint || value.width == 0U ||
value.width < value.reg.bits.size() || value.width > 64U) {
value.width < value.reg.bits.size()) {
throw std::runtime_error(
"Qiskit classical-register expression has an invalid type");
}
Expand Down
65 changes: 56 additions & 9 deletions bindings/mlir/qiskit/QiskitExport.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
#include "mlir/Dialect/QC/Translation/StandardGate.h"
#include "mlir/Support/IntegerExpressions.h"

#include <llvm/ADT/APInt.h>
#include <llvm/ADT/DenseMap.h>
#include <llvm/ADT/DenseSet.h>
#include <llvm/ADT/STLExtras.h>
Expand Down Expand Up @@ -1017,10 +1018,11 @@ static void setExpressionType(Expression& expression, const mlir::Type type) {
}

[[nodiscard]] static Register
classicalRegisterLayout(mlir::Value value, const ExportState& state) {
classicalRegisterLayout(mlir::Value value, const ExportState& state,
const bool allowWide = false) {
const auto info = state.classicalRegisterInfo.find(value);
if (info == state.classicalRegisterInfo.end() || info->second.size == 0U ||
info->second.size > 64U) {
(!allowWide && info->second.size > 64U)) {
throw std::runtime_error(
"Qiskit classical registers require between 1 and 64 bits");
}
Expand All @@ -1037,7 +1039,8 @@ classicalRegisterLayout(mlir::Value value, const ExportState& state) {
}

[[nodiscard]] static Register classicalRegister(mlir::Value value,
const ExportState& state) {
const ExportState& state,
const bool allowWide = false) {
const auto info = state.classicalRegisterInfo.find(value);
if (info != state.classicalRegisterInfo.end() &&
info->second.initialization != mlir::cbit::Initialization::Zero) {
Expand All @@ -1048,7 +1051,7 @@ classicalRegisterLayout(mlir::Value value, const ExportState& state) {
"Qiskit classical expression reads undefined classical bits");
}
}
return classicalRegisterLayout(value, state);
return classicalRegisterLayout(value, state, allowWide);
}

[[nodiscard]] static BinaryOperation
Expand Down Expand Up @@ -1123,7 +1126,7 @@ exportExpressionImpl(mlir::Value value, ExportState& state,
if (result->type == ClassicalType::Bool) {
result->boolValue = !integer.getValue().isZero();
} else if (result->type == ClassicalType::Uint) {
result->uintValue = integer.getValue().getZExtValue();
result->uintValue = integer.getValue();
} else {
throw std::runtime_error(
"Qiskit Float expressions require a floating-point constant");
Expand Down Expand Up @@ -1236,7 +1239,7 @@ exportExpressionImpl(mlir::Value value, ExportState& state,
if (operand->kind == ExpressionKind::Value &&
operand->type == ClassicalType::Bool && *bitVectorWidth == 1U) {
operand->type = ClassicalType::Uint;
operand->uintValue = operand->boolValue;
operand->uintValue = llvm::APInt(1U, operand->boolValue);
return;
}
countExpressionNode(nodeCount);
Expand Down Expand Up @@ -1266,7 +1269,7 @@ exportExpressionImpl(mlir::Value value, ExportState& state,
auto literal = std::make_unique<Expression>();
literal->type = ClassicalType::Uint;
literal->width = width;
literal->uintValue = bits;
literal->uintValue = llvm::APInt(width, bits);
return literal;
};
const auto uintCast = [&](std::unique_ptr<Expression> operand,
Expand Down Expand Up @@ -1379,7 +1382,7 @@ exportExpressionImpl(mlir::Value value, ExportState& state,
auto zero = std::make_unique<Expression>();
setExpressionType(*zero, cast.getIn().getType());
zero->kind = ExpressionKind::Value;
zero->uintValue = 0U;
zero->uintValue = llvm::APInt::getZero(zero->width);
result->right = std::move(zero);
}
state.expressionOperations.insert(operation);
Expand All @@ -1397,6 +1400,50 @@ exportExpressionImpl(mlir::Value value, ExportState& state,
"Qiskit integer comparisons require integer operands");
}
const auto width = type.getWidth();
if (width > 64U) {
auto read = op.getLhs().getDefiningOp<mlir::cbit::ReadOp>();
auto constant = op.getRhs().getDefiningOp<mlir::arith::ConstantOp>();
bool reverse = false;
if (!read || !constant ||
!llvm::isa<mlir::IntegerAttr>(constant.getValue())) {
read = op.getRhs().getDefiningOp<mlir::cbit::ReadOp>();
constant = op.getLhs().getDefiningOp<mlir::arith::ConstantOp>();
reverse = true;
}
const auto integer =
constant ? llvm::dyn_cast<mlir::IntegerAttr>(constant.getValue())
: mlir::IntegerAttr{};
if (read && integer) {
if (mlir::mqt::unsignedPredicate(op.getPredicate()) !=
op.getPredicate()) {
throw std::runtime_error(
"Qiskit signed register comparisons support at most 64 bits");
}
if (read->getBlock() != &evaluationBlock) {
throw std::runtime_error(
"Qiskit classical expressions cannot capture a computed SSA "
"value across a control-flow region");
}
countExpressionNode(nodeCount);
auto reg = std::make_unique<Expression>();
reg->kind = ExpressionKind::ClassicalRegister;
reg->type = ClassicalType::Uint;
reg->width = width;
reg->reg = classicalRegister(read.getReg(), state, true);
countExpressionNode(nodeCount);
auto expected = std::make_unique<Expression>();
expected->type = ClassicalType::Uint;
expected->width = width;
expected->uintValue = integer.getValue();
result->kind = ExpressionKind::Binary;
result->binaryOperation = comparisonOperation(op.getPredicate());
result->left = reverse ? std::move(expected) : std::move(reg);
result->right = reverse ? std::move(reg) : std::move(expected);
state.expressionOperations.insert(read);
state.expressionOperations.insert(operation);
return result;
}
}
auto comparison = binary(
comparisonOperation(mlir::mqt::unsignedPredicate(op.getPredicate())),
op.getLhs(), op.getRhs(), width);
Expand Down Expand Up @@ -1761,7 +1808,7 @@ exportSwitchTarget(mlir::Value value, ExportState& state,
expression->kind = ExpressionKind::Value;
expression->type = ClassicalType::Uint;
expression->width = 64U;
expression->uintValue = *constant;
expression->uintValue = llvm::APInt(64U, *constant);
return {.kind = ClassicalTargetKind::Expression,
.width = 64U,
.expression = std::move(expression)};
Expand Down
Loading
Loading