diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md
index 08485b3669..c0a521febe 100644
--- a/doc/releases/changelog-dev.md
+++ b/doc/releases/changelog-dev.md
@@ -453,6 +453,10 @@
operations for interacting with an external Pauli frame tracking library.
[(#2188)](https://github.com/PennyLaneAI/catalyst/pull/2188)
+* A new `to-pauli-frame` compilation pass has been added, which applies the Pauli frame protocols to
+ a Clifford+T program.
+ [(#2269)](https://github.com/PennyLaneAI/catalyst/pull/2269)
+
Documentation 📝
* A typo in the code example for :func:`~.passes.ppr_to_ppm` has been corrected.
diff --git a/mlir/include/PauliFrame/CMakeLists.txt b/mlir/include/PauliFrame/CMakeLists.txt
index f33061b2d8..9f57627c32 100644
--- a/mlir/include/PauliFrame/CMakeLists.txt
+++ b/mlir/include/PauliFrame/CMakeLists.txt
@@ -1 +1,2 @@
add_subdirectory(IR)
+add_subdirectory(Transforms)
diff --git a/mlir/include/PauliFrame/Transforms/Passes.h b/mlir/include/PauliFrame/Transforms/Passes.h
new file mode 100644
index 0000000000..05497cb721
--- /dev/null
+++ b/mlir/include/PauliFrame/Transforms/Passes.h
@@ -0,0 +1,27 @@
+// Copyright 2025 Xanadu Quantum Technologies Inc.
+
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+
+// http://www.apache.org/licenses/LICENSE-2.0
+
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#pragma once
+
+#include "mlir/Pass/Pass.h"
+
+namespace catalyst {
+namespace pauli_frame {
+
+#define GEN_PASS_DECL
+#define GEN_PASS_REGISTRATION
+#include "PauliFrame/Transforms/Passes.h.inc"
+
+} // namespace pauli_frame
+} // namespace catalyst
diff --git a/mlir/include/PauliFrame/Transforms/Passes.td b/mlir/include/PauliFrame/Transforms/Passes.td
new file mode 100644
index 0000000000..b0f2b4ceb5
--- /dev/null
+++ b/mlir/include/PauliFrame/Transforms/Passes.td
@@ -0,0 +1,30 @@
+// Copyright 2025 Xanadu Quantum Technologies Inc.
+
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+
+// http://www.apache.org/licenses/LICENSE-2.0
+
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef PAULI_FRAME_PASSES
+#define PAULI_FRAME_PASSES
+
+include "mlir/Pass/PassBase.td"
+
+def CliffordTToPauliFramePass : Pass<"to-pauli-frame"> {
+ let summary = "Apply the Pauli frame tracking protocols to a Clifford+T quantum program.";
+
+ let dependentDialects = [
+ "scf::SCFDialect",
+ "catalyst::quantum::QuantumDialect",
+ "catalyst::pauli_frame::PauliFrameDialect"
+ ];
+}
+
+#endif // PAULI_FRAME_PASSES
diff --git a/mlir/include/PauliFrame/Transforms/Patterns.h b/mlir/include/PauliFrame/Transforms/Patterns.h
new file mode 100644
index 0000000000..6f27154a07
--- /dev/null
+++ b/mlir/include/PauliFrame/Transforms/Patterns.h
@@ -0,0 +1,25 @@
+// Copyright 2025 Xanadu Quantum Technologies Inc.
+
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+
+// http://www.apache.org/licenses/LICENSE-2.0
+
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#pragma once
+
+#include "mlir/IR/PatternMatch.h"
+
+namespace catalyst {
+namespace pauli_frame {
+
+void populateCliffordTToPauliFramePatterns(mlir::RewritePatternSet &patterns);
+
+} // namespace pauli_frame
+} // namespace catalyst
diff --git a/mlir/include/RegisterAllPasses.h b/mlir/include/RegisterAllPasses.h
index 496cc2f593..6980b36e09 100644
--- a/mlir/include/RegisterAllPasses.h
+++ b/mlir/include/RegisterAllPasses.h
@@ -19,6 +19,7 @@
#include "Ion/Transforms/Passes.h"
#include "MBQC/Transforms/Passes.h"
#include "Mitigation/Transforms/Passes.h"
+#include "PauliFrame/Transforms/Passes.h"
#include "QEC/Transforms/Passes.h"
#include "Quantum/Transforms/Passes.h"
#include "Test/Transforms/Passes.h"
@@ -34,6 +35,7 @@ inline void registerAllPasses()
ion::registerIonPasses();
mbqc::registerMBQCPasses();
mitigation::registerMitigationPasses();
+ pauli_frame::registerPauliFramePasses();
qec::registerQECPasses();
quantum::registerQuantumPasses();
test::registerTestPasses();
diff --git a/mlir/lib/Driver/CMakeLists.txt b/mlir/lib/Driver/CMakeLists.txt
index 3ffd467987..644828e4bd 100644
--- a/mlir/lib/Driver/CMakeLists.txt
+++ b/mlir/lib/Driver/CMakeLists.txt
@@ -42,6 +42,8 @@ set(LIBS
mbqc-transforms
MLIRMitigation
mitigation-transforms
+ MLIRPauliFrame
+ pauli-frame-transforms
MLIRIon
ion-transforms
MLIRRTIO
diff --git a/mlir/lib/Driver/CompilerDriver.cpp b/mlir/lib/Driver/CompilerDriver.cpp
index 6b53fec856..9c264fbe67 100644
--- a/mlir/lib/Driver/CompilerDriver.cpp
+++ b/mlir/lib/Driver/CompilerDriver.cpp
@@ -72,6 +72,7 @@
#include "Ion/IR/IonDialect.h"
#include "MBQC/IR/MBQCDialect.h"
#include "Mitigation/IR/MitigationDialect.h"
+#include "PauliFrame/IR/PauliFrameDialect.h"
#include "QEC/IR/QECDialect.h"
#include "Quantum/IR/QuantumDialect.h"
#include "Quantum/Transforms/BufferizableOpInterfaceImpl.h"
@@ -380,6 +381,7 @@ void registerAllCatalystDialects(DialectRegistry ®istry)
registry.insert();
registry.insert();
registry.insert();
+ registry.insert();
}
} // namespace
diff --git a/mlir/lib/PauliFrame/CMakeLists.txt b/mlir/lib/PauliFrame/CMakeLists.txt
index f33061b2d8..9f57627c32 100644
--- a/mlir/lib/PauliFrame/CMakeLists.txt
+++ b/mlir/lib/PauliFrame/CMakeLists.txt
@@ -1 +1,2 @@
add_subdirectory(IR)
+add_subdirectory(Transforms)
diff --git a/mlir/lib/PauliFrame/Transforms/CMakeLists.txt b/mlir/lib/PauliFrame/Transforms/CMakeLists.txt
new file mode 100644
index 0000000000..7556802b3c
--- /dev/null
+++ b/mlir/lib/PauliFrame/Transforms/CMakeLists.txt
@@ -0,0 +1,27 @@
+set(LIBRARY_NAME pauli-frame-transforms)
+
+
+file(GLOB SRC
+ CliffordTToPauliFramePatterns.cpp
+ to_pauli_frame.cpp
+)
+
+get_property(dialect_libs GLOBAL PROPERTY MLIR_DIALECT_LIBS)
+get_property(conversion_libs GLOBAL PROPERTY MLIR_CONVERSION_LIBS)
+set(LIBS
+ ${dialect_libs}
+ ${conversion_libs}
+ MLIRPauliFrame
+)
+
+set(DEPENDS
+ MLIRPauliFramePassIncGen
+)
+
+add_mlir_library(${LIBRARY_NAME} STATIC ${SRC} LINK_LIBS PRIVATE ${LIBS} DEPENDS ${DEPENDS})
+target_compile_features(${LIBRARY_NAME} PUBLIC cxx_std_20)
+
+target_include_directories(${LIBRARY_NAME} PUBLIC
+ .
+ ${PROJECT_SOURCE_DIR}/include
+ ${CMAKE_BINARY_DIR}/include)
diff --git a/mlir/lib/PauliFrame/Transforms/CliffordTToPauliFramePatterns.cpp b/mlir/lib/PauliFrame/Transforms/CliffordTToPauliFramePatterns.cpp
new file mode 100644
index 0000000000..91ed797683
--- /dev/null
+++ b/mlir/lib/PauliFrame/Transforms/CliffordTToPauliFramePatterns.cpp
@@ -0,0 +1,447 @@
+// Copyright 2025 Xanadu Quantum Technologies Inc.
+
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+
+// http://www.apache.org/licenses/LICENSE-2.0
+
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#define DEBUG_TYPE "to-pauli-frame"
+
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "PauliFrame/IR/PauliFrameOps.h"
+#include "PauliFrame/Transforms/Patterns.h"
+#include "Quantum/IR/QuantumDialect.h"
+#include "Quantum/IR/QuantumOps.h"
+
+using namespace mlir;
+
+namespace {
+
+using namespace catalyst::pauli_frame;
+using namespace catalyst::quantum;
+
+//===----------------------------------------------------------------------===//
+// Helper functions
+//===----------------------------------------------------------------------===//
+
+/**
+ * Concept for operations that have an observable operand (`obs`). This is generally used for
+ * operations representing measurement processes. This concept encapsulates the following
+ * requirements on type `T`:
+ *
+ * 1. The expression obj.getObs() must be valid
+ * 2. The type returned by obj.getObs() must be exactly TypedValue
+ */
+template
+concept OpWithObservable = requires(T obj) {
+ { obj.getObs() } -> std::same_as>;
+};
+
+// The supported Clifford+T gates
+enum class GateEnum { I, X, Y, Z, H, S, T, CNOT, Unknown };
+
+// Hash gate name to GateEnum
+GateEnum hashGate(CustomOp op)
+{
+ auto gateName = op.getGateName();
+ if (gateName == "Identity" || gateName == "I")
+ return GateEnum::I;
+ else if (gateName == "PauliX" || gateName == "X")
+ return GateEnum::X;
+ else if (gateName == "PauliY" || gateName == "Y")
+ return GateEnum::Y;
+ else if (gateName == "PauliZ" || gateName == "Z")
+ return GateEnum::Z;
+ else if (gateName == "H" || gateName == "Hadamard")
+ return GateEnum::H;
+ else if (gateName == "S")
+ return GateEnum::S;
+ else if (gateName == "T")
+ return GateEnum::T;
+ else if (gateName == "CNOT")
+ return GateEnum::CNOT;
+ else
+ return GateEnum::Unknown;
+}
+
+// Insert the ops that physically apply the Pauli X and Z gates and a flush op.
+// Applies the gates in the order X -> Z and returns the output qubit of the Z gate.
+OpResult insertPauliOpsAfterFlush(PatternRewriter &rewriter, Location loc, FlushOp flushOp)
+{
+ auto pauliXIfOp = rewriter.create(
+ loc, flushOp.getXParity(),
+ [&](OpBuilder &builder, Location loc) { // then
+ auto pauliX = rewriter.create(loc, "X", flushOp.getOutQubit());
+ builder.create(loc, pauliX.getOutQubits());
+ },
+ [&](OpBuilder &builder, Location loc) { // else
+ builder.create(loc, flushOp.getOutQubit());
+ });
+
+ auto pauliXOutQubit = pauliXIfOp->getResult(0);
+
+ auto pauliZIfOp = rewriter.create(
+ loc, flushOp.getZParity(),
+ [&](OpBuilder &builder, Location loc) { // then
+ auto pauliZ = rewriter.create(loc, "Z", pauliXOutQubit);
+ builder.create(loc, pauliZ.getOutQubits());
+ },
+ [&](OpBuilder &builder, Location loc) { // else
+ builder.create(loc, pauliXOutQubit);
+ });
+
+ return pauliZIfOp->getResult(0);
+}
+
+//===----------------------------------------------------------------------===//
+// Gate-conversion functions
+//===----------------------------------------------------------------------===//
+
+/**
+ * @brief Helper function to the Clifford+T -> PauliFrame pattern for Pauli gates (I, X, Y, Z).
+ *
+ * Performs the following rewrite, for example, from:
+ *
+ * %0 = ... : !quantum.bit
+ * %1 = quantum.custom "X"() %0 : !quantum.bit // or "I", "Y", "Z"
+ * %2 = %1 : ...
+ *
+ * to:
+ *
+ * %0 = ... : !quantum.bit
+ * %1 = pauli_frame.update[true, false] %0 : !quantum.bit // and similar for other Pauli gates
+ * %2 = %1 : ...
+ */
+LogicalResult convertPauliGate(CustomOp op, PatternRewriter &rewriter, bool x_parity, bool z_parity)
+{
+ LLVM_DEBUG(llvm::dbgs() << "Applying Pauli frame protocol to Pauli gate: " << op.getGateName()
+ << "\n");
+ auto loc = op->getLoc();
+ auto outQubitTypes = op.getOutQubits().getTypes();
+ auto inQubits = op.getInQubits();
+
+ UpdateOp updateOp =
+ rewriter.create(loc, outQubitTypes, rewriter.getBoolAttr(x_parity),
+ rewriter.getBoolAttr(z_parity), inQubits);
+
+ rewriter.replaceOp(op, updateOp.getOutQubits());
+ return success();
+}
+
+/**
+ * @brief Helper function to the Clifford+T -> PauliFrame pattern for Clifford gates (H, S, CNOT).
+ *
+ * Performs the following rewrite, for example, from:
+ *
+ * %0 = ... : !quantum.bit
+ * %1 = quantum.custom "Hadamard"() %0 : !quantum.bit
+ * %2 = %1 : ...
+ *
+ * to:
+ *
+ * %0 = ... : !quantum.bit
+ * %1 = pauli_frame.update_with_clifford[Hadamard] %0 : !quantum.bit
+ * %2 = quantum.custom "Hadamard"() %1 : !quantum.bit
+ * %3 = %2 : ...
+ *
+ * Note that since H = H†, CNOT = CNOT†, and since the Pauli conjugation relations for S and S† are
+ * equivalent up to a global phase, we need not consider the adjoint parameter of the quantum gate.
+ */
+LogicalResult convertCliffordGate(CustomOp op, PatternRewriter &rewriter, CliffordGate gate)
+{
+ LLVM_DEBUG(llvm::dbgs() << "Applying Pauli frame protocol to Clifford gate: "
+ << op.getGateName() << "\n");
+ auto loc = op->getLoc();
+ auto outQubitTypes = op.getOutQubits().getTypes();
+ auto inQubits = op.getInQubits();
+
+ UpdateWithCliffordOp updateOp =
+ rewriter.create(loc, outQubitTypes, gate, inQubits);
+
+ op->setOperands(updateOp->getResults());
+ return success();
+}
+
+/**
+ * @brief Helper function to the Clifford+T -> PauliFrame pattern for non-Clifford gates (T).
+ *
+ * Performs the following rewrite, for example, from:
+ *
+ * %0 = ... : !quantum.bit
+ * %1 = quantum.custom "T"() %0 : !quantum.bit
+ * %2 = %1 : ...
+ *
+ * to:
+ *
+ * %0 = ... : !quantum.bit
+ * %x_parity, %z_parity, %out_qubit = pauli_frame.flush %0 : i1, i1, !quantum.bit
+ * %1 = scf.if %x_parity -> (!quantum.bit) {
+ * %out_qubits_0 = quantum.custom "X"() %out_qubit : !quantum.bit
+ * scf.yield %out_qubits_0 : !quantum.bit
+ * } else {
+ * scf.yield %out_qubit : !quantum.bit
+ * }
+ * %2 = scf.if %z_parity -> (!quantum.bit) {
+ * %out_qubits_0 = quantum.custom "Z"() %1 : !quantum.bit
+ * scf.yield %out_qubits_0 : !quantum.bit
+ * } else {
+ * scf.yield %1 : !quantum.bit
+ * }
+ * %3 = quantum.custom "T"() %2 : !quantum.bit
+ * %4 = %3 : ...
+ */
+LogicalResult convertNonCliffordGate(CustomOp op, PatternRewriter &rewriter)
+{
+ LLVM_DEBUG(llvm::dbgs() << "Applying Pauli frame protocol to non-Clifford gate: "
+ << op.getGateName() << "\n");
+ auto loc = op->getLoc();
+ auto outQubitTypes = op.getOutQubits().getTypes();
+
+ if (outQubitTypes.size() > 1) {
+ op->emitError() << "Only single-qubit non-Clifford gates are supported";
+ return failure();
+ }
+
+ auto outQubitType = outQubitTypes[0];
+ auto inQubits = op.getInQubits();
+
+ FlushOp flushOp = rewriter.create(loc, rewriter.getI1Type(), rewriter.getI1Type(),
+ outQubitType, inQubits[0]);
+
+ auto pauliZOutQubit = insertPauliOpsAfterFlush(rewriter, loc, flushOp);
+
+ op->setOperands(pauliZOutQubit);
+
+ return success();
+}
+
+//===----------------------------------------------------------------------===//
+// Clifford+T to Pauli Frame Patterns
+//===----------------------------------------------------------------------===//
+
+/**
+ * @brief Rewrite pattern for Clifford+T ops -> PauliFrame
+ */
+struct CliffordTToPauliFramePattern : public OpRewritePattern {
+ using OpRewritePattern::OpRewritePattern;
+
+ LogicalResult matchAndRewrite(CustomOp op, PatternRewriter &rewriter) const override
+ {
+ auto op_enum = hashGate(op);
+ switch (op_enum) {
+ case GateEnum::I:
+ return convertPauliGate(op, rewriter, false, false);
+ case GateEnum::X:
+ return convertPauliGate(op, rewriter, true, false);
+ case GateEnum::Y:
+ return convertPauliGate(op, rewriter, true, true);
+ case GateEnum::Z:
+ return convertPauliGate(op, rewriter, false, true);
+ case GateEnum::H:
+ return convertCliffordGate(op, rewriter, CliffordGate::Hadamard);
+ case GateEnum::S:
+ return convertCliffordGate(op, rewriter, CliffordGate::S);
+ case GateEnum::CNOT:
+ return convertCliffordGate(op, rewriter, CliffordGate::CNOT);
+ case GateEnum::T:
+ return convertNonCliffordGate(op, rewriter);
+ case GateEnum::Unknown: {
+ op->emitError() << "Unsupported gate: '" << op.getGateName()
+ << "'. Only Clifford+T gates are supported for Pauli frame conversion: "
+ << "I, X, Y, Z, H, S, S†, T, T†, and CNOT";
+ return failure();
+ }
+ }
+ return success();
+ }
+};
+
+/**
+ * @brief Rewrite pattern for Pauli record initialization of a single qubit
+ *
+ * The Pauli records are initialized by inserting `pauli_frame.init` ops immediately after each
+ * single-qubit allocation op, `quantum.alloc_qb`, as follows, from:
+ *
+ * %0 = quantum.alloc_qb : !quantum.bit
+ * %1 = %0 : ...
+ *
+ * to:
+ *
+ * %0 = quantum.alloc_qb : !quantum.bit
+ * %1 = pauli_frame.init %0
+ * %2 = %1 : ...
+ */
+struct InitPauliRecordQbitPattern : public OpRewritePattern {
+ using OpRewritePattern::OpRewritePattern;
+
+ LogicalResult matchAndRewrite(AllocQubitOp op, PatternRewriter &rewriter) const override
+ {
+ auto loc = op->getLoc();
+ auto qubit = op.getQubit();
+ LLVM_DEBUG(llvm::dbgs() << "Initializing Pauli record of qubit: " << qubit << "\n");
+
+ rewriter.setInsertionPointAfter(op);
+ InitOp initOp = rewriter.create(loc, qubit.getType(), qubit);
+
+ qubit.replaceAllUsesExcept(initOp.getOutQubits()[0], initOp);
+ return success();
+ }
+};
+
+/**
+ * @brief Rewrite pattern for Pauli record initialization of a quantum register
+ *
+ * The Pauli records are initialized by inserting `pauli_frame.init_qreg` ops immediately after each
+ * register allocation op, `quantum.alloc`, as follows, from:
+ *
+ * %0 = quantum.alloc( 1) : !quantum.reg
+ * %1 = %0 : ...
+ *
+ * to:
+ *
+ * %0 = quantum.alloc( 1) : !quantum.reg
+ * %1 = pauli_frame.init_qreg %0 : !quantum.reg
+ * %2 = %1 : ...
+ */
+struct InitPauliRecordQregPattern : public OpRewritePattern {
+ using OpRewritePattern::OpRewritePattern;
+
+ LogicalResult matchAndRewrite(AllocOp op, PatternRewriter &rewriter) const override
+ {
+ auto loc = op->getLoc();
+ auto qreg = op.getQreg();
+ LLVM_DEBUG(llvm::dbgs() << "Initializing Pauli records of qubits in register: " << qreg
+ << "\n");
+
+ rewriter.setInsertionPointAfter(op);
+ InitQregOp initQregOp = rewriter.create(loc, qreg.getType(), qreg);
+
+ qreg.replaceAllUsesExcept(initQregOp.getOutQreg(), initQregOp);
+ return success();
+ }
+};
+
+/**
+ * @brief Rewrite pattern for measurement corrections
+ *
+ * Measurement results are corrected by inserting `pauli_frame.correct_measurement` ops immediately
+ * after each computational-basis mid-circuit measurement op, `quantum.measure`, as follows, from:
+ *
+ * %0 = ... : !quantum.bit
+ * %mres, %1 = quantum.measure %0 : i1, !quantum.bit
+ *
+ * to:
+ *
+ * %0 = ... : !quantum.bit
+ * %mres, %1 = quantum.measure %0 : i1, !quantum.bit
+ * %mres_1, %2 = pauli_frame.correct_measurement %mres, %1 : i1, !quantum.bit
+ */
+struct CorrectMeasurementPattern : public OpRewritePattern {
+ using OpRewritePattern::OpRewritePattern;
+
+ LogicalResult matchAndRewrite(MeasureOp op, PatternRewriter &rewriter) const override
+ {
+ auto loc = op->getLoc();
+ auto mres = op.getMres();
+ auto outQubit = op.getOutQubit();
+ LLVM_DEBUG(
+ llvm::dbgs() << "Applying Pauli frame protocol to correct measurement result of qubit: "
+ << outQubit << "\n");
+
+ rewriter.setInsertionPointAfter(op);
+ CorrectMeasurementOp correctMeasOp = rewriter.create(
+ loc, mres.getType(), outQubit.getType(), mres, outQubit);
+
+ mres.replaceAllUsesExcept(correctMeasOp.getOutMres(), correctMeasOp);
+ outQubit.replaceAllUsesExcept(correctMeasOp.getOutQubit(), correctMeasOp);
+ return success();
+ }
+};
+
+template
+struct FlushBeforeMeasurementProcessPattern : public OpRewritePattern {
+ using OpRewritePattern::OpRewritePattern;
+
+ LogicalResult matchAndRewrite(MeasurementProcessOp op, PatternRewriter &rewriter) const override
+ {
+ LLVM_DEBUG(llvm::dbgs() << "Applying Pauli frame protocol to flush Pauli record before "
+ "terminal measurement process: "
+ << op << "\n");
+ auto loc = op->getLoc();
+
+ auto obs = op.getObs();
+ if (!obs) {
+ op.emitError() << "Failed to flush Pauli record before terminal measurement process";
+ return failure();
+ }
+
+ auto obsOp = obs.getDefiningOp();
+
+ // The flush op will be inserted before the observable op
+ rewriter.setInsertionPoint(obsOp);
+
+ // Helper function to insert the flush operations per qubit operand of the observable op
+ auto insertFlushOpsPerQubit = [&](unsigned int idx, const Value qubit) {
+ auto flushOp = rewriter.create(loc, rewriter.getI1Type(), rewriter.getI1Type(),
+ qubit.getType(), qubit);
+ auto pauliZOutQubit = insertPauliOpsAfterFlush(rewriter, loc, flushOp);
+ obsOp->setOperand(idx, pauliZOutQubit);
+ };
+
+ if (auto compBasisOp = dyn_cast(obsOp)) {
+ auto qubits = compBasisOp.getQubits();
+ for (const auto &[idx, qubit] : llvm::enumerate(qubits)) {
+ insertFlushOpsPerQubit(idx, qubit);
+ }
+ }
+ else if (auto namedObsOp = dyn_cast(obsOp)) {
+ insertFlushOpsPerQubit(0, namedObsOp.getQubit());
+ }
+ else {
+ obsOp->emitError() << "Unsupported observable op: " << obsOp->getName();
+ return failure();
+ }
+
+ return success();
+ }
+};
+
+} // namespace
+
+namespace catalyst {
+namespace pauli_frame {
+
+void populateCliffordTToPauliFramePatterns(RewritePatternSet &patterns)
+{
+ patterns.add(patterns.getContext());
+ patterns.add(patterns.getContext());
+ patterns.add(patterns.getContext());
+ patterns.add(patterns.getContext());
+ patterns.add>(patterns.getContext());
+ patterns.add>(patterns.getContext());
+ patterns.add>(patterns.getContext());
+ patterns.add>(patterns.getContext());
+ patterns.add>(patterns.getContext());
+}
+
+} // namespace pauli_frame
+} // namespace catalyst
diff --git a/mlir/lib/PauliFrame/Transforms/to_pauli_frame.cpp b/mlir/lib/PauliFrame/Transforms/to_pauli_frame.cpp
new file mode 100644
index 0000000000..13fb34dd31
--- /dev/null
+++ b/mlir/lib/PauliFrame/Transforms/to_pauli_frame.cpp
@@ -0,0 +1,58 @@
+// Copyright 2025 Xanadu Quantum Technologies Inc.
+
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+
+// http://www.apache.org/licenses/LICENSE-2.0
+
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#define DEBUG_TYPE "to-pauli-frame"
+
+#include "mlir/Dialect/SCF/IR/SCF.h"
+#include "mlir/IR/Operation.h"
+#include "mlir/IR/PatternMatch.h"
+#include "mlir/Pass/Pass.h"
+#include "mlir/Transforms/WalkPatternRewriteDriver.h"
+#include "llvm/Support/Debug.h"
+#include "llvm/Support/LogicalResult.h"
+
+#include "PauliFrame/IR/PauliFrameOps.h"
+#include "PauliFrame/Transforms/Patterns.h"
+
+using namespace llvm;
+using namespace mlir;
+
+namespace catalyst {
+namespace pauli_frame {
+
+#define GEN_PASS_DECL_CLIFFORDTTOPAULIFRAMEPASS
+#define GEN_PASS_DEF_CLIFFORDTTOPAULIFRAMEPASS
+#include "PauliFrame/Transforms/Passes.h.inc"
+
+struct CliffordTToPauliFramePass : impl::CliffordTToPauliFramePassBase {
+ using CliffordTToPauliFramePassBase::CliffordTToPauliFramePassBase;
+
+ void runOnOperation() final
+ {
+ LLVM_DEBUG(dbgs() << "Clifford+T to Pauli frame pass\n");
+
+ Operation *module = getOperation();
+
+ RewritePatternSet patterns(&getContext());
+ populateCliffordTToPauliFramePatterns(patterns);
+
+ // NOTE: We want the walk-based pattern rewrite driver here, and not a greedy rewriter like
+ // applyPatternsGreedily, since we match on ops and do not replace them, therefore a greedy
+ // rewriter would loop infinitely.
+ walkAndApplyPatterns(module, std::move(patterns));
+ }
+};
+
+} // namespace pauli_frame
+} // namespace catalyst
diff --git a/mlir/test/PauliFrame/CliffordTToPauliFrame.mlir b/mlir/test/PauliFrame/CliffordTToPauliFrame.mlir
new file mode 100644
index 0000000000..34b424ffe0
--- /dev/null
+++ b/mlir/test/PauliFrame/CliffordTToPauliFrame.mlir
@@ -0,0 +1,287 @@
+// Copyright 2025 Xanadu Quantum Technologies Inc.
+
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+
+// http://www.apache.org/licenses/LICENSE-2.0
+
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// RUN: quantum-opt --to-pauli-frame --split-input-file --verify-diagnostics %s | FileCheck %s
+
+// CHECK-LABEL: test_to_pauli_frame_pauli_gates
+func.func @test_to_pauli_frame_pauli_gates(%arg0 : !quantum.bit) -> !quantum.bit {
+ // CHECK: [[q1:%.+]] = pauli_frame.update{{\s*}}[false, false] %arg0
+ %q1 = quantum.custom "I"() %arg0 : !quantum.bit
+ // CHECK: [[q2:%.+]] = pauli_frame.update{{\s*}}[true, false] [[q1]]
+ %q2 = quantum.custom "X"() %q1 : !quantum.bit
+ // CHECK: [[q3:%.+]] = pauli_frame.update{{\s*}}[true, true] [[q2]]
+ %q3 = quantum.custom "Y"() %q2 : !quantum.bit
+ // CHECK: [[q4:%.+]] = pauli_frame.update{{\s*}}[false, true] [[q3]]
+ %q4 = quantum.custom "Z"() %q3 : !quantum.bit
+ // CHECK-NOT: quantum.custom
+ // CHECK: return [[q4]]
+ func.return %q4 : !quantum.bit
+}
+
+// -----
+
+// CHECK-LABEL: test_to_pauli_frame_clifford_gates_single_qubit
+func.func @test_to_pauli_frame_clifford_gates_single_qubit(%arg0 : !quantum.bit) -> !quantum.bit {
+ // CHECK: [[q1a:%.+]] = pauli_frame.update_with_clifford[ Hadamard] %arg0
+ // CHECK-NEXT: [[q1b:%.+]] = quantum.custom "Hadamard"() [[q1a]]
+ %q1 = quantum.custom "Hadamard"() %arg0 : !quantum.bit
+ // CHECK: [[q2a:%.+]] = pauli_frame.update_with_clifford[ S] [[q1b]]
+ // CHECK-NEXT: [[q2b:%.+]] = quantum.custom "S"() [[q2a]]
+ %q2 = quantum.custom "S"() %q1 : !quantum.bit
+ // CHECK: return [[q2b]]
+ func.return %q2 : !quantum.bit
+}
+
+// -----
+
+// CHECK-LABEL: test_to_pauli_frame_clifford_gates_two_qubit
+func.func @test_to_pauli_frame_clifford_gates_two_qubit(%arg0 : !quantum.bit, %arg1 : !quantum.bit) -> (!quantum.bit, !quantum.bit) {
+ // CHECK: [[q1a:%.+]]:2 = pauli_frame.update_with_clifford[ CNOT] %arg0, %arg1
+ // CHECK-NEXT: [[q1b:%.+]]:2 = quantum.custom "CNOT"() [[q1a]]#0, [[q1a]]#1
+ %q1:2 = quantum.custom "CNOT"() %arg0, %arg1 : !quantum.bit, !quantum.bit
+ // CHECK: return [[q1b]]#0, [[q1b]]#1
+ func.return %q1#0, %q1#1 : !quantum.bit, !quantum.bit
+}
+
+// -----
+
+// CHECK-LABEL: test_to_pauli_frame_non_clifford_gate
+func.func @test_to_pauli_frame_non_clifford_gate(%arg0 : !quantum.bit) -> (!quantum.bit) {
+ // CHECK: [[xbit:%.+]], [[zbit:%.+]], [[q1:%.+]] = pauli_frame.flush %arg0
+ // CHECK: [[q2:%.+]] = scf.if [[xbit]] {{.*}} {
+ // CHECK: [[x_outq:%.+]] = quantum.custom "X"() [[q1]]
+ // CHECK: yield [[x_outq]]
+ // CHECK: } else {
+ // CHECK: yield [[q1]]
+ // CHECK: }
+ // CHECK: [[q3:%.+]] = scf.if [[zbit]] {{.*}} {
+ // CHECK: [[z_outq:%.+]] = quantum.custom "Z"() [[q2]]
+ // CHECK: yield [[z_outq]]
+ // CHECK: } else {
+ // CHECK: yield [[q2]]
+ // CHECK: }
+ // CHECK: [[q4:%.+]] = quantum.custom "T"() [[q3]]
+ %q1 = quantum.custom "T"() %arg0 : !quantum.bit
+ // CHECK: return [[q4]]
+ func.return %q1 : !quantum.bit
+}
+
+// -----
+
+// CHECK-LABEL: test_to_pauli_frame_init_qubit
+func.func @test_to_pauli_frame_init_qubit() -> !quantum.bit {
+ // CHECK: [[q1:%.+]] = quantum.alloc_qb
+ // CHECK-NEXT: [[q2:%.+]] = pauli_frame.init [[q1]]
+ %q = quantum.alloc_qb : !quantum.bit
+ // return [[q2]]
+ func.return %q : !quantum.bit
+}
+
+// -----
+
+// CHECK-LABEL: test_to_pauli_frame_init_qreg
+func.func @test_to_pauli_frame_init_qreg() {
+ // CHECK: [[qreg1:%.+]] = quantum.alloc( 1)
+ // CHECK-NEXT: [[qreg2:%.+]] = pauli_frame.init_qreg [[qreg1]]
+ // CHECK-NEXT: quantum.extract [[qreg2]][ 0]
+ %qreg = quantum.alloc( 1) : !quantum.reg
+ %q = quantum.extract %qreg[ 0] : !quantum.reg -> !quantum.bit
+ func.return
+}
+
+// -----
+
+// CHECK-LABEL: test_to_pauli_frame_correct_meas
+func.func @test_to_pauli_frame_correct_meas(%arg0 : !quantum.bit) -> (i1, !quantum.bit) {
+ // CHECK: [[mres1:%.+]], [[q1:%.+]] = quantum.measure %arg0
+ // CHECK-NEXT: [[mres2:%.+]], [[q2:%.+]] = pauli_frame.correct_measurement [[mres1]], [[q1]]
+ %mres, %q = quantum.measure %arg0 : i1, !quantum.bit
+ // CHECK: return [[mres2]], [[q2]]
+ func.return %mres, %q : i1, !quantum.bit
+}
+
+// -----
+
+// CHECK-LABEL: test_to_pauli_frame_expval_single_qubit
+func.func @test_to_pauli_frame_expval_single_qubit(%arg0 : !quantum.bit) -> f64 {
+ // CHECK: [[xbit:%.+]], [[zbit:%.+]], [[q1:%.+]] = pauli_frame.flush %arg0
+ // CHECK: [[q2:%.+]] = scf.if [[xbit]]
+ // CHECK: quantum.custom "X"() [[q1]]
+ // CHECK: else
+ // CHECK: [[q3:%.+]] = scf.if [[zbit]]
+ // CHECK: quantum.custom "Z"() [[q2]]
+ // CHECK: else
+ // CHECK: [[obs:%.+]] = quantum.namedobs [[q3]]
+ // CHECK: [[res:%.+]] = quantum.expval [[obs]]
+ %obs = quantum.namedobs %arg0 [PauliZ] : !quantum.obs
+ %res = quantum.expval %obs : f64
+ // CHECK: return [[res]]
+ func.return %res : f64
+}
+
+// -----
+
+// CHECK-LABEL: test_to_pauli_frame_var_single_qubit
+func.func @test_to_pauli_frame_var_single_qubit(%arg0 : !quantum.bit) -> f64 {
+ // CHECK: [[xbit:%.+]], [[zbit:%.+]], [[q1:%.+]] = pauli_frame.flush %arg0
+ // CHECK: [[q2:%.+]] = scf.if [[xbit]]
+ // CHECK: quantum.custom "X"() [[q1]]
+ // CHECK: else
+ // CHECK: [[q3:%.+]] = scf.if [[zbit]]
+ // CHECK: quantum.custom "Z"() [[q2]]
+ // CHECK: else
+ // CHECK: [[obs:%.+]] = quantum.namedobs [[q3]]
+ // CHECK: [[res:%.+]] = quantum.var [[obs]]
+ %obs = quantum.namedobs %arg0 [PauliZ] : !quantum.obs
+ %res = quantum.var %obs : f64
+ // CHECK: return [[res]]
+ func.return %res : f64
+}
+
+// -----
+
+// CHECK-LABEL: test_to_pauli_frame_sample_single_qubit
+func.func @test_to_pauli_frame_sample_single_qubit(%arg0 : !quantum.bit) -> tensor<1000x1xf64> {
+ // CHECK: [[xbit:%.+]], [[zbit:%.+]], [[q1:%.+]] = pauli_frame.flush %arg0
+ // CHECK: [[q2:%.+]] = scf.if [[xbit]]
+ // CHECK: quantum.custom "X"() [[q1]]
+ // CHECK: else
+ // CHECK: [[q3:%.+]] = scf.if [[zbit]]
+ // CHECK: quantum.custom "Z"() [[q2]]
+ // CHECK: else
+ // CHECK: [[obs:%.+]] = quantum.compbasis qubits [[q3]] : !quantum.obs
+ // CHECK: [[samples:%.+]] = quantum.sample [[obs]]
+ %obs = quantum.compbasis qubits %arg0 : !quantum.obs
+ %samples = quantum.sample %obs : tensor<1000x1xf64>
+ // CHECK: return [[samples]]
+ func.return %samples : tensor<1000x1xf64>
+}
+
+// -----
+
+// CHECK-LABEL: test_to_pauli_frame_sample_two_qubits
+func.func @test_to_pauli_frame_sample_two_qubits(%arg0 : !quantum.bit, %arg1 : !quantum.bit) -> tensor<1000x2xf64> {
+ // CHECK: [[xbit0:%.+]], [[zbit0:%.+]], [[q00:%.+]] = pauli_frame.flush %arg0
+ // CHECK: [[q01:%.+]] = scf.if [[xbit0]]
+ // CHECK: quantum.custom "X"() [[q00]]
+ // CHECK: else
+ // CHECK: [[q02:%.+]] = scf.if [[zbit0]]
+ // CHECK: quantum.custom "Z"() [[q01]]
+ // CHECK: else
+ // CHECK: [[xbit1:%.+]], [[zbit1:%.+]], [[q10:%.+]] = pauli_frame.flush %arg1
+ // CHECK: [[q11:%.+]] = scf.if [[xbit1]]
+ // CHECK: quantum.custom "X"() [[q10]]
+ // CHECK: else
+ // CHECK: [[q12:%.+]] = scf.if [[zbit]]
+ // CHECK: quantum.custom "Z"() [[q11]]
+ // CHECK: else
+ // CHECK: [[obs:%.+]] = quantum.compbasis qubits [[q02]], [[q12]] : !quantum.obs
+ // CHECK: [[samples:%.+]] = quantum.sample [[obs]]
+ %obs = quantum.compbasis qubits %arg0, %arg1 : !quantum.obs
+ %samples = quantum.sample %obs : tensor<1000x2xf64>
+ // CHECK: return [[samples]]
+ func.return %samples : tensor<1000x2xf64>
+}
+
+// -----
+
+// CHECK-LABEL: test_to_pauli_frame_counts_single_qubit
+func.func @test_to_pauli_frame_counts_single_qubit(%arg0 : !quantum.bit) -> tensor<2xi64> {
+ // CHECK: [[xbit:%.+]], [[zbit:%.+]], [[q1:%.+]] = pauli_frame.flush %arg0
+ // CHECK: [[q2:%.+]] = scf.if [[xbit]]
+ // CHECK: quantum.custom "X"() [[q1]]
+ // CHECK: else
+ // CHECK: [[q3:%.+]] = scf.if [[zbit]]
+ // CHECK: quantum.custom "Z"() [[q2]]
+ // CHECK: else
+ // CHECK: [[obs:%.+]] = quantum.compbasis qubits [[q3]] : !quantum.obs
+ // CHECK: {{%.+}}, [[counts:%.+]] = quantum.counts [[obs]]
+ %obs = quantum.compbasis qubits %arg0 : !quantum.obs
+ %eigvals, %counts = quantum.counts %obs : tensor<2xf64>, tensor<2xi64>
+ // CHECK: return [[counts]]
+ func.return %counts : tensor<2xi64>
+}
+
+// -----
+
+// CHECK-LABEL: test_to_pauli_frame_probs_single_qubit
+func.func @test_to_pauli_frame_probs_single_qubit(%arg0 : !quantum.bit) -> tensor<2xf64> {
+ // CHECK: [[xbit:%.+]], [[zbit:%.+]], [[q1:%.+]] = pauli_frame.flush %arg0
+ // CHECK: [[q2:%.+]] = scf.if [[xbit]]
+ // CHECK: quantum.custom "X"() [[q1]]
+ // CHECK: else
+ // CHECK: [[q3:%.+]] = scf.if [[zbit]]
+ // CHECK: quantum.custom "Z"() [[q2]]
+ // CHECK: else
+ // CHECK: [[obs:%.+]] = quantum.compbasis qubits [[q3]] : !quantum.obs
+ // CHECK: [[probs:%.+]] = quantum.probs [[obs]]
+ %obs = quantum.compbasis qubits %arg0 : !quantum.obs
+ %probs = quantum.probs %obs : tensor<2xf64>
+ // CHECK: return [[probs]]
+ func.return %probs : tensor<2xf64>
+}
+
+// -----
+
+// COM: This program represents the following circuit:
+// COM: 0: ──H──X─╭●──T──┤↗├─┤
+// COM: 1: ──S──Z─╰X──Y──┤↗├─┤
+// CHECK-LABEL: test_to_pauli_frame_integration
+func.func @test_to_pauli_frame_integration() -> (i1, i1) {
+ // CHECK: quantum.alloc( 2) : !quantum.reg
+ // CHECK: pauli_frame.init_qreg {{%.+}} : !quantum.reg
+ %qreg = quantum.alloc( 2) : !quantum.reg
+ // CHECK: quantum.extract {{%.+}}[ 0] : !quantum.reg -> !quantum.bit
+ // CHECK: quantum.extract {{%.+}}[ 1] : !quantum.reg -> !quantum.bit
+ %q00 = quantum.extract %qreg[ 0] : !quantum.reg -> !quantum.bit
+ %q10 = quantum.extract %qreg[ 1] : !quantum.reg -> !quantum.bit
+ // CHECK: pauli_frame.update_with_clifford[ Hadamard] {{%.+}} : !quantum.bit
+ // CHECK: quantum.custom "Hadamard"() {{%.+}} : !quantum.bit
+ %q01 = quantum.custom "Hadamard"() %q00 : !quantum.bit
+ // CHECK: pauli_frame.update_with_clifford[ S] {{%.+}} : !quantum.bit
+ // CHECK: quantum.custom "S"() {{%.+}} : !quantum.bit
+ %q11 = quantum.custom "S"() %q10 : !quantum.bit
+ // CHECK: pauli_frame.update[true, false] {{%.+}} : !quantum.bit
+ %q02 = quantum.custom "X"() %q01 : !quantum.bit
+ // CHECK: pauli_frame.update[false, true] {{%.+}} : !quantum.bit
+ %q12 = quantum.custom "Z"() %q11 : !quantum.bit
+ // CHECK: pauli_frame.update_with_clifford[ CNOT] {{%.+}}, {{%.+}} : !quantum.bit, !quantum.bit
+ // CHECK: quantum.custom "CNOT"() {{%.+}}, {{%.+}} : !quantum.bit, !quantum.bit
+ %q03, %q13 = quantum.custom "CNOT"() %q02, %q12 : !quantum.bit, !quantum.bit
+ // CHECK: pauli_frame.flush {{%.+}} : i1, i1, !quantum.bit
+ // CHECK: scf.if {{%.+}} -> (!quantum.bit) {
+ // CHECK: quantum.custom "X"() {{%.+}} : !quantum.bit
+ // CHECK: scf.yield {{%.+}} : !quantum.bit
+ // CHECK: } else {
+ // CHECK: scf.yield {{%.+}} : !quantum.bit
+ // CHECK: }
+ // CHECK: scf.if {{%.+}} -> (!quantum.bit) {
+ // CHECK: quantum.custom "Z"() {{%.+}} : !quantum.bit
+ // CHECK: scf.yield {{%.+}} : !quantum.bit
+ // CHECK: } else {
+ // CHECK: scf.yield {{%.+}} : !quantum.bit
+ // CHECK: }
+ // CHECK: quantum.custom "T"() {{%.+}} : !quantum.bit
+ %q04 = quantum.custom "T"() %q03 : !quantum.bit
+ // CHECK: pauli_frame.update[true, true] {{%.+}} : !quantum.bit
+ %q14 = quantum.custom "Y"() %q13 : !quantum.bit
+ // CHECK: quantum.measure {{%.+}} : i1, !quantum.bit
+ // CHECK: pauli_frame.correct_measurement {{%.+}}, {{%.+}} : i1, !quantum.bit
+ %mres0, %q05 = quantum.measure %q04 : i1, !quantum.bit
+ // CHECK: quantum.measure {{%.+}} : i1, !quantum.bit
+ // CHECK: pauli_frame.correct_measurement {{%.+}}, {{%.+}} : i1, !quantum.bit
+ %mres1, %q15 = quantum.measure %q14 : i1, !quantum.bit
+ // CHECK: return {{%.+}}, {{%.+}} : i1, i1
+ func.return %mres0, %mres1 : i1, i1
+}
diff --git a/mlir/tools/catalyst-cli/CMakeLists.txt b/mlir/tools/catalyst-cli/CMakeLists.txt
index caa6754285..39bd44ecf4 100644
--- a/mlir/tools/catalyst-cli/CMakeLists.txt
+++ b/mlir/tools/catalyst-cli/CMakeLists.txt
@@ -39,6 +39,7 @@ set(LIBS
MLIRMitigation
mitigation-transforms
MLIRPauliFrame
+ pauli-frame-transforms
MLIRIon
ion-transforms
MLIRRTIO
diff --git a/mlir/tools/quantum-opt/CMakeLists.txt b/mlir/tools/quantum-opt/CMakeLists.txt
index f9d08252a1..3fc81bda36 100644
--- a/mlir/tools/quantum-opt/CMakeLists.txt
+++ b/mlir/tools/quantum-opt/CMakeLists.txt
@@ -23,6 +23,7 @@ set(LIBS
MLIRMitigation
mitigation-transforms
MLIRPauliFrame
+ pauli-frame-transforms
MLIRIon
ion-transforms
MLIRRTIO