Repository: amaranth-lang/amaranth
Pull request #1129 (2024-02-13 15:12:28)

Remember origins of a fragment during elaboration
This isn't expected to result in a significant increase in memory use, so for now it's enabled by default. Elaboration chains where it is not desired to preserve origins can delete the `_origins` attribute from the fragment and nothing will be stored.

The interface `Fragment._origins` remains private, as is the rest of the `Fragment` interface (including itself), but it enables certain codebases that currently use a much more invasive technique to rely on reading a single private field.

If this interface proves to be sufficient and useful, it will graduate in a largely unmodified form into a stable interface on `Fragment`'s successor.

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/amaranth-lang/amaranth/pull/1129?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=amaranth-lang) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Comparison is base [(`4a8dd80`)](https://app.codecov.io/gh/amaranth-lang/amaranth/commit/4a8dd808c30820fbc53fe919f04fd2b96b62cfd9?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=amaranth-lang) 85.47% compared to head [(`48187d4`)](https://app.codecov.io/gh/amaranth-lang/amaranth/pull/1129?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=amaranth-lang) 85.07%.


<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main    #1129      +/-   ##
==========================================
- Coverage   85.47%   85.07%   -0.40%     
==========================================
  Files          42       42              
  Lines        8358     8351       -7     
  Branches     1973     1735     -238     
==========================================
- Hits         7144     7105      -39     
- Misses       1018     1050      +32     
  Partials      196      196              
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/amaranth-lang/amaranth/pull/1129?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=amaranth-lang).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=amaranth-lang).

anuejn: I really appreciate this, we also have [a monkey patched hack](https://github.com/apertus-open-source-cinema/naps/blob/main/naps/soc/tracing_elaborate.py) to have this functionality downstream. Happy to see this 🎉
whitequark: @anuejn Good to know! Waiting on @tilk or @lekcyjna123 to give feedback before merging this now.
tilk: A heads-up: I'm currently away and the soonest date I will be able to look at this is Monday. 
lekcyjna123: Hi,
I have checked the code and did some small experiments and in my opinion it will fit very well in our tracing code. Thanks!

Patch:
diff --git a/amaranth/hdl/_ir.py b/amaranth/hdl/_ir.py
index 968b372..c491725 100644
--- a/amaranth/hdl/_ir.py
+++ b/amaranth/hdl/_ir.py
@@ -31,8 +31,11 @@ class Fragment:
     @staticmethod
     def get(obj, platform):
         code = None
+        origins = []
         while True:
             if isinstance(obj, Fragment):
+                if hasattr(obj, "origins"):
+                    obj.origins = tuple(origins)
                 return obj
             elif isinstance(obj, Elaboratable):
                 code = obj.elaborate.__code__
@@ -58,6 +61,7 @@ class Fragment:
                     category=UserWarning,
                     filename=code.co_filename,
                     lineno=code.co_firstlineno)
+            origins.append(obj)
             obj = new_obj
 
     def __init__(self, *, src_loc=None):
@@ -70,6 +74,7 @@ class Fragment:
         self.generated = OrderedDict()
         self.flatten = False
         self.src_loc = src_loc
+        self.origins = None
 
     def add_ports(self, *ports, dir):
         assert dir in ("i", "o", "io")


Test patch:
diff --git a/tests/test_hdl_ir.py b/tests/test_hdl_ir.py
index d38c197..2e9dabe 100644
--- a/tests/test_hdl_ir.py
+++ b/tests/test_hdl_ir.py
@@ -4,6 +4,7 @@ from collections import OrderedDict
 
 from amaranth.hdl._ast import *
 from amaranth.hdl._cd import *
+from amaranth.hdl._dsl import *
 from amaranth.hdl._ir import *
 from amaranth.hdl._mem import *
 
@@ -919,3 +920,29 @@ class InstanceTestCase(FHDLTestCase):
             f: ("top",),
             a_f: ("top", "a$U$0")
         })
+
+
+class ElaboratesTo(Elaboratable):
+    def __init__(self, lower):
+        self.lower = lower
+
+    def elaborate(self, platform):
+        return self.lower
+
+
+class OriginsTestCase(FHDLTestCase):
+    def test_origins(self):
+        elab1 = ElaboratesTo(elab2 := ElaboratesTo(m := Module()))
+        frag = Fragment.get(elab1, platform=None)
+        self.assertEqual(len(frag.origins), 3)
+        self.assertIsInstance(frag.origins, tuple)
+        self.assertIs(frag.origins[0], elab1)
+        self.assertIs(frag.origins[1], elab2)
+        self.assertIs(frag.origins[2], 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"))
\ No newline at end of file