From 0c4246fb81aa4f15ddfe763c3da5a353e57b7a4e Mon Sep 17 00:00:00 2001 From: Callum Macpherson Date: Mon, 17 Nov 2025 17:56:00 +0000 Subject: [PATCH 01/10] add overwrite_hugr method --- hugr-py/src/hugr/hugr/base.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/hugr-py/src/hugr/hugr/base.py b/hugr-py/src/hugr/hugr/base.py index 10396ed38..35fcd0715 100644 --- a/hugr-py/src/hugr/hugr/base.py +++ b/hugr-py/src/hugr/hugr/base.py @@ -934,6 +934,14 @@ def insert_hugr(self, hugr: Hugr, parent: ToNode | None = None) -> dict[Node, No ) return mapping + def overwrite_hugr(self, new_hugr: Hugr) -> None: + """Modify a Hugr in place by replacing attributes with those from a new Hugr.""" + self.module_root = new_hugr.module_root + self.entrypoint = new_hugr.entrypoint + self._nodes = new_hugr._nodes + self._links = new_hugr._links + self._free_nodes = new_hugr._free_nodes + def _to_serial(self) -> SerialHugr: """Serialize the HUGR.""" From 63e120a67b9001132a1c3cf98e45de37bc4d9a64 Mon Sep 17 00:00:00 2001 From: Callum Macpherson Date: Tue, 18 Nov 2025 11:47:25 +0000 Subject: [PATCH 02/10] update ComposablePass impl to have a Hugr return type --- hugr-py/src/hugr/passes/_composable_pass.py | 24 +++++++++++++++++---- hugr-py/tests/test_passes.py | 4 ++-- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/hugr-py/src/hugr/passes/_composable_pass.py b/hugr-py/src/hugr/passes/_composable_pass.py index 8da632346..fbaa294dd 100644 --- a/hugr-py/src/hugr/passes/_composable_pass.py +++ b/hugr-py/src/hugr/passes/_composable_pass.py @@ -5,6 +5,7 @@ from __future__ import annotations +from copy import deepcopy from dataclasses import dataclass from typing import TYPE_CHECKING, Protocol, runtime_checkable @@ -16,9 +17,23 @@ class ComposablePass(Protocol): """A Protocol which represents a composable Hugr transformation.""" - def __call__(self, hugr: Hugr) -> None: + def __call__(self, hugr: Hugr, inplace: bool = True) -> Hugr: """Call the pass to transform a HUGR.""" - ... + if inplace: + return self._apply_inplace(hugr) + else: + return self._apply(hugr) + + # At least one of the following must be ovewritten + def _apply(self, hugr: Hugr) -> Hugr: + hugr = deepcopy(hugr) + self._apply_inplace(hugr) + return hugr + + def _apply_inplace(self, hugr: Hugr) -> Hugr: + new_hugr = self._apply(hugr) + hugr.overwrite_hugr(new_hugr) + return hugr @property def name(self) -> str: @@ -48,7 +63,8 @@ class ComposedPass(ComposablePass): passes: list[ComposablePass] - def __call__(self, hugr: Hugr): + def __call__(self, hugr: Hugr, inplace: bool = True) -> Hugr: """Call all of the passes in sequence.""" for comp_pass in self.passes: - comp_pass(hugr) + comp_pass(hugr, inplace) + return hugr diff --git a/hugr-py/tests/test_passes.py b/hugr-py/tests/test_passes.py index 45f117350..a89c99dd3 100644 --- a/hugr-py/tests/test_passes.py +++ b/hugr-py/tests/test_passes.py @@ -4,8 +4,8 @@ def test_composable_pass() -> None: class MyDummyPass(ComposablePass): - def __call__(self, hugr: Hugr) -> None: - return self(hugr) + def __call__(self, hugr: Hugr, inplace: bool = True) -> Hugr: + return self(hugr, inplace) def then(self, other: ComposablePass) -> ComposablePass: return ComposedPass([self, other]) From e83887c6c737660815ce96f124ba16be0fd29e6e Mon Sep 17 00:00:00 2001 From: Callum Macpherson Date: Tue, 18 Nov 2025 12:11:11 +0000 Subject: [PATCH 03/10] apply some of Agustin's suggestions --- hugr-py/src/hugr/hugr/base.py | 2 +- hugr-py/src/hugr/passes/_composable_pass.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/hugr-py/src/hugr/hugr/base.py b/hugr-py/src/hugr/hugr/base.py index 35fcd0715..80234f315 100644 --- a/hugr-py/src/hugr/hugr/base.py +++ b/hugr-py/src/hugr/hugr/base.py @@ -934,7 +934,7 @@ def insert_hugr(self, hugr: Hugr, parent: ToNode | None = None) -> dict[Node, No ) return mapping - def overwrite_hugr(self, new_hugr: Hugr) -> None: + def _overwrite_hugr(self, new_hugr: Hugr) -> None: """Modify a Hugr in place by replacing attributes with those from a new Hugr.""" self.module_root = new_hugr.module_root self.entrypoint = new_hugr.entrypoint diff --git a/hugr-py/src/hugr/passes/_composable_pass.py b/hugr-py/src/hugr/passes/_composable_pass.py index fbaa294dd..6c37c01a7 100644 --- a/hugr-py/src/hugr/passes/_composable_pass.py +++ b/hugr-py/src/hugr/passes/_composable_pass.py @@ -20,7 +20,8 @@ class ComposablePass(Protocol): def __call__(self, hugr: Hugr, inplace: bool = True) -> Hugr: """Call the pass to transform a HUGR.""" if inplace: - return self._apply_inplace(hugr) + self._apply_inplace(hugr) + return hugr else: return self._apply(hugr) @@ -30,10 +31,9 @@ def _apply(self, hugr: Hugr) -> Hugr: self._apply_inplace(hugr) return hugr - def _apply_inplace(self, hugr: Hugr) -> Hugr: + def _apply_inplace(self, hugr: Hugr) -> None: new_hugr = self._apply(hugr) - hugr.overwrite_hugr(new_hugr) - return hugr + hugr._overwrite_hugr(new_hugr) @property def name(self) -> str: From 0e1ffa9575248c058fb4d652a1f9f48ea5af905d Mon Sep 17 00:00:00 2001 From: Callum Macpherson Date: Tue, 18 Nov 2025 16:24:15 +0000 Subject: [PATCH 04/10] fix ComposedPass __call__ impl --- hugr-py/src/hugr/passes/_composable_pass.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/hugr-py/src/hugr/passes/_composable_pass.py b/hugr-py/src/hugr/passes/_composable_pass.py index 6c37c01a7..f54af8f8c 100644 --- a/hugr-py/src/hugr/passes/_composable_pass.py +++ b/hugr-py/src/hugr/passes/_composable_pass.py @@ -65,6 +65,12 @@ class ComposedPass(ComposablePass): def __call__(self, hugr: Hugr, inplace: bool = True) -> Hugr: """Call all of the passes in sequence.""" - for comp_pass in self.passes: - comp_pass(hugr, inplace) - return hugr + if inplace: + for comp_pass in self.passes: + comp_pass(hugr, True) + return hugr + + else: + for comp_pass in self.passes: + res = comp_pass(hugr, False) + return res From 71a0a869805f21c752f7416013a1672defc86e43 Mon Sep 17 00:00:00 2001 From: Callum Macpherson <93673602+CalMacCQ@users.noreply.github.com> Date: Wed, 19 Nov 2025 11:56:52 +0000 Subject: [PATCH 05/10] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Agustín Borgna <121866228+aborgna-q@users.noreply.github.com> --- hugr-py/src/hugr/passes/_composable_pass.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hugr-py/src/hugr/passes/_composable_pass.py b/hugr-py/src/hugr/passes/_composable_pass.py index f54af8f8c..2d17390e5 100644 --- a/hugr-py/src/hugr/passes/_composable_pass.py +++ b/hugr-py/src/hugr/passes/_composable_pass.py @@ -17,7 +17,7 @@ class ComposablePass(Protocol): """A Protocol which represents a composable Hugr transformation.""" - def __call__(self, hugr: Hugr, inplace: bool = True) -> Hugr: + def __call__(self, hugr: Hugr, *, inplace: bool = True) -> Hugr: """Call the pass to transform a HUGR.""" if inplace: self._apply_inplace(hugr) @@ -63,7 +63,7 @@ class ComposedPass(ComposablePass): passes: list[ComposablePass] - def __call__(self, hugr: Hugr, inplace: bool = True) -> Hugr: + def __call__(self, hugr: Hugr, *, inplace: bool = True) -> Hugr: """Call all of the passes in sequence.""" if inplace: for comp_pass in self.passes: From e29aafdfb3f2c029523ebef63b214d15d6823f46 Mon Sep 17 00:00:00 2001 From: Callum Macpherson Date: Wed, 19 Nov 2025 12:01:01 +0000 Subject: [PATCH 06/10] fix name --- hugr-py/src/hugr/passes/_composable_pass.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/hugr-py/src/hugr/passes/_composable_pass.py b/hugr-py/src/hugr/passes/_composable_pass.py index 2d17390e5..129ddde9a 100644 --- a/hugr-py/src/hugr/passes/_composable_pass.py +++ b/hugr-py/src/hugr/passes/_composable_pass.py @@ -67,10 +67,14 @@ def __call__(self, hugr: Hugr, *, inplace: bool = True) -> Hugr: """Call all of the passes in sequence.""" if inplace: for comp_pass in self.passes: - comp_pass(hugr, True) + comp_pass(hugr, inplace=True) return hugr else: for comp_pass in self.passes: - res = comp_pass(hugr, False) + res = comp_pass(hugr, inplace=False) return res + + @property + def name(self) -> str: + return f"Composed({ ', '.join(pass_.name for pass_ in self.passes) })" From 1b6559cf97aaf0889d750549629b878bbe4f394b Mon Sep 17 00:00:00 2001 From: Callum Macpherson Date: Wed, 19 Nov 2025 13:51:33 +0000 Subject: [PATCH 07/10] add _apply and _apply_inplace for ComposedPass --- hugr-py/src/hugr/passes/_composable_pass.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/hugr-py/src/hugr/passes/_composable_pass.py b/hugr-py/src/hugr/passes/_composable_pass.py index 129ddde9a..fe894bd99 100644 --- a/hugr-py/src/hugr/passes/_composable_pass.py +++ b/hugr-py/src/hugr/passes/_composable_pass.py @@ -25,7 +25,7 @@ def __call__(self, hugr: Hugr, *, inplace: bool = True) -> Hugr: else: return self._apply(hugr) - # At least one of the following must be ovewritten + # At least one of the following _apply methods must be ovewritten def _apply(self, hugr: Hugr) -> Hugr: hugr = deepcopy(hugr) self._apply_inplace(hugr) @@ -63,17 +63,14 @@ class ComposedPass(ComposablePass): passes: list[ComposablePass] - def __call__(self, hugr: Hugr, *, inplace: bool = True) -> Hugr: - """Call all of the passes in sequence.""" - if inplace: - for comp_pass in self.passes: - comp_pass(hugr, inplace=True) - return hugr + def _apply(self, hugr: Hugr) -> Hugr: + for comp_pass in self.passes: + res = comp_pass(hugr, inplace=False) + return res - else: - for comp_pass in self.passes: - res = comp_pass(hugr, inplace=False) - return res + def _apply_inplace(self, hugr: Hugr) -> None: + for comp_pass in self.passes: + comp_pass(hugr, inplace=True) @property def name(self) -> str: From bfb6f26b9510cded105d272e8be9678a915c1aa9 Mon Sep 17 00:00:00 2001 From: Callum Macpherson Date: Wed, 19 Nov 2025 14:40:50 +0000 Subject: [PATCH 08/10] apply suggestions from @acl-cqc --- hugr-py/src/hugr/passes/_composable_pass.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/hugr-py/src/hugr/passes/_composable_pass.py b/hugr-py/src/hugr/passes/_composable_pass.py index fe894bd99..ac698c52e 100644 --- a/hugr-py/src/hugr/passes/_composable_pass.py +++ b/hugr-py/src/hugr/passes/_composable_pass.py @@ -25,7 +25,7 @@ def __call__(self, hugr: Hugr, *, inplace: bool = True) -> Hugr: else: return self._apply(hugr) - # At least one of the following _apply methods must be ovewritten + # At least one of the following _apply methods must be overriden def _apply(self, hugr: Hugr) -> Hugr: hugr = deepcopy(hugr) self._apply_inplace(hugr) @@ -64,9 +64,10 @@ class ComposedPass(ComposablePass): passes: list[ComposablePass] def _apply(self, hugr: Hugr) -> Hugr: + result_hugr = hugr for comp_pass in self.passes: - res = comp_pass(hugr, inplace=False) - return res + result_hugr = comp_pass(result_hugr, inplace=False) + return result_hugr def _apply_inplace(self, hugr: Hugr) -> None: for comp_pass in self.passes: From 80370522118173f4e7d113d1e2d2a22a78cad4bf Mon Sep 17 00:00:00 2001 From: Callum Macpherson Date: Wed, 19 Nov 2025 14:41:46 +0000 Subject: [PATCH 09/10] docstring --- hugr-py/src/hugr/hugr/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hugr-py/src/hugr/hugr/base.py b/hugr-py/src/hugr/hugr/base.py index 80234f315..38e90a34c 100644 --- a/hugr-py/src/hugr/hugr/base.py +++ b/hugr-py/src/hugr/hugr/base.py @@ -935,7 +935,7 @@ def insert_hugr(self, hugr: Hugr, parent: ToNode | None = None) -> dict[Node, No return mapping def _overwrite_hugr(self, new_hugr: Hugr) -> None: - """Modify a Hugr in place by replacing attributes with those from a new Hugr.""" + """Modify a Hugr in place by replacing contents with those from a new Hugr.""" self.module_root = new_hugr.module_root self.entrypoint = new_hugr.entrypoint self._nodes = new_hugr._nodes From 859c8111b498fb21532274ef862318d4eaf0a876 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agust=C3=ADn=20Borgna?= Date: Thu, 20 Nov 2025 14:07:29 +0000 Subject: [PATCH 10/10] idea: Alternative to multiple ComposablePass apply methods --- hugr-py/src/hugr/passes/_composable_pass.py | 82 +++++++++++++++------ hugr-py/tests/test_passes.py | 8 +- 2 files changed, 64 insertions(+), 26 deletions(-) diff --git a/hugr-py/src/hugr/passes/_composable_pass.py b/hugr-py/src/hugr/passes/_composable_pass.py index ac698c52e..371e8206a 100644 --- a/hugr-py/src/hugr/passes/_composable_pass.py +++ b/hugr-py/src/hugr/passes/_composable_pass.py @@ -10,6 +10,8 @@ from typing import TYPE_CHECKING, Protocol, runtime_checkable if TYPE_CHECKING: + from collections.abc import Callable + from hugr.hugr.base import Hugr @@ -18,22 +20,10 @@ class ComposablePass(Protocol): """A Protocol which represents a composable Hugr transformation.""" def __call__(self, hugr: Hugr, *, inplace: bool = True) -> Hugr: - """Call the pass to transform a HUGR.""" - if inplace: - self._apply_inplace(hugr) - return hugr - else: - return self._apply(hugr) - - # At least one of the following _apply methods must be overriden - def _apply(self, hugr: Hugr) -> Hugr: - hugr = deepcopy(hugr) - self._apply_inplace(hugr) - return hugr + """Call the pass to transform a HUGR. - def _apply_inplace(self, hugr: Hugr) -> None: - new_hugr = self._apply(hugr) - hugr._overwrite_hugr(new_hugr) + See :func:`_impl_pass_call` for a helper function to implement this method. + """ @property def name(self) -> str: @@ -57,21 +47,65 @@ def then(self, other: ComposablePass) -> ComposablePass: return ComposedPass(pass_list) +def impl_pass_call( + *, + hugr: Hugr, + inplace: bool, + inplace_call: Callable[[Hugr], None] | None = None, + copy_call: Callable[[Hugr], Hugr] | None = None, +) -> Hugr: + """Helper function to implement a ComposablePass.__call__ method, given an + inplace or copy-returning pass methods. + + At least one of the `inplace_call` or `copy_call` arguments must be provided. + + :param hugr: The Hugr to apply the pass to. + :param inplace: Whether to apply the pass inplace. + :param inplace_call: The method to apply the pass inplace. + :param copy_call: The method to apply the pass by copying the Hugr. + :return: The transformed Hugr. + """ + if inplace and inplace_call is not None: + inplace_call(hugr) + return hugr + elif inplace and copy_call is not None: + new_hugr = copy_call(hugr) + hugr._overwrite_hugr(new_hugr) + return hugr + elif not inplace and copy_call is not None: + return copy_call(hugr) + elif not inplace and inplace_call is not None: + new_hugr = deepcopy(hugr) + inplace_call(new_hugr) + return new_hugr + else: + msg = "Pass must implement at least an inplace or copy run method" + raise ValueError(msg) + + @dataclass class ComposedPass(ComposablePass): """A sequence of composable passes.""" passes: list[ComposablePass] - def _apply(self, hugr: Hugr) -> Hugr: - result_hugr = hugr - for comp_pass in self.passes: - result_hugr = comp_pass(result_hugr, inplace=False) - return result_hugr - - def _apply_inplace(self, hugr: Hugr) -> None: - for comp_pass in self.passes: - comp_pass(hugr, inplace=True) + def __call__(self, hugr: Hugr, *, inplace: bool = True) -> Hugr: + def apply(hugr: Hugr) -> Hugr: + result_hugr = hugr + for comp_pass in self.passes: + result_hugr = comp_pass(result_hugr, inplace=False) + return result_hugr + + def apply_inplace(hugr: Hugr) -> None: + for comp_pass in self.passes: + comp_pass(hugr, inplace=True) + + return impl_pass_call( + hugr=hugr, + inplace=inplace, + inplace_call=apply_inplace, + copy_call=apply, + ) @property def name(self) -> str: diff --git a/hugr-py/tests/test_passes.py b/hugr-py/tests/test_passes.py index 0d4e9aff5..64426dcda 100644 --- a/hugr-py/tests/test_passes.py +++ b/hugr-py/tests/test_passes.py @@ -1,11 +1,15 @@ from hugr.hugr.base import Hugr -from hugr.passes._composable_pass import ComposablePass, ComposedPass +from hugr.passes._composable_pass import ComposablePass, ComposedPass, impl_pass_call def test_composable_pass() -> None: class MyDummyPass(ComposablePass): def __call__(self, hugr: Hugr, inplace: bool = True) -> Hugr: - return self(hugr, inplace) + return impl_pass_call( + hugr=hugr, + inplace=inplace, + inplace_call=lambda hugr: None, + ) dummy = MyDummyPass()