diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md
index 20f2e46918..bcc8f7ff57 100644
--- a/doc/releases/changelog-dev.md
+++ b/doc/releases/changelog-dev.md
@@ -2,6 +2,13 @@
New features since last release
+* A new PennyLane operation :func:`~.fabricate` has been added to expose the PBC
+ ``pbc.fabricate`` instruction from the frontend. The operation produces a new auxiliary
+ qubit in a logical factory state (``plus_i``, ``minus_i``, ``magic``, or ``magic_conj``)
+ and is lowered through the ``pbc.ref.fabricate`` reference-semantics op to
+ ``pbc.fabricate`` during compilation.
+ [(#3019)](https://github.com/PennyLaneAI/catalyst/pull/3019)
+
* The `local-random` unitary folding option for :func:`~.mitigate_with_zne` is now implemented,
reproducing Mitiq's ``fold_gates_at_random``: every gate is folded ``floor((scale_factor-1)/2)``
times, then a random subset is folded once more (without replacement) to reach ``scale_factor * n``
diff --git a/frontend/catalyst/api_extensions/__init__.py b/frontend/catalyst/api_extensions/__init__.py
index 6c020597c8..50e6d41622 100644
--- a/frontend/catalyst/api_extensions/__init__.py
+++ b/frontend/catalyst/api_extensions/__init__.py
@@ -37,8 +37,10 @@
MidCircuitPauliMeasure,
adjoint,
ctrl,
+ deallocate,
measure,
pauli_measure,
+ fabricate,
)
__all__ = (
@@ -58,6 +60,8 @@
"vmap",
"measure",
"pauli_measure",
+ "fabricate",
+ "deallocate",
"adjoint",
"ctrl",
)
diff --git a/frontend/catalyst/api_extensions/quantum_operators.py b/frontend/catalyst/api_extensions/quantum_operators.py
index 959f939bba..bfc79cfb94 100644
--- a/frontend/catalyst/api_extensions/quantum_operators.py
+++ b/frontend/catalyst/api_extensions/quantum_operators.py
@@ -46,7 +46,14 @@
deduce_avals,
new_inner_tracer,
)
-from catalyst.jax_primitives import AbstractQreg, adjoint_p, measure_p, pauli_measure_p
+from catalyst.jax_primitives import (
+ AbstractQreg,
+ adjoint_p,
+ fabricate_p,
+ measure_p,
+ pauli_measure_p,
+ qdealloc_qb_p,
+)
from catalyst.jax_tracer import (
HybridOp,
HybridOpRegion,
@@ -243,6 +250,56 @@ def pauli_measure(
return m
+def fabricate(init_state: str) -> DynamicJaxprTracer:
+ r"""A :func:`qjit` compatible fabricate operation for PennyLane/Catalyst.
+
+ .. important::
+
+ Under the legacy tracing pathway, use :func:`catalyst.fabricate` or ensure
+ :func:`qp.fabricate() ` is called from within an active
+ :func:`~.qjit` compilation.
+
+ Args:
+ init_state (str): The logical state to fabricate. One of ``"plus_i"``,
+ ``"minus_i"``, ``"magic"``, or ``"magic_conj"``.
+
+ Returns:
+ A JAX tracer for the fabricated qubit.
+ """
+ EvaluationContext.check_is_tracing("catalyst.fabricate can only be used from within @qjit.")
+ EvaluationContext.check_is_quantum_tracing(
+ "catalyst.fabricate can only be used from within a qp.qnode."
+ )
+
+ valid_states = {"plus_i", "minus_i", "magic", "magic_conj"}
+ if init_state not in valid_states:
+ raise ValueError(
+ f'The init_state "{init_state}" is not allowed. '
+ f"Allowed values are {sorted(valid_states)}."
+ )
+
+ (qubit,) = fabricate_p.bind(init_state=init_state)
+ return qubit
+
+
+def deallocate(*qubits) -> None:
+ r"""A :func:`qjit` compatible deallocate operation for standalone fabricated qubits.
+
+ Deallocates one or more standalone value-semantics qubits, such as those returned
+ by :func:`fabricate`.
+
+ Args:
+ *qubits: Standalone qubit tracers to deallocate.
+ """
+ EvaluationContext.check_is_tracing("catalyst.deallocate can only be used from within @qjit.")
+ EvaluationContext.check_is_quantum_tracing(
+ "catalyst.deallocate can only be used from within a qp.qnode."
+ )
+
+ for qubit in qubits:
+ qdealloc_qb_p.bind(qubit)
+
+
def adjoint(f: Union[Callable, Operator], lazy=True) -> Union[Callable, Operator]:
"""A :func:`~.qjit` compatible adjoint transformer for PennyLane/Catalyst.
diff --git a/frontend/catalyst/device/qjit_device.py b/frontend/catalyst/device/qjit_device.py
index cc6ef89e53..144cd62bf5 100644
--- a/frontend/catalyst/device/qjit_device.py
+++ b/frontend/catalyst/device/qjit_device.py
@@ -82,6 +82,7 @@
"MultiRZ",
"PauliRot",
"PauliMeasure",
+ "Fabricate",
"PauliX",
"PauliY",
"PauliZ",
diff --git a/frontend/catalyst/from_plxpr/qfunc_interpreter.py b/frontend/catalyst/from_plxpr/qfunc_interpreter.py
index f644fe1337..6a1b0d1cac 100644
--- a/frontend/catalyst/from_plxpr/qfunc_interpreter.py
+++ b/frontend/catalyst/from_plxpr/qfunc_interpreter.py
@@ -27,6 +27,7 @@
from pennylane.capture import PlxprInterpreter, pause
from pennylane.capture.primitives import cond_prim as pl_cond_prim
from pennylane.capture.primitives import ctrl_transform_prim as plxpr_ctrl_transform_prim
+from pennylane.capture.primitives import fabricate_prim as plxpr_fabricate_prim
from pennylane.capture.primitives import measure_prim as plxpr_measure_prim
from pennylane.capture.primitives import operator_p
from pennylane.capture.primitives import pauli_measure_prim as plxpr_pauli_measure_prim
@@ -40,6 +41,8 @@
qref_alloc_p,
qref_compbasis_p,
qref_dealloc_p,
+ qref_dealloc_qb_p,
+ qref_fabricate_p,
qref_get_p,
qref_gphase_p,
qref_hermitian_p,
@@ -427,17 +430,25 @@ def handle_allocate(self, *, num_wires, state=None, restored=False):
def handle_deallocate(self, *wires):
"""Handle the conversion from plxpr to Catalyst jaxpr for the qp.deallocate primitive"""
qregs = set()
+ standalone_qubits = []
for w in wires:
- get_op = w.parent
+ parent_eqn = w.parent
+ if parent_eqn.primitive is qref_fabricate_p:
+ standalone_qubits.append(w)
+ elif parent_eqn.primitive is qref_get_p:
+ qreg = parent_eqn.in_tracers[0]
+ qregs.add(qreg)
+ else:
+ raise AssertionError(
+ "Manual deallocation is only supported for manually allocated or fabricated wires"
+ )
+ for qubit in standalone_qubits:
+ qref_dealloc_qb_p.bind(qubit)
+ if qregs:
assert (
- get_op.primitive is qref_get_p
- ), "Manual deallocation is only supported for manually allocated wires"
- qreg = get_op.in_tracers[0]
- qregs.add(qreg)
- assert (
- len(qregs) == 1
- ), "Expected all wires to deallocate to come from the same allocation instruction"
- qref_dealloc_p.bind(list(qregs)[0])
+ len(qregs) == 1
+ ), "Expected all wires to deallocate to come from the same allocation instruction"
+ qref_dealloc_p.bind(list(qregs)[0])
return []
@@ -543,6 +554,13 @@ def wrapper(*args):
return ()
+@PLxPRToQuantumJaxprInterpreter.register_primitive(plxpr_fabricate_prim)
+def handle_fabricate(self, *_, init_state=""):
+ """Handle the conversion from plxpr to Catalyst jaxpr for the fabricate primitive"""
+ (qubit,) = qref_fabricate_p.bind(init_state=init_state)
+ return (qubit,)
+
+
@PLxPRToQuantumJaxprInterpreter.register_primitive(plxpr_pauli_measure_prim)
def handle_pauli_measure(self, *wires_inval, pauli_word, **params):
"""Handle the conversion from plxpr to Catalyst jaxpr for the PauliMeasure primitive"""
diff --git a/frontend/catalyst/from_plxpr/qref_jax_primitives.py b/frontend/catalyst/from_plxpr/qref_jax_primitives.py
index 903942dfe3..103e8583f2 100644
--- a/frontend/catalyst/from_plxpr/qref_jax_primitives.py
+++ b/frontend/catalyst/from_plxpr/qref_jax_primitives.py
@@ -40,6 +40,7 @@
from catalyst.jax_extras.patches import mock_attributes
from catalyst.jax_primitives import (
AbstractObs,
+ _logical_init_attr,
_named_obs_attribute,
extract_scalar,
safe_cast_to_f64,
@@ -61,13 +62,14 @@
),
):
from mlir_quantum.dialects.mbqc import RefMeasureInBasisOp
- from mlir_quantum.dialects.pbc import RefPPMeasurementOp
+ from mlir_quantum.dialects.pbc import RefFabricateOp, RefPPMeasurementOp
from mlir_quantum.dialects.qref import (
AdjointOp,
AllocOp,
ComputationalBasisOp,
CustomOp,
DeallocOp,
+ DeallocQubitOp,
GetOp,
GlobalPhaseOp,
HermitianOp,
@@ -147,6 +149,8 @@ class MeasurementPlane(Enum):
qref_alloc_p = Primitive("qref_alloc")
qref_dealloc_p = Primitive("qref_dealloc")
qref_dealloc_p.multiple_results = True
+qref_dealloc_qb_p = Primitive("qref_dealloc_qb")
+qref_dealloc_qb_p.multiple_results = True
qref_get_p = Primitive("qref_get")
qref_set_state_p = Primitive("qref_state_prep")
qref_set_state_p.multiple_results = True
@@ -157,6 +161,8 @@ class MeasurementPlane(Enum):
qref_gphase_p = Primitive("qref_gphase")
qref_gphase_p.multiple_results = True
qref_pauli_measure_p = Primitive("pref_pauli_measure")
+qref_fabricate_p = Primitive("qref_fabricate")
+qref_fabricate_p.multiple_results = True
qref_pauli_rot_p = Primitive("qref_pauli_rot")
qref_pauli_rot_p.multiple_results = True
qref_unitary_p = Primitive("qref_unitary")
@@ -215,6 +221,21 @@ def _qref_dealloc_lowering(jax_ctx: mlir.LoweringRuleContext, qreg):
return ()
+#
+# qref_dealloc_qb_p
+#
+@qref_dealloc_qb_p.def_abstract_eval
+def _qref_dealloc_qb_abstract_eval(qubit):
+ return ()
+
+
+def _qref_dealloc_qb_lowering(jax_ctx: mlir.LoweringRuleContext, qubit):
+ ctx = jax_ctx.module_context.context
+ ctx.allow_unregistered_dialects = True
+ DeallocQubitOp(qubit=qubit)
+ return ()
+
+
#
# qref_get_p
#
@@ -542,6 +563,24 @@ def _qref_pauli_measure_lowering(
return (from_elements_op.results[0],)
+#
+# fabricate operation
+#
+@qref_fabricate_p.def_abstract_eval
+def _qref_fabricate_abstract_eval(*_, init_state=""):
+ return (AbstractQubit(),)
+
+
+def _qref_fabricate_lowering(jax_ctx: mlir.LoweringRuleContext, *_, init_state=""):
+ ctx = jax_ctx.module_context.context
+ ctx.allow_unregistered_dialects = True
+
+ qubit_type = ir.OpaqueType.get("qref", "bit", ctx)
+ return RefFabricateOp(
+ qubits=[qubit_type], init_state=_logical_init_attr(ctx, init_state)
+ ).results
+
+
#
# qubit unitary operation
#
@@ -826,6 +865,7 @@ def _qref_hermitian_lowering(jax_ctx: mlir.LoweringRuleContext, matrix: ir.Value
(qref_operator_p, _qref_operator_p_lowering),
(qref_alloc_p, _qref_alloc_lowering),
(qref_dealloc_p, _qref_dealloc_lowering),
+ (qref_dealloc_qb_p, _qref_dealloc_qb_lowering),
(qref_get_p, _qref_get_lowering),
(qref_set_state_p, _qref_set_state_lowering),
(qref_set_basis_state_p, _qref_set_basis_state_lowering),
@@ -833,6 +873,7 @@ def _qref_hermitian_lowering(jax_ctx: mlir.LoweringRuleContext, matrix: ir.Value
(qref_gphase_p, _qref_gphase_lowering),
(qref_pauli_rot_p, _qref_pauli_rot_lowering),
(qref_pauli_measure_p, _qref_pauli_measure_lowering),
+ (qref_fabricate_p, _qref_fabricate_lowering),
(qref_unitary_p, _qref_unitary_lowering),
(qref_measure_p, _qref_measure_lowering),
(qref_measure_in_basis_p, _qref_measure_in_basis_lowering),
diff --git a/frontend/catalyst/jax_primitives.py b/frontend/catalyst/jax_primitives.py
index 4be7ff097e..cc3086aa91 100644
--- a/frontend/catalyst/jax_primitives.py
+++ b/frontend/catalyst/jax_primitives.py
@@ -87,7 +87,7 @@
VJPOp,
)
from mlir_quantum.dialects.mitigation import ZneOp
- from mlir_quantum.dialects.pbc import PPMeasurementOp
+ from mlir_quantum.dialects.pbc import FabricateOp, PPMeasurementOp
from mlir_quantum.dialects.quantum import (
AdjointOp,
AllocOp,
@@ -298,6 +298,8 @@ class Folding(Enum):
pauli_rot_p.multiple_results = True
pauli_measure_p = Primitive("pauli_measure")
pauli_measure_p.multiple_results = True
+fabricate_p = Primitive("fabricate")
+fabricate_p.multiple_results = True
measure_p = Primitive("measure")
measure_p.multiple_results = True
compbasis_p = Primitive("compbasis")
@@ -1660,6 +1662,29 @@ def _pauli_measure_lowering(
return (from_elements_op.results[0],) + tuple(out_qubits)
+#
+# fabricate operation
+#
+@fabricate_p.def_abstract_eval
+def _fabricate_abstract_eval(*_, init_state=""):
+ return (AbstractQbit(),)
+
+
+@fabricate_p.def_impl
+def _fabricate_def_impl(*args, **kwargs): # pragma: no cover
+ raise NotImplementedError()
+
+
+def _fabricate_lowering(jax_ctx: mlir.LoweringRuleContext, *_, init_state=""):
+ ctx = jax_ctx.module_context.context
+ ctx.allow_unregistered_dialects = True
+
+ qubit_type = ir.OpaqueType.get("quantum", "bit", ctx)
+ return FabricateOp(
+ out_qubits=[qubit_type], init_state=_logical_init_attr(ctx, init_state)
+ ).results
+
+
#
# measure
#
@@ -1760,6 +1785,10 @@ def _namedobs_abstract_eval(qubit, kind):
return AbstractObs()
+def _logical_init_attr(ctx, init_state: str):
+ return ir.Attribute.parse(f"#pbc", context=ctx)
+
+
def _named_obs_attribute(ctx, kind: str):
return ir.OpaqueAttr.get(
"quantum", ("named_observable " + kind).encode("utf-8"), ir.NoneType.get(ctx), ctx
@@ -3080,6 +3109,7 @@ def subroutine_lowering(*args, **kwargs):
(unitary_p, _unitary_lowering),
(pauli_rot_p, _pauli_rot_lowering),
(pauli_measure_p, _pauli_measure_lowering),
+ (fabricate_p, _fabricate_lowering),
(measure_p, _measure_lowering),
(compbasis_p, _compbasis_lowering),
(namedobs_p, _named_obs_lowering),
diff --git a/frontend/catalyst/jax_tracer.py b/frontend/catalyst/jax_tracer.py
index 3eb26bae34..a619c66dc6 100644
--- a/frontend/catalyst/jax_tracer.py
+++ b/frontend/catalyst/jax_tracer.py
@@ -86,6 +86,7 @@
)
from catalyst.jax_extras.tracing import uses_transform
from catalyst.jax_primitives import (
+ AbstractQbit,
AbstractQreg,
compbasis_p,
counts_p,
@@ -398,6 +399,11 @@ def unify_convert_result_types(jaxprs, consts, nimplouts):
return jaxpr_acc, type_acc[0], tracers_acc, consts_acc
+def _is_standalone_qubit(wire) -> bool:
+ """Return True if ``wire`` is a standalone value-semantics qubit tracer."""
+ return isinstance(wire, Tracer) and isinstance(get_aval(wire), AbstractQbit)
+
+
class QRegPromise:
"""QReg adaptor tracing the qubit extractions and insertions. The adaptor works by postponing
the insertions in order to re-use qubits later thus skipping the extractions."""
@@ -413,12 +419,21 @@ def extract(self, wires: List[Any], allow_reuse=False) -> List[DynamicJaxprTrace
from cache"""
# pylint: disable=consider-iterating-dictionary
qrp = self
- cached_tracers = {w for w in qrp.cache.keys() if isinstance(w, Tracer)}
- requested_tracers = {w for w in wires if isinstance(w, Tracer)}
+ cached_tracers = {w for w in qrp.cache.keys() if _is_standalone_qubit(w)}
+ requested_tracers = {w for w in wires if _is_standalone_qubit(w)}
if cached_tracers != requested_tracers:
qrp.actualize()
qubits = []
for w in wires:
+ if _is_standalone_qubit(w):
+ if w in qrp.cache and qrp.cache[w] is not None:
+ qubit = qrp.cache[w]
+ qubits.append(qubit)
+ if not allow_reuse:
+ qrp.cache[w] = None
+ else:
+ qubits.append(w)
+ continue
if w in qrp.cache:
qubit = qrp.cache[w]
assert (
@@ -447,10 +462,15 @@ def actualize(self) -> DynamicJaxprTracer:
"""Prune the qubit cache by performing the postponed insertions."""
qrp = self
qreg = qrp.base
+ standalone_cache = {}
for w, qubit in qrp.cache.items():
- if qubit is not None:
+ if qubit is None:
+ continue
+ if _is_standalone_qubit(w):
+ standalone_cache[w] = qubit
+ else:
qreg = qinsert_p.bind(qreg, w, qubit)
- qrp.cache = {}
+ qrp.cache = standalone_cache
qrp.base = qreg
return qreg
diff --git a/frontend/test/lit/test_fabricate.py b/frontend/test/lit/test_fabricate.py
new file mode 100644
index 0000000000..a834f8675e
--- /dev/null
+++ b/frontend/test/lit/test_fabricate.py
@@ -0,0 +1,54 @@
+# Copyright 2026 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: %PYTHON %s | FileCheck %s
+
+"""Test fabricate lowering to pbc.fabricate."""
+
+import pennylane as qp
+
+from catalyst import qjit
+
+_PIPELINE = [
+ (
+ "pipe",
+ [
+ "canonicalize",
+ "verify-no-quantum-use-after-free",
+ "convert-to-value-semantics",
+ "canonicalize",
+ ],
+ )
+]
+
+
+def test_fabricate_lowering():
+ """Test that qp.fabricate is lowered to pbc.fabricate."""
+ dev = qp.device("null.qubit", wires=2)
+
+ @qjit(pipelines=_PIPELINE, target="mlir", capture=True)
+ @qp.qnode(device=dev)
+ def circuit():
+ magic = qp.fabricate("magic")
+ qp.pauli_measure("ZZ", wires=[0, magic])
+ qp.deallocate(magic)
+ return qp.expval(qp.Z(0))
+
+ # CHECK: pbc.fabricate magic
+ # CHECK-NOT: pbc.ref.fabricate
+ # CHECK: pbc.ppm ["Z", "Z"]
+ print(circuit.mlir_opt)
+
+
+test_fabricate_lowering()
diff --git a/frontend/test/pytest/conftest.py b/frontend/test/pytest/conftest.py
index 1e2e2901f2..70af9b13d7 100644
--- a/frontend/test/pytest/conftest.py
+++ b/frontend/test/pytest/conftest.py
@@ -58,14 +58,19 @@ def use_capture():
@pytest.fixture(scope="function")
-def use_capture_dgraph():
+def enable_graph_decomposition():
+ """Enable graph-decomposition around each test."""
+ with qp.decomposition.toggle_graph_ctx(True):
+ yield
+
+
+@pytest.fixture(scope="function")
+def use_capture_dgraph(enable_graph_decomposition):
"""Enable capture and graph-decomposition before and disable them both after the test."""
qp.capture.enable()
- qp.decomposition.enable_graph()
try:
yield
finally:
- qp.decomposition.disable_graph()
qp.capture.disable()
diff --git a/frontend/test/pytest/test_fabricate.py b/frontend/test/pytest/test_fabricate.py
new file mode 100644
index 0000000000..6ad3af737f
--- /dev/null
+++ b/frontend/test/pytest/test_fabricate.py
@@ -0,0 +1,109 @@
+# Copyright 2026 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.
+
+"""Tests for the fabricate operation."""
+
+import pytest
+
+import pennylane as qp
+from pennylane.ops.qubit.fabricate import Fabricate, fabricate, _VALID_INIT_STATES
+
+from catalyst import qjit
+
+_FABRICATE_PIPELINE = [
+ (
+ "pipe",
+ [
+ "canonicalize",
+ "verify-no-quantum-use-after-free",
+ "convert-to-value-semantics",
+ "canonicalize",
+ ],
+ )
+]
+
+
+class TestFabricateOp:
+ """Tests for the Fabricate operator and function."""
+
+ @pytest.mark.parametrize("init_state", sorted(_VALID_INIT_STATES))
+ def test_fabricate_valid_init_states(self, init_state):
+ """Test that valid init states are accepted."""
+ op = Fabricate(init_state)
+ assert op.init_state == init_state
+ assert len(op.wires) == 0
+
+ def test_fabricate_invalid_init_state(self):
+ """Test that invalid init states are rejected."""
+ with pytest.raises(ValueError, match="not allowed"):
+ Fabricate("zero")
+
+ @pytest.mark.parametrize("init_state", sorted(_VALID_INIT_STATES))
+ @pytest.mark.usefixtures("use_capture")
+ def test_fabricate_function_capture(self, init_state):
+ """Test fabricate primitive binding under capture."""
+ import jax
+
+ def circuit():
+ fabricate(init_state)
+
+ jaxpr = jax.make_jaxpr(circuit)().jaxpr
+ assert any(eqn.primitive.name == "fabricate" for eqn in jaxpr.eqns)
+
+ @pytest.mark.usefixtures("enable_graph_decomposition")
+ def test_fabricate_mlir_lowering(self):
+ """Test that fabricate appears as pbc.fabricate in optimized MLIR."""
+ dev = qp.device("null.qubit", wires=1)
+
+ @qjit(pipelines=_FABRICATE_PIPELINE, target="mlir", capture=True)
+ @qp.qnode(device=dev)
+ def circuit():
+ magic = fabricate("magic_conj")
+ qp.pauli_measure("Z", wires=[magic])
+ qp.deallocate(magic)
+ return qp.expval(qp.Z(0))
+
+ mlir = circuit.mlir_opt
+ assert "pbc.fabricate" in mlir and "magic_conj" in mlir
+ assert "pbc.ref.fabricate" not in mlir
+
+ @pytest.mark.usefixtures("enable_graph_decomposition")
+ def test_fabricate_mlir_lowering_capture_false(self):
+ """Test fabricate lowering via the legacy tracing pathway."""
+ dev = qp.device("null.qubit", wires=2)
+ pipe = [
+ (
+ "pipe",
+ [
+ "canonicalize",
+ "verify-no-quantum-use-after-free",
+ "convert-to-value-semantics",
+ "canonicalize",
+ ],
+ )
+ ]
+
+ @qjit(pipelines=pipe, target="mlir", capture=False)
+ @qp.qnode(device=dev)
+ def circuit():
+ magic = fabricate("magic")
+ qp.pauli_measure("ZZ", wires=[0, magic])
+ qp.deallocate(magic)
+ return qp.expval(qp.Z(0))
+
+ mlir = circuit.mlir_opt
+ assert "pbc.fabricate" in mlir and "magic" in mlir
+ assert "pbc.ref.fabricate" not in mlir
+ assert "pbc.ppm" in mlir
+ assert "quantum.dealloc_qb" in mlir
diff --git a/mlir/include/PBC/IR/PBCRefOps.td b/mlir/include/PBC/IR/PBCRefOps.td
index aa9989dd61..f80e1ee07f 100644
--- a/mlir/include/PBC/IR/PBCRefOps.td
+++ b/mlir/include/PBC/IR/PBCRefOps.td
@@ -37,3 +37,26 @@ def RefPPMeasurementOp : PBC_Op<"ref.ppm"> {
let hasVerifier = 1;
}
+
+def RefFabricateOp : PBC_Op<"ref.fabricate"> {
+ let summary = "Fabricate auxiliary qubits from qubit factories (reference semantics).";
+
+ let description = [{
+ Identical to the pbc.fabricate operation, except in reference semantics.
+ }];
+
+ let arguments = (ins
+ LogicalInit:$init_state
+ );
+
+ let results = (outs
+ Variadic:$qubits
+ );
+
+ let assemblyFormat = [{
+ $init_state attr-dict `:` type($qubits)
+ }];
+
+ let hasCanonicalizeMethod = 1;
+ let hasVerifier = 1;
+}
diff --git a/mlir/lib/PBC/IR/PBCOps.cpp b/mlir/lib/PBC/IR/PBCOps.cpp
index 49aca12605..f5746d08fb 100644
--- a/mlir/lib/PBC/IR/PBCOps.cpp
+++ b/mlir/lib/PBC/IR/PBCOps.cpp
@@ -23,6 +23,7 @@
#include "mlir/IR/Region.h"
#include "QRef/IR/QRefDialect.h"
+#include "QRef/IR/QRefTypes.h"
#include "Quantum/IR/QuantumDialect.h"
using namespace mlir;
@@ -91,6 +92,26 @@ LogicalResult RefPPMeasurementOp::verify()
return success();
}
+LogicalResult RefFabricateOp::verify()
+{
+ auto initState = getInitState();
+ if (initState == LogicalInitKind::zero || initState == LogicalInitKind::one ||
+ initState == LogicalInitKind::plus || initState == LogicalInitKind::minus) {
+ return emitOpError("Logical state should not be fabricated, use `PrepareStateOp` instead.");
+ }
+ return mlir::success();
+}
+
+LogicalResult RefFabricateOp::canonicalize(RefFabricateOp op, PatternRewriter &rewriter)
+{
+ if (op->use_empty()) {
+ rewriter.eraseOp(op);
+ return success();
+ }
+
+ return failure();
+}
+
LogicalResult SelectPPMeasurementOp::verify()
{
if (getInQubits().size() != getPauliProduct_0().size() ||
diff --git a/mlir/lib/QRef/Transforms/value_semantics_conversion.cpp b/mlir/lib/QRef/Transforms/value_semantics_conversion.cpp
index 68cb97ea67..98f9d8d611 100644
--- a/mlir/lib/QRef/Transforms/value_semantics_conversion.cpp
+++ b/mlir/lib/QRef/Transforms/value_semantics_conversion.cpp
@@ -1208,6 +1208,17 @@ void handlePPM(IRRewriter &builder, pbc::RefPPMeasurementOp rPPMOp, QubitValueTr
builder.eraseOp(rPPMOp);
}
+void handleFabricate(IRRewriter &builder, pbc::RefFabricateOp rFabricateOp,
+ QubitValueTracker &tracker)
+{
+ OpBuilder::InsertionGuard guard(builder);
+ builder.setInsertionPoint(rFabricateOp);
+ Location loc = rFabricateOp.getLoc();
+
+ auto vFabricateOp = pbc::FabricateOp::create(builder, loc, rFabricateOp.getInitState());
+ tracker.setCurrentVQubit(rFabricateOp.getQubits().front(), vFabricateOp.getOutQubits().front());
+}
+
void handleCall(IRRewriter &builder, func::CallOp callOp, QubitValueTracker &tracker)
{
OpBuilder::InsertionGuard guard(builder);
@@ -1878,6 +1889,7 @@ void handleRegion(IRRewriter &builder, Region &r, QubitValueTracker &tracker)
.Case(
[&](auto o) { handleMeasureInBasis(builder, o, tracker); })
.Case([&](auto o) { handlePPM(builder, o, tracker); })
+ .Case([&](auto o) { handleFabricate(builder, o, tracker); })
.Case([&](auto o) { handleAdjoint(builder, o, tracker); })
.Case([&](auto o) { handleIf(builder, o, tracker); })
.Case([&](auto o) { handleSwitch(builder, o, tracker); })