Repository: amaranth-lang/amaranth
Pull request #1593 (2025-05-11 18:44:26)

hdl._ir: chain Fragment.origins with preexisting
This PR changes `Fragment.origins` from being overwritten if during `Fragment.get` a `Fragment` is encountered that already has a `Fragment.origins` field to
appending to the preexisting `origins` tuple. This case is interesting for anything using a `FragmentTransformer`. With this change,
```python
from amaranth import *

for transformer in (DomainRenamer("sync"), ResetInserter(Signal()), EnableInserter(Signal())):
    origins = Fragment.get(transformer(inst := Module()), None).origins
    assert inst in origins
```
no longer asserts, like it does currently.

Some before after examples:

Before:
```python
>>> Fragment.get(DomainRenamer("sync")(Module()), None).origins
(<amaranth.hdl._xfrm.TransformedElaboratable object at 0x7fa86fee1d10>,)
>>> Fragment.get(ResetInserter(Signal())(DomainRenamer("sync")(Module())), None).origins
(<amaranth.hdl._xfrm.TransformedElaboratable object at 0x7fa86fee3310>,)
```
After:
```python
>>> Fragment.get(DomainRenamer("sync")(Module()), None).origins
(<amaranth.hdl._xfrm.TransformedElaboratable object at 0x7f701f7266d0>, <amaranth.hdl._dsl.Module object at 0x7f70200a6690>)
>>> Fragment.get(ResetInserter(Signal())(DomainRenamer("sync")(Module())), None).origins
(<amaranth.hdl._xfrm.TransformedElaboratable object at 0x7f701f425a50>, <amaranth.hdl._dsl.Module object at 0x7f701f8f9250>)
```

Furthermore, with this change, the duplicate fragment check performed by `Fragment.prepare()` now also works if the duplicate is behind something that uses a fragment transformer, for example
```python
from amaranth import *

m = Module()
m.submodules.a1 = a1 = DomainRenamer("sync")(inst := Module())
m.submodules.a2 = a2 = inst

Fragment.get(m, None).prepare()
```
previously did not raise any exception, but now (correctly) raises:
```
amaranth.hdl._ir.DuplicateElaboratable: Elaboratable <amaranth.hdl._dsl.Module object at 0x7fabfb3872d0> is included twice in the hierarchy, as top.a1 and top.a2
```



Patch:
diff --git a/amaranth/hdl/_ir.py b/amaranth/hdl/_ir.py
index 1370bde..e49a82b 100644
--- a/amaranth/hdl/_ir.py
+++ b/amaranth/hdl/_ir.py
@@ -54,7 +54,7 @@ class Fragment:
         while True:
             if isinstance(obj, Fragment):
                 if hasattr(obj, "origins"):
-                    obj.origins = tuple(origins)
+                    obj.origins = tuple(origins) + (obj.origins or ())
                 return obj
             elif isinstance(obj, Elaboratable):
                 code = obj.elaborate.__code__
diff --git a/amaranth/lib/wiring.py b/amaranth/lib/wiring.py
index f3d1750..b2a7585 100644
--- a/amaranth/lib/wiring.py
+++ b/amaranth/lib/wiring.py
@@ -1277,6 +1277,9 @@ class FlippedInterface:
         """
         return type(self) is type(other) and self.__unflipped == other.__unflipped
 
+    def __hash__(self):
+        return hash(self.__unflipped)
+
     # See the note in `FlippedSignature`. In addition, these accessors also handle flipping of
     # an interface member.
 


Test patch:
diff --git a/tests/test_hdl_ir.py b/tests/test_hdl_ir.py
index c017f38..78bfcd2 100644
--- a/tests/test_hdl_ir.py
+++ b/tests/test_hdl_ir.py
@@ -968,12 +968,31 @@ class OriginsTestCase(FHDLTestCase):
         self.assertIs(frag.origins[1], elab2)
         self.assertIs(frag.origins[2], m)
 
+    def test_origins_transformed_elaboratable(self):
+        renamed = DomainRenamer("sync")(elab1 := ElaboratesTo(elab2 := ElaboratesTo(m := Module())))
+        frag = Fragment.get(renamed, platform=None)
+        self.assertEqual(len(frag.origins), 4)
+        self.assertIsInstance(frag.origins, tuple)
+        self.assertIs(frag.origins[0], renamed)
+        self.assertIs(frag.origins[1], elab1)
+        self.assertIs(frag.origins[2], elab2)
+        self.assertIs(frag.origins[3], m)
+
+        renamed_nested = ElaboratesTo(elab2 := ElaboratesTo(renamed := DomainRenamer("sync")(m := Module())))
+        frag = Fragment.get(renamed_nested, platform=None)
+        self.assertEqual(len(frag.origins), 4)
+        self.assertIsInstance(frag.origins, tuple)
+        self.assertIs(frag.origins[0], renamed_nested)
+        self.assertIs(frag.origins[1], elab2)
+        self.assertIs(frag.origins[2], renamed)
+        self.assertIs(frag.origins[3], m)
+
     def test_origins_disable(self):
         inst = Instance("test")
         del inst.origins
         elab = ElaboratesTo(inst)
         frag = Fragment.get(elab, platform=None)
-        self.assertFalse(hasattr(frag, "_origins"))
+        self.assertFalse(hasattr(frag, "origins"))
 
 
 class IOBufferTestCase(FHDLTestCase):