From 0c4246fb81aa4f15ddfe763c3da5a353e57b7a4e Mon Sep 17 00:00:00 2001 From: Callum Macpherson Date: Mon, 17 Nov 2025 17:56:00 +0000 Subject: [PATCH 01/24] 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 10396ed38b..35fcd07153 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/24] 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 8da6323463..fbaa294dda 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 45f117350c..a89c99dd3a 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/24] 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 35fcd07153..80234f315f 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 fbaa294dda..6c37c01a7a 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/24] 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 6c37c01a7a..f54af8f8c6 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/24] 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 f54af8f8c6..2d17390e56 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/24] 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 2d17390e56..129ddde9ac 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/24] 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 129ddde9ac..fe894bd991 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/24] 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 fe894bd991..ac698c52e0 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/24] 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 80234f315f..38e90a34cf 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/24] 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 ac698c52e0..371e8206a4 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 0d4e9aff59..64426dcdad 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() From d79a03124c50ca1c1f2739e04c768789adc5e906 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agust=C3=ADn=20Borgna?= Date: Thu, 20 Nov 2025 13:52:39 +0000 Subject: [PATCH 11/24] feat: PassResult definition --- hugr-py/src/hugr/passes/_composable_pass.py | 141 ++++++++++++++------ hugr-py/tests/test_passes.py | 21 ++- 2 files changed, 117 insertions(+), 45 deletions(-) diff --git a/hugr-py/src/hugr/passes/_composable_pass.py b/hugr-py/src/hugr/passes/_composable_pass.py index 371e8206a4..49ea6ac0c0 100644 --- a/hugr-py/src/hugr/passes/_composable_pass.py +++ b/hugr-py/src/hugr/passes/_composable_pass.py @@ -6,8 +6,8 @@ from __future__ import annotations from copy import deepcopy -from dataclasses import dataclass -from typing import TYPE_CHECKING, Protocol, runtime_checkable +from dataclasses import dataclass, field +from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable if TYPE_CHECKING: from collections.abc import Callable @@ -20,7 +20,11 @@ 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. + """Call the pass to transform a HUGR, returning a Hugr.""" + return self.run(hugr, inplace=inplace).hugr + + def run(self, hugr: Hugr, *, inplace: bool = True) -> PassResult: + """Run the pass to transform a HUGR, returning a PassResult. See :func:`_impl_pass_call` for a helper function to implement this method. """ @@ -32,29 +36,17 @@ def name(self) -> str: def then(self, other: ComposablePass) -> ComposablePass: """Perform another composable pass after this pass.""" - # Provide a default implementation for composing passes. - pass_list = [] - if isinstance(self, ComposedPass): - pass_list.extend(self.passes) - else: - pass_list.append(self) - - if isinstance(other, ComposedPass): - pass_list.extend(other.passes) - else: - pass_list.append(other) - - return ComposedPass(pass_list) + return ComposedPass(self, other) -def impl_pass_call( +def impl_pass_run( *, 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_call: Callable[[Hugr], PassResult] | None = None, + copy_call: Callable[[Hugr], PassResult] | None = None, +) -> PassResult: + """Helper function to implement a ComposablePass.run method, given an inplace or copy-returning pass methods. At least one of the `inplace_call` or `copy_call` arguments must be provided. @@ -63,21 +55,25 @@ def impl_pass_call( :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. + :return: The result of the pass application. + :raises ValueError: If neither `inplace_call` nor `copy_call` is provided. """ if inplace and inplace_call is not None: - inplace_call(hugr) - return hugr + return inplace_call(hugr) elif inplace and copy_call is not None: - new_hugr = copy_call(hugr) - hugr._overwrite_hugr(new_hugr) - return hugr + pass_result = copy_call(hugr) + pass_result.hugr = hugr + if pass_result.modified: + hugr._overwrite_hugr(pass_result.hugr) + pass_result.original_dirty = True + return pass_result 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 + pass_result = inplace_call(new_hugr) + pass_result.original_dirty = False + return pass_result else: msg = "Pass must implement at least an inplace or copy run method" raise ValueError(msg) @@ -89,24 +85,89 @@ class ComposedPass(ComposablePass): passes: list[ComposablePass] - 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: + def __init__(self, *passes: ComposablePass) -> None: + self.passes = [] + for pass_ in passes: + if isinstance(pass_, ComposedPass): + self.passes.extend(pass_.passes) + else: + self.passes.append(pass_) + + def run(self, hugr: Hugr, *, inplace: bool = True) -> PassResult: + def apply(hugr: Hugr) -> PassResult: + pass_result = PassResult(hugr=hugr) for comp_pass in self.passes: - comp_pass(hugr, inplace=True) + new_result = comp_pass.run(pass_result.hugr, inplace=inplace) + pass_result = pass_result.then(new_result) + return pass_result - return impl_pass_call( + return impl_pass_run( hugr=hugr, inplace=inplace, - inplace_call=apply_inplace, + inplace_call=apply, copy_call=apply, ) @property def name(self) -> str: return f"Composed({ ', '.join(pass_.name for pass_ in self.passes) })" + + +@dataclass +class PassResult: + """The result of a series of composed passes applied to a HUGR. + + Includes a flag indicating whether the passes modified the HUGR, and an + arbitrary result object for each pass. + + In some cases, `modified` may be set to `True` even if the pass did not + modify the program. + + :attr hugr: The transformed Hugr. + :attr original_dirty: Whether the original HUGR was modified by the pass. + :attr modified: Whether the pass made changes to the HUGR. + :attr results: The result of each applied pass, as a tuple of the pass and + the result. + """ + + hugr: Hugr + original_dirty: bool = False + modified: bool = False + results: list[tuple[ComposablePass, Any]] = field(default_factory=list) + + @classmethod + def for_pass( + cls, + pass_: ComposablePass, + hugr: Hugr, + *, + result: Any, + inline: bool, + modified: bool = True, + ) -> PassResult: + """Create a new PassResult after a pass application. + + :param hugr: The Hugr that was transformed. + :param pass_: The pass that was applied. + :param result: The result of the pass application. + :param inline: Whether the pass was applied inplace. + :param modified: Whether the pass modified the HUGR. + """ + return cls( + hugr=hugr, + original_dirty=inline and modified, + modified=modified, + results=[(pass_, result)], + ) + + def then(self, other: PassResult) -> PassResult: + """Extend the PassResult with the results of another PassResult. + + Keeps the hugr returned by the last pass. + """ + return PassResult( + hugr=other.hugr, + original_dirty=self.original_dirty or other.original_dirty, + modified=self.modified or other.modified, + results=self.results + other.results, + ) diff --git a/hugr-py/tests/test_passes.py b/hugr-py/tests/test_passes.py index 64426dcdad..d40d7c259d 100644 --- a/hugr-py/tests/test_passes.py +++ b/hugr-py/tests/test_passes.py @@ -1,21 +1,32 @@ from hugr.hugr.base import Hugr -from hugr.passes._composable_pass import ComposablePass, ComposedPass, impl_pass_call +from hugr.passes._composable_pass import ( + ComposablePass, + ComposedPass, + PassResult, + impl_pass_run, +) def test_composable_pass() -> None: class MyDummyPass(ComposablePass): - def __call__(self, hugr: Hugr, inplace: bool = True) -> Hugr: - return impl_pass_call( + def run(self, hugr: Hugr, inplace: bool = True) -> PassResult: + return impl_pass_run( hugr=hugr, inplace=inplace, - inplace_call=lambda hugr: None, + inplace_call=lambda hugr: PassResult.for_pass( + self, + hugr, + result=None, + inline=True, + modified=False, + ), ) dummy = MyDummyPass() composed_dummies = dummy.then(dummy) - my_composed_pass = ComposedPass([dummy, dummy]) + my_composed_pass = ComposedPass(dummy, dummy) assert my_composed_pass.passes == [dummy, dummy] assert isinstance(composed_dummies, ComposablePass) From b3eabdeee36a639eabefa8ac2a188a62c67ff16b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agust=C3=ADn=20Borgna?= Date: Thu, 20 Nov 2025 14:50:33 +0000 Subject: [PATCH 12/24] Use pass names rather than objects, so the result is serializable --- hugr-py/src/hugr/passes/_composable_pass.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/hugr-py/src/hugr/passes/_composable_pass.py b/hugr-py/src/hugr/passes/_composable_pass.py index 49ea6ac0c0..76b915520b 100644 --- a/hugr-py/src/hugr/passes/_composable_pass.py +++ b/hugr-py/src/hugr/passes/_composable_pass.py @@ -15,6 +15,10 @@ from hugr.hugr.base import Hugr +# Type alias for a pass name +PassName = str + + @runtime_checkable class ComposablePass(Protocol): """A Protocol which represents a composable Hugr transformation.""" @@ -30,7 +34,7 @@ def run(self, hugr: Hugr, *, inplace: bool = True) -> PassResult: """ @property - def name(self) -> str: + def name(self) -> PassName: """Returns the name of the pass.""" return self.__class__.__name__ @@ -109,7 +113,7 @@ def apply(hugr: Hugr) -> PassResult: ) @property - def name(self) -> str: + def name(self) -> PassName: return f"Composed({ ', '.join(pass_.name for pass_ in self.passes) })" @@ -133,7 +137,7 @@ class PassResult: hugr: Hugr original_dirty: bool = False modified: bool = False - results: list[tuple[ComposablePass, Any]] = field(default_factory=list) + results: list[tuple[PassName, Any]] = field(default_factory=list) @classmethod def for_pass( @@ -157,7 +161,7 @@ def for_pass( hugr=hugr, original_dirty=inline and modified, modified=modified, - results=[(pass_, result)], + results=[(pass_.name, result)], ) def then(self, other: PassResult) -> PassResult: From 97e5406f58961bbd6c84ad09f638b3e767ed7044 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agust=C3=ADn=20Borgna?= Date: Thu, 20 Nov 2025 15:13:51 +0000 Subject: [PATCH 13/24] More tests --- hugr-py/tests/test_passes.py | 71 +++++++++++++++++++++++++++++------- 1 file changed, 57 insertions(+), 14 deletions(-) diff --git a/hugr-py/tests/test_passes.py b/hugr-py/tests/test_passes.py index d40d7c259d..2a934d63c1 100644 --- a/hugr-py/tests/test_passes.py +++ b/hugr-py/tests/test_passes.py @@ -1,3 +1,5 @@ +from copy import deepcopy + from hugr.hugr.base import Hugr from hugr.passes._composable_pass import ( ComposablePass, @@ -8,7 +10,7 @@ def test_composable_pass() -> None: - class MyDummyPass(ComposablePass): + class MyDummyInlinePass(ComposablePass): def run(self, hugr: Hugr, inplace: bool = True) -> PassResult: return impl_pass_run( hugr=hugr, @@ -18,24 +20,65 @@ def run(self, hugr: Hugr, inplace: bool = True) -> PassResult: hugr, result=None, inline=True, - modified=False, + # Say that we modified the HUGR even though we didn't + modified=True, + ), + ) + + class MyDummyCopyPass(ComposablePass): + def run(self, hugr: Hugr, inplace: bool = True) -> PassResult: + return impl_pass_run( + hugr=hugr, + inplace=inplace, + copy_call=lambda hugr: PassResult.for_pass( + self, + deepcopy(hugr), + result=None, + inline=False, + # Say that we modified the HUGR even though we didn't + modified=True, ), ) - dummy = MyDummyPass() + dummy_inline = MyDummyInlinePass() + dummy_copy = MyDummyCopyPass() - composed_dummies = dummy.then(dummy) + composed_dummies = dummy_inline.then(dummy_copy) + assert isinstance(composed_dummies, ComposedPass) - my_composed_pass = ComposedPass(dummy, dummy) - assert my_composed_pass.passes == [dummy, dummy] + assert dummy_inline.name == "MyDummyInlinePass" + assert dummy_copy.name == "MyDummyCopyPass" + assert composed_dummies.name == "Composed(MyDummyInlinePass, MyDummyCopyPass)" + assert composed_dummies.then(dummy_inline).then(composed_dummies).name == ( + "Composed(" + + "MyDummyInlinePass, MyDummyCopyPass, " + + "MyDummyInlinePass, " + + "MyDummyInlinePass, MyDummyCopyPass)" + ) - assert isinstance(composed_dummies, ComposablePass) - assert composed_dummies == my_composed_pass + # Apply the passes + hugr: Hugr = Hugr() + new_hugr = composed_dummies(hugr, inplace=False) + assert hugr == new_hugr + assert new_hugr is not hugr - assert dummy.name == "MyDummyPass" - assert composed_dummies.name == "Composed(MyDummyPass, MyDummyPass)" + # Verify the pass results + hugr = Hugr() + inplace_result = composed_dummies.run(hugr, inplace=True) + assert inplace_result.modified + assert inplace_result.original_dirty + assert inplace_result.results == [ + ("MyDummyInlinePass", None), + ("MyDummyCopyPass", None), + ] + assert inplace_result.hugr is hugr - assert ( - composed_dummies.then(my_composed_pass).name - == "Composed(MyDummyPass, MyDummyPass, MyDummyPass, MyDummyPass)" - ) + hugr = Hugr() + copy_result = composed_dummies.run(hugr, inplace=False) + assert copy_result.modified + assert not copy_result.original_dirty + assert copy_result.results == [ + ("MyDummyInlinePass", None), + ("MyDummyCopyPass", None), + ] + assert copy_result.hugr is not hugr From 07caa46988cbd57055570d26fee33b7848aafbc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agust=C3=ADn=20Borgna?= Date: Thu, 20 Nov 2025 15:24:54 +0000 Subject: [PATCH 14/24] Add pass name to error message --- hugr-py/src/hugr/passes/_composable_pass.py | 5 ++++- hugr-py/tests/test_passes.py | 21 +++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/hugr-py/src/hugr/passes/_composable_pass.py b/hugr-py/src/hugr/passes/_composable_pass.py index 76b915520b..b923c64e39 100644 --- a/hugr-py/src/hugr/passes/_composable_pass.py +++ b/hugr-py/src/hugr/passes/_composable_pass.py @@ -44,6 +44,7 @@ def then(self, other: ComposablePass) -> ComposablePass: def impl_pass_run( + pass_: ComposablePass, *, hugr: Hugr, inplace: bool, @@ -55,6 +56,7 @@ def impl_pass_run( At least one of the `inplace_call` or `copy_call` arguments must be provided. + :param pass_: The pass being run. Used for error messages. :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. @@ -79,7 +81,7 @@ def impl_pass_run( pass_result.original_dirty = False return pass_result else: - msg = "Pass must implement at least an inplace or copy run method" + msg = f"{pass_.name} needs to implement at least an inplace or copy run method" raise ValueError(msg) @@ -106,6 +108,7 @@ def apply(hugr: Hugr) -> PassResult: return pass_result return impl_pass_run( + self, hugr=hugr, inplace=inplace, inplace_call=apply, diff --git a/hugr-py/tests/test_passes.py b/hugr-py/tests/test_passes.py index 2a934d63c1..20b77dffb5 100644 --- a/hugr-py/tests/test_passes.py +++ b/hugr-py/tests/test_passes.py @@ -1,5 +1,7 @@ from copy import deepcopy +import pytest + from hugr.hugr.base import Hugr from hugr.passes._composable_pass import ( ComposablePass, @@ -13,6 +15,7 @@ def test_composable_pass() -> None: class MyDummyInlinePass(ComposablePass): def run(self, hugr: Hugr, inplace: bool = True) -> PassResult: return impl_pass_run( + self, hugr=hugr, inplace=inplace, inplace_call=lambda hugr: PassResult.for_pass( @@ -28,6 +31,7 @@ def run(self, hugr: Hugr, inplace: bool = True) -> PassResult: class MyDummyCopyPass(ComposablePass): def run(self, hugr: Hugr, inplace: bool = True) -> PassResult: return impl_pass_run( + self, hugr=hugr, inplace=inplace, copy_call=lambda hugr: PassResult.for_pass( @@ -82,3 +86,20 @@ def run(self, hugr: Hugr, inplace: bool = True) -> PassResult: ("MyDummyCopyPass", None), ] assert copy_result.hugr is not hugr + + +def test_invalid_composable_pass() -> None: + class MyDummyInvalidPass(ComposablePass): + def run(self, hugr: Hugr, inplace: bool = True) -> PassResult: + return impl_pass_run( + self, + hugr=hugr, + inplace=inplace, + ) + + dummy_invalid = MyDummyInvalidPass() + with pytest.raises( + ValueError, + match="MyDummyInvalidPass needs to implement at least an inplace or copy run method", # noqa: E501 + ): + dummy_invalid.run(Hugr()) From a1eebb04d11ba2ea44ba28aa5dbacc062256b379 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agust=C3=ADn=20Borgna?= <121866228+aborgna-q@users.noreply.github.com> Date: Mon, 24 Nov 2025 10:09:07 +0000 Subject: [PATCH 15/24] Update hugr-py/src/hugr/passes/_composable_pass.py Co-authored-by: Alan Lawrence --- hugr-py/src/hugr/passes/_composable_pass.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hugr-py/src/hugr/passes/_composable_pass.py b/hugr-py/src/hugr/passes/_composable_pass.py index b923c64e39..53e2db4e18 100644 --- a/hugr-py/src/hugr/passes/_composable_pass.py +++ b/hugr-py/src/hugr/passes/_composable_pass.py @@ -30,7 +30,7 @@ def __call__(self, hugr: Hugr, *, inplace: bool = True) -> Hugr: def run(self, hugr: Hugr, *, inplace: bool = True) -> PassResult: """Run the pass to transform a HUGR, returning a PassResult. - See :func:`_impl_pass_call` for a helper function to implement this method. + See :func:`_impl_pass_run` for a helper function to implement this method. """ @property From ad8ed712cdfb3181d99bf4ca281f9559aacb8158 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agust=C3=ADn=20Borgna?= Date: Mon, 24 Nov 2025 11:24:09 +0100 Subject: [PATCH 16/24] if tree --- hugr-py/src/hugr/passes/_composable_pass.py | 40 +++++++++++---------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/hugr-py/src/hugr/passes/_composable_pass.py b/hugr-py/src/hugr/passes/_composable_pass.py index 53e2db4e18..4937a7e46b 100644 --- a/hugr-py/src/hugr/passes/_composable_pass.py +++ b/hugr-py/src/hugr/passes/_composable_pass.py @@ -64,25 +64,27 @@ def impl_pass_run( :return: The result of the pass application. :raises ValueError: If neither `inplace_call` nor `copy_call` is provided. """ - if inplace and inplace_call is not None: - return inplace_call(hugr) - elif inplace and copy_call is not None: - pass_result = copy_call(hugr) - pass_result.hugr = hugr - if pass_result.modified: - hugr._overwrite_hugr(pass_result.hugr) - pass_result.original_dirty = True - return pass_result - 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) - pass_result = inplace_call(new_hugr) - pass_result.original_dirty = False - return pass_result - else: - msg = f"{pass_.name} needs to implement at least an inplace or copy run method" - raise ValueError(msg) + if inplace: + if inplace_call is not None: + return inplace_call(hugr) + elif copy_call is not None: + pass_result = copy_call(hugr) + pass_result.hugr = hugr + if pass_result.modified: + hugr._overwrite_hugr(pass_result.hugr) + pass_result.original_dirty = True + return pass_result + elif not inplace: + if copy_call is not None: + return copy_call(hugr) + elif inplace_call is not None: + new_hugr = deepcopy(hugr) + pass_result = inplace_call(new_hugr) + pass_result.original_dirty = False + return pass_result + + msg = f"{pass_.name} needs to implement at least an inplace or copy run method" + raise ValueError(msg) @dataclass From 44f5fa35bbb2fb629092db11cb1612a82e62ce16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agust=C3=ADn=20Borgna?= Date: Mon, 24 Nov 2025 11:25:12 +0100 Subject: [PATCH 17/24] Overrite inplace param --- hugr-py/src/hugr/passes/_composable_pass.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/hugr-py/src/hugr/passes/_composable_pass.py b/hugr-py/src/hugr/passes/_composable_pass.py index 4937a7e46b..c00a850e2c 100644 --- a/hugr-py/src/hugr/passes/_composable_pass.py +++ b/hugr-py/src/hugr/passes/_composable_pass.py @@ -102,7 +102,7 @@ def __init__(self, *passes: ComposablePass) -> None: self.passes.append(pass_) def run(self, hugr: Hugr, *, inplace: bool = True) -> PassResult: - def apply(hugr: Hugr) -> PassResult: + def apply(inplace: bool, hugr: Hugr) -> PassResult: pass_result = PassResult(hugr=hugr) for comp_pass in self.passes: new_result = comp_pass.run(pass_result.hugr, inplace=inplace) @@ -113,8 +113,8 @@ def apply(hugr: Hugr) -> PassResult: self, hugr=hugr, inplace=inplace, - inplace_call=apply, - copy_call=apply, + inplace_call=lambda hugr: apply(True, hugr), + copy_call=lambda hugr: apply(False, hugr), ) @property From 21ee55a4a67076f5e52ba3e76e5bbc2d8ec72560 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agust=C3=ADn=20Borgna?= Date: Mon, 24 Nov 2025 11:29:59 +0100 Subject: [PATCH 18/24] typo --- hugr-py/src/hugr/passes/_composable_pass.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hugr-py/src/hugr/passes/_composable_pass.py b/hugr-py/src/hugr/passes/_composable_pass.py index c00a850e2c..db35148fc6 100644 --- a/hugr-py/src/hugr/passes/_composable_pass.py +++ b/hugr-py/src/hugr/passes/_composable_pass.py @@ -30,7 +30,7 @@ def __call__(self, hugr: Hugr, *, inplace: bool = True) -> Hugr: def run(self, hugr: Hugr, *, inplace: bool = True) -> PassResult: """Run the pass to transform a HUGR, returning a PassResult. - See :func:`_impl_pass_run` for a helper function to implement this method. + See :func:`impl_pass_run` for a helper function to implement this method. """ @property From 8b9b59b75e9fad5b1c82f60bf838b67a7032e336 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agust=C3=ADn=20Borgna?= Date: Mon, 24 Nov 2025 11:39:58 +0100 Subject: [PATCH 19/24] post-merge cleanup --- hugr-py/src/hugr/passes/_composable_pass.py | 36 --------------------- 1 file changed, 36 deletions(-) diff --git a/hugr-py/src/hugr/passes/_composable_pass.py b/hugr-py/src/hugr/passes/_composable_pass.py index 65478eb8c6..db35148fc6 100644 --- a/hugr-py/src/hugr/passes/_composable_pass.py +++ b/hugr-py/src/hugr/passes/_composable_pass.py @@ -87,42 +87,6 @@ def impl_pass_run( raise ValueError(msg) -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.""" From 4c4cdcd4d3d7f215bf150ffef38870b2f6aaf60e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agust=C3=ADn=20Borgna?= Date: Mon, 24 Nov 2025 12:30:20 +0100 Subject: [PATCH 20/24] not my dummy --- hugr-py/tests/test_passes.py | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/hugr-py/tests/test_passes.py b/hugr-py/tests/test_passes.py index 20b77dffb5..5794ec83b7 100644 --- a/hugr-py/tests/test_passes.py +++ b/hugr-py/tests/test_passes.py @@ -12,7 +12,7 @@ def test_composable_pass() -> None: - class MyDummyInlinePass(ComposablePass): + class DummyInlinePass(ComposablePass): def run(self, hugr: Hugr, inplace: bool = True) -> PassResult: return impl_pass_run( self, @@ -28,7 +28,7 @@ def run(self, hugr: Hugr, inplace: bool = True) -> PassResult: ), ) - class MyDummyCopyPass(ComposablePass): + class DummyCopyPass(ComposablePass): def run(self, hugr: Hugr, inplace: bool = True) -> PassResult: return impl_pass_run( self, @@ -44,20 +44,20 @@ def run(self, hugr: Hugr, inplace: bool = True) -> PassResult: ), ) - dummy_inline = MyDummyInlinePass() - dummy_copy = MyDummyCopyPass() + dummy_inline = DummyInlinePass() + dummy_copy = DummyCopyPass() composed_dummies = dummy_inline.then(dummy_copy) assert isinstance(composed_dummies, ComposedPass) - assert dummy_inline.name == "MyDummyInlinePass" - assert dummy_copy.name == "MyDummyCopyPass" - assert composed_dummies.name == "Composed(MyDummyInlinePass, MyDummyCopyPass)" + assert dummy_inline.name == "DummyInlinePass" + assert dummy_copy.name == "DummyCopyPass" + assert composed_dummies.name == "Composed(DummyInlinePass, DummyCopyPass)" assert composed_dummies.then(dummy_inline).then(composed_dummies).name == ( "Composed(" - + "MyDummyInlinePass, MyDummyCopyPass, " - + "MyDummyInlinePass, " - + "MyDummyInlinePass, MyDummyCopyPass)" + + "DummyInlinePass, DummyCopyPass, " + + "DummyInlinePass, " + + "DummyInlinePass, DummyCopyPass)" ) # Apply the passes @@ -72,8 +72,8 @@ def run(self, hugr: Hugr, inplace: bool = True) -> PassResult: assert inplace_result.modified assert inplace_result.original_dirty assert inplace_result.results == [ - ("MyDummyInlinePass", None), - ("MyDummyCopyPass", None), + ("DummyInlinePass", None), + ("DummyCopyPass", None), ] assert inplace_result.hugr is hugr @@ -82,14 +82,14 @@ def run(self, hugr: Hugr, inplace: bool = True) -> PassResult: assert copy_result.modified assert not copy_result.original_dirty assert copy_result.results == [ - ("MyDummyInlinePass", None), - ("MyDummyCopyPass", None), + ("DummyInlinePass", None), + ("DummyCopyPass", None), ] assert copy_result.hugr is not hugr def test_invalid_composable_pass() -> None: - class MyDummyInvalidPass(ComposablePass): + class DummyInvalidPass(ComposablePass): def run(self, hugr: Hugr, inplace: bool = True) -> PassResult: return impl_pass_run( self, @@ -97,9 +97,9 @@ def run(self, hugr: Hugr, inplace: bool = True) -> PassResult: inplace=inplace, ) - dummy_invalid = MyDummyInvalidPass() + dummy_invalid = DummyInvalidPass() with pytest.raises( ValueError, - match="MyDummyInvalidPass needs to implement at least an inplace or copy run method", # noqa: E501 + match="DummyInvalidPass needs to implement at least an inplace or copy run method", # noqa: E501 ): dummy_invalid.run(Hugr()) From 1e64469c233f3f27df2b14757643f9482d66e8c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agust=C3=ADn=20Borgna?= Date: Mon, 24 Nov 2025 12:31:21 +0100 Subject: [PATCH 21/24] s/impl_pass_run/implement_pass_run/ --- hugr-py/src/hugr/passes/_composable_pass.py | 6 +++--- hugr-py/tests/test_passes.py | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/hugr-py/src/hugr/passes/_composable_pass.py b/hugr-py/src/hugr/passes/_composable_pass.py index db35148fc6..be82f513c6 100644 --- a/hugr-py/src/hugr/passes/_composable_pass.py +++ b/hugr-py/src/hugr/passes/_composable_pass.py @@ -30,7 +30,7 @@ def __call__(self, hugr: Hugr, *, inplace: bool = True) -> Hugr: def run(self, hugr: Hugr, *, inplace: bool = True) -> PassResult: """Run the pass to transform a HUGR, returning a PassResult. - See :func:`impl_pass_run` for a helper function to implement this method. + See :func:`implement_pass_run` for a helper function to implement this method. """ @property @@ -43,7 +43,7 @@ def then(self, other: ComposablePass) -> ComposablePass: return ComposedPass(self, other) -def impl_pass_run( +def implement_pass_run( pass_: ComposablePass, *, hugr: Hugr, @@ -109,7 +109,7 @@ def apply(inplace: bool, hugr: Hugr) -> PassResult: pass_result = pass_result.then(new_result) return pass_result - return impl_pass_run( + return implement_pass_run( self, hugr=hugr, inplace=inplace, diff --git a/hugr-py/tests/test_passes.py b/hugr-py/tests/test_passes.py index 5794ec83b7..402ffdc5ba 100644 --- a/hugr-py/tests/test_passes.py +++ b/hugr-py/tests/test_passes.py @@ -7,14 +7,14 @@ ComposablePass, ComposedPass, PassResult, - impl_pass_run, + implement_pass_run, ) def test_composable_pass() -> None: class DummyInlinePass(ComposablePass): def run(self, hugr: Hugr, inplace: bool = True) -> PassResult: - return impl_pass_run( + return implement_pass_run( self, hugr=hugr, inplace=inplace, @@ -30,7 +30,7 @@ def run(self, hugr: Hugr, inplace: bool = True) -> PassResult: class DummyCopyPass(ComposablePass): def run(self, hugr: Hugr, inplace: bool = True) -> PassResult: - return impl_pass_run( + return implement_pass_run( self, hugr=hugr, inplace=inplace, @@ -91,7 +91,7 @@ def run(self, hugr: Hugr, inplace: bool = True) -> PassResult: def test_invalid_composable_pass() -> None: class DummyInvalidPass(ComposablePass): def run(self, hugr: Hugr, inplace: bool = True) -> PassResult: - return impl_pass_run( + return implement_pass_run( self, hugr=hugr, inplace=inplace, From 1794edcf8a7cd89642c689a154096f39c18b4a29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agust=C3=ADn=20Borgna?= Date: Mon, 24 Nov 2025 12:34:00 +0100 Subject: [PATCH 22/24] s/pass_/composable_pass/ --- hugr-py/src/hugr/passes/_composable_pass.py | 26 ++++++++++++--------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/hugr-py/src/hugr/passes/_composable_pass.py b/hugr-py/src/hugr/passes/_composable_pass.py index be82f513c6..6e566a3724 100644 --- a/hugr-py/src/hugr/passes/_composable_pass.py +++ b/hugr-py/src/hugr/passes/_composable_pass.py @@ -44,7 +44,7 @@ def then(self, other: ComposablePass) -> ComposablePass: def implement_pass_run( - pass_: ComposablePass, + composable_pass: ComposablePass, *, hugr: Hugr, inplace: bool, @@ -56,7 +56,7 @@ def implement_pass_run( At least one of the `inplace_call` or `copy_call` arguments must be provided. - :param pass_: The pass being run. Used for error messages. + :param composable_pass: The pass being run. Used for error messages. :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. @@ -83,7 +83,10 @@ def implement_pass_run( pass_result.original_dirty = False return pass_result - msg = f"{pass_.name} needs to implement at least an inplace or copy run method" + msg = ( + f"{composable_pass.name} needs to implement at least " + + "an inplace or copy run method" + ) raise ValueError(msg) @@ -95,11 +98,11 @@ class ComposedPass(ComposablePass): def __init__(self, *passes: ComposablePass) -> None: self.passes = [] - for pass_ in passes: - if isinstance(pass_, ComposedPass): - self.passes.extend(pass_.passes) + for composable_pass in passes: + if isinstance(composable_pass, ComposedPass): + self.passes.extend(composable_pass.passes) else: - self.passes.append(pass_) + self.passes.append(composable_pass) def run(self, hugr: Hugr, *, inplace: bool = True) -> PassResult: def apply(inplace: bool, hugr: Hugr) -> PassResult: @@ -119,7 +122,8 @@ def apply(inplace: bool, hugr: Hugr) -> PassResult: @property def name(self) -> PassName: - return f"Composed({ ', '.join(pass_.name for pass_ in self.passes) })" + names = [composable_pass.name for composable_pass in self.passes] + return f"Composed({ ', '.join(names) })" @dataclass @@ -147,7 +151,7 @@ class PassResult: @classmethod def for_pass( cls, - pass_: ComposablePass, + composable_pass: ComposablePass, hugr: Hugr, *, result: Any, @@ -157,7 +161,7 @@ def for_pass( """Create a new PassResult after a pass application. :param hugr: The Hugr that was transformed. - :param pass_: The pass that was applied. + :param composable_pass: The pass that was applied. :param result: The result of the pass application. :param inline: Whether the pass was applied inplace. :param modified: Whether the pass modified the HUGR. @@ -166,7 +170,7 @@ def for_pass( hugr=hugr, original_dirty=inline and modified, modified=modified, - results=[(pass_.name, result)], + results=[(composable_pass.name, result)], ) def then(self, other: PassResult) -> PassResult: From 19662f54b788e9955115244e6fb8ed016f4e1b75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agust=C3=ADn=20Borgna?= Date: Mon, 24 Nov 2025 12:38:03 +0100 Subject: [PATCH 23/24] typos --- 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 6e566a3724..f72f21d0f0 100644 --- a/hugr-py/src/hugr/passes/_composable_pass.py +++ b/hugr-py/src/hugr/passes/_composable_pass.py @@ -52,7 +52,7 @@ def implement_pass_run( copy_call: Callable[[Hugr], PassResult] | None = None, ) -> PassResult: """Helper function to implement a ComposablePass.run method, given an - inplace or copy-returning pass methods. + inplace or copy-returning pass method. At least one of the `inplace_call` or `copy_call` arguments must be provided. @@ -139,7 +139,7 @@ class PassResult: :attr hugr: The transformed Hugr. :attr original_dirty: Whether the original HUGR was modified by the pass. :attr modified: Whether the pass made changes to the HUGR. - :attr results: The result of each applied pass, as a tuple of the pass and + :attr results: The result of each applied pass, as a tuple of the pass name and the result. """ From 7cf550d71dd45ae4a381f1dd56ddd91c16d82f46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agust=C3=ADn=20Borgna?= Date: Mon, 24 Nov 2025 12:46:43 +0100 Subject: [PATCH 24/24] Replace `original_dirty` with `inplace` flag in results, explain flag guarantees --- hugr-py/src/hugr/passes/_composable_pass.py | 29 +++++++++++---------- hugr-py/tests/test_passes.py | 8 +++--- 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/hugr-py/src/hugr/passes/_composable_pass.py b/hugr-py/src/hugr/passes/_composable_pass.py index f72f21d0f0..68d935f3bf 100644 --- a/hugr-py/src/hugr/passes/_composable_pass.py +++ b/hugr-py/src/hugr/passes/_composable_pass.py @@ -72,7 +72,7 @@ def implement_pass_run( pass_result.hugr = hugr if pass_result.modified: hugr._overwrite_hugr(pass_result.hugr) - pass_result.original_dirty = True + pass_result.inplace = True return pass_result elif not inplace: if copy_call is not None: @@ -80,7 +80,7 @@ def implement_pass_run( elif inplace_call is not None: new_hugr = deepcopy(hugr) pass_result = inplace_call(new_hugr) - pass_result.original_dirty = False + pass_result.inplace = False return pass_result msg = ( @@ -106,7 +106,7 @@ def __init__(self, *passes: ComposablePass) -> None: def run(self, hugr: Hugr, *, inplace: bool = True) -> PassResult: def apply(inplace: bool, hugr: Hugr) -> PassResult: - pass_result = PassResult(hugr=hugr) + pass_result = PassResult(hugr=hugr, inplace=inplace) for comp_pass in self.passes: new_result = comp_pass.run(pass_result.hugr, inplace=inplace) pass_result = pass_result.then(new_result) @@ -133,18 +133,19 @@ class PassResult: Includes a flag indicating whether the passes modified the HUGR, and an arbitrary result object for each pass. - In some cases, `modified` may be set to `True` even if the pass did not - modify the program. - :attr hugr: The transformed Hugr. - :attr original_dirty: Whether the original HUGR was modified by the pass. + :attr inplace: Whether the pass was applied inplace. + If this is `True`, `hugr` will be the same object passed as input. + If this is `False`, `hugr` will be an independent copy of the original Hugr. :attr modified: Whether the pass made changes to the HUGR. - :attr results: The result of each applied pass, as a tuple of the pass name and - the result. + If `False`, `hugr` will have the same contents as the original Hugr. + If `True`, no guarantees are made about the contents of `hugr`. + :attr results: The result of each applied pass, as a tuple of the pass name + and the result. """ hugr: Hugr - original_dirty: bool = False + inplace: bool = False modified: bool = False results: list[tuple[PassName, Any]] = field(default_factory=list) @@ -155,7 +156,7 @@ def for_pass( hugr: Hugr, *, result: Any, - inline: bool, + inplace: bool, modified: bool = True, ) -> PassResult: """Create a new PassResult after a pass application. @@ -163,12 +164,12 @@ def for_pass( :param hugr: The Hugr that was transformed. :param composable_pass: The pass that was applied. :param result: The result of the pass application. - :param inline: Whether the pass was applied inplace. + :param inplace: Whether the pass was applied inplace. :param modified: Whether the pass modified the HUGR. """ return cls( hugr=hugr, - original_dirty=inline and modified, + inplace=inplace, modified=modified, results=[(composable_pass.name, result)], ) @@ -180,7 +181,7 @@ def then(self, other: PassResult) -> PassResult: """ return PassResult( hugr=other.hugr, - original_dirty=self.original_dirty or other.original_dirty, + inplace=self.inplace and other.inplace, modified=self.modified or other.modified, results=self.results + other.results, ) diff --git a/hugr-py/tests/test_passes.py b/hugr-py/tests/test_passes.py index 402ffdc5ba..d7e517c0b1 100644 --- a/hugr-py/tests/test_passes.py +++ b/hugr-py/tests/test_passes.py @@ -22,7 +22,7 @@ def run(self, hugr: Hugr, inplace: bool = True) -> PassResult: self, hugr, result=None, - inline=True, + inplace=True, # Say that we modified the HUGR even though we didn't modified=True, ), @@ -38,7 +38,7 @@ def run(self, hugr: Hugr, inplace: bool = True) -> PassResult: self, deepcopy(hugr), result=None, - inline=False, + inplace=False, # Say that we modified the HUGR even though we didn't modified=True, ), @@ -70,7 +70,7 @@ def run(self, hugr: Hugr, inplace: bool = True) -> PassResult: hugr = Hugr() inplace_result = composed_dummies.run(hugr, inplace=True) assert inplace_result.modified - assert inplace_result.original_dirty + assert inplace_result.inplace assert inplace_result.results == [ ("DummyInlinePass", None), ("DummyCopyPass", None), @@ -80,7 +80,7 @@ def run(self, hugr: Hugr, inplace: bool = True) -> PassResult: hugr = Hugr() copy_result = composed_dummies.run(hugr, inplace=False) assert copy_result.modified - assert not copy_result.original_dirty + assert not copy_result.inplace assert copy_result.results == [ ("DummyInlinePass", None), ("DummyCopyPass", None),