Repository: amaranth-lang/amaranth
Pull request #1323 (2024-04-11 18:36:05)

hdl._nir, back.rtlil: use `Format.*` to emit enum attributes and wires for fields.

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/amaranth-lang/amaranth/pull/1323?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=amaranth-lang) Report
Attention: Patch coverage is `93.24324%` with `5 lines` in your changes are missing coverage. Please review.
> Project coverage is 90.43%. Comparing base [(`b36e7e0`)](https://app.codecov.io/gh/amaranth-lang/amaranth/commit/b36e7e088010c601cbbb037d25927d939d6da600?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=amaranth-lang) to head [(`1444a9e`)](https://app.codecov.io/gh/amaranth-lang/amaranth/pull/1323?dropdown=coverage&src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=amaranth-lang).
> Report is 12 commits behind head on main.

| [Files](https://app.codecov.io/gh/amaranth-lang/amaranth/pull/1323?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=amaranth-lang) | Patch % | Lines |
|---|---|---|
| [amaranth/hdl/\_ir.py](https://app.codecov.io/gh/amaranth-lang/amaranth/pull/1323?src=pr&el=tree&filepath=amaranth%2Fhdl%2F_ir.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=amaranth-lang#diff-YW1hcmFudGgvaGRsL19pci5weQ==) | 83.87% | [2 Missing and 3 partials :warning: ](https://app.codecov.io/gh/amaranth-lang/amaranth/pull/1323?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=amaranth-lang) |

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


```diff
@@            Coverage Diff             @@
##             main    #1323      +/-   ##
==========================================
+ Coverage   89.94%   90.43%   +0.49%     
==========================================
  Files          44       44              
  Lines       10647    10771     +124     
  Branches     2595     2337     -258     
==========================================
+ Hits         9576     9741     +165     
+ Misses        882      846      -36     
+ Partials      189      184       -5     
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/amaranth-lang/amaranth/pull/1323?dropdown=coverage&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).


Patch:
diff --git a/amaranth/back/rtlil.py b/amaranth/back/rtlil.py
index b70fd39..a5e5a8b 100644
--- a/amaranth/back/rtlil.py
+++ b/amaranth/back/rtlil.py
@@ -3,6 +3,7 @@ from contextlib import contextmanager
 import io
 
 from ..utils import bits_for
+from .._utils import to_binary
 from ..lib import wiring
 from ..hdl import _repr, _ast, _ir, _nir
 
@@ -421,6 +422,7 @@ class ModuleEmitter:
         self.emit_cell_wires()
         self.emit_submodule_wires()
         self.emit_connects()
+        self.emit_signal_fields()
         self.emit_submodules()
         self.emit_cells()
 
@@ -491,12 +493,12 @@ class ModuleEmitter:
             attrs.update(signal.attrs)
             self.value_src_loc[value] = signal.src_loc
 
-            for repr in signal._value_repr:
-                if repr.path == () and isinstance(repr.format, _repr.FormatEnum):
-                    enum = repr.format.enum
-                    attrs["enum_base_type"] = enum.__name__
-                    for enum_value in enum:
-                        attrs["enum_value_{:0{}b}".format(enum_value.value, len(signal))] = enum_value.name
+            field = self.netlist.signal_fields[signal][()]
+            if field.enum_name is not None:
+                attrs["enum_base_type"] = field.enum_name
+            if field.enum_variants is not None:
+                for var_val, var_name in field.enum_variants.items():
+                    attrs["enum_value_" + to_binary(var_val, len(signal))] = var_name
 
             if name in self.module.ports:
                 port_value, _flow = self.module.ports[name]
@@ -666,6 +668,30 @@ class ModuleEmitter:
             if name not in self.driven_sigports:
                 self.builder.connect(wire.name, self.sigspec(value))
 
+    def emit_signal_fields(self):
+        for signal, name in self.module.signal_names.items():
+            fields = self.netlist.signal_fields[signal]
+            for path, field in fields.items():
+                if path == ():
+                    continue
+                name_parts = [name]
+                for component in path:
+                    if isinstance(component, str):
+                        name_parts.append(f".{component}")
+                    elif isinstance(component, int):
+                        name_parts.append(f"[{component}]")
+                    else:
+                        assert False # :nocov:
+                attrs = {}
+                if field.enum_name is not None:
+                    attrs["enum_base_type"] = field.enum_name
+                if field.enum_variants is not None:
+                    for var_val, var_name in field.enum_variants.items():
+                        attrs["enum_value_" + to_binary(var_val, len(field.value))] = var_name
+                wire = self.builder.wire(width=len(field.value), signed=field.signed, attrs=attrs,
+                                         name="".join(name_parts), src_loc=signal.src_loc)
+                self.builder.connect(wire.name, self.sigspec(field.value))
+
     def emit_submodules(self):
         for submodule_idx in self.module.submodules:
             submodule = self.netlist.modules[submodule_idx]
diff --git a/amaranth/hdl/_ast.py b/amaranth/hdl/_ast.py
index 86abc42..81bce00 100644
--- a/amaranth/hdl/_ast.py
+++ b/amaranth/hdl/_ast.py
@@ -2095,6 +2095,11 @@ class Signal(Value, DUID, metaclass=_SignalMeta):
 
         self._attrs = OrderedDict(() if attrs is None else attrs)
 
+        if isinstance(orig_shape, ShapeCastable):
+            self._format = orig_shape.format(orig_shape(self), "")
+        else:
+            self._format = Format("{}", self)
+
         if decoder is not None:
             # The value representation is specified explicitly. Since we do not expose `hdl._repr`,
             # this is the only way to add a custom filter to the signal right now.
diff --git a/amaranth/hdl/_ir.py b/amaranth/hdl/_ir.py
index e1696b1..cc77e4a 100644
--- a/amaranth/hdl/_ir.py
+++ b/amaranth/hdl/_ir.py
@@ -690,7 +690,7 @@ class NetlistDriver:
 
 
 class NetlistEmitter:
-    def __init__(self, netlist: _nir.Netlist, design, *, all_undef_to_ff=False):
+    def __init__(self, netlist: _nir.Netlist, design: Design, *, all_undef_to_ff=False):
         self.netlist = netlist
         self.design = design
         self.all_undef_to_ff = all_undef_to_ff
@@ -776,7 +776,7 @@ class NetlistEmitter:
     def emit_operator(self, module_idx: int, operator: str, *inputs: _nir.Value, src_loc):
         op = _nir.Operator(module_idx, operator=operator, inputs=inputs, src_loc=src_loc)
         return self.netlist.add_value_cell(op.width, op)
-    
+
     def emit_matches(self, module_idx: int, value: _nir.Value, patterns, *, src_loc):
         key = module_idx, value, patterns, src_loc
         try:
@@ -1334,6 +1334,42 @@ class NetlistEmitter:
             else:
                 raise ValueError(f"Invalid port direction {dir!r}")
 
+    def emit_signal_fields(self):
+        for signal, fragment in self.design.signal_lca.items():
+            module_idx = self.fragment_module_idx[fragment]
+            fields = {}
+            def emit_format(path, fmt):
+                if isinstance(fmt, _ast.Format):
+                    specs = [
+                        chunk[0]
+                        for chunk in fmt._chunks
+                        if not isinstance(chunk, str)
+                    ]
+                    if len(specs) != 1:
+                        return
+                    val, signed = self.emit_rhs(module_idx, specs[0])
+                    fields[path] = _nir.SignalField(val, signed=signed)
+                elif isinstance(fmt, _ast.Format.Enum):
+                    val, signed = self.emit_rhs(module_idx, fmt._value)
+                    fields[path] = _nir.SignalField(val, signed=signed,
+                                                    enum_name=fmt._name,
+                                                    enum_variants=fmt._variants)
+                elif isinstance(fmt, _ast.Format.Struct):
+                    val, signed = self.emit_rhs(module_idx, fmt._value)
+                    fields[path] = _nir.SignalField(val, signed=signed)
+                    for name, subfmt in fmt._fields.items():
+                        emit_format(path + (name,), subfmt)
+                elif isinstance(fmt, _ast.Format.Array):
+                    val, signed = self.emit_rhs(module_idx, fmt._value)
+                    fields[path] = _nir.SignalField(val, signed=signed)
+                    for idx, subfmt in enumerate(fmt._fields):
+                        emit_format(path + (idx,), subfmt)
+            emit_format((), signal._format)
+            val, signed = self.emit_rhs(module_idx, signal)
+            if () not in fields or fields[()].value != val:
+                fields[()] = _nir.SignalField(val, signed=signed)
+            self.netlist.signal_fields[signal] = fields
+
     def emit_drivers(self):
         for driver in self.drivers.values():
             if (driver.domain is not None and
@@ -1452,6 +1488,7 @@ class NetlistEmitter:
             for subfragment, _name, sub_src_loc in fragment.subfragments:
                 self.emit_fragment(subfragment, module_idx, cell_src_loc=sub_src_loc)
             if parent_module_idx is None:
+                self.emit_signal_fields()
                 self.emit_drivers()
                 self.emit_top_ports(fragment)
                 if self.all_undef_to_ff:
diff --git a/amaranth/hdl/_nir.py b/amaranth/hdl/_nir.py
index 0ee5b1d..595e31f 100644
--- a/amaranth/hdl/_nir.py
+++ b/amaranth/hdl/_nir.py
@@ -289,6 +289,22 @@ class Format:
                 chunk.value = netlist.resolve_value(chunk.value)
 
 
+class SignalField:
+    """Describes a single field of a signal."""
+    def __init__(self, value, *, signed, enum_name=None, enum_variants=None):
+        self.value = Value(value)
+        self.signed = bool(signed)
+        self.enum_name = enum_name
+        self.enum_variants = enum_variants
+
+    def __eq__(self, other):
+        return (type(self) is type(other) and
+            self.value == other.value and
+            self.signed == other.signed and
+            self.enum_name == other.enum_name and
+            self.enum_variants == other.enum_variants)
+
+
 class Netlist:
     """A fine netlist. Consists of:
 
@@ -321,6 +337,7 @@ class Netlist:
     connections : dict of (negative) int to int
     io_ports : list of ``IOPort``
     signals : dict of Signal to ``Value``
+    signal_fields: dict of Signal to dict of tuple[str | int] to SignalField
     last_late_net: int
     """
     def __init__(self):
@@ -329,6 +346,7 @@ class Netlist:
         self.connections: dict[Net, Net] = {}
         self.io_ports: list[_ast.IOPort] = []
         self.signals = SignalDict()
+        self.signal_fields = SignalDict()
         self.last_late_net = 0
 
     def resolve_net(self, net: Net):
@@ -345,6 +363,9 @@ class Netlist:
             cell.resolve_nets(self)
         for sig in self.signals:
             self.signals[sig] = self.resolve_value(self.signals[sig])
+        for fields in self.signal_fields.values():
+            for field in fields.values():
+                field.value = self.resolve_value(field.value)
 
     def __repr__(self):
         result = ["("]


Test patch:
diff --git a/tests/test_back_rtlil.py b/tests/test_back_rtlil.py
index f168b7e..96ac2ee 100644
--- a/tests/test_back_rtlil.py
+++ b/tests/test_back_rtlil.py
@@ -4,7 +4,7 @@ import re
 from amaranth.back import rtlil
 from amaranth.hdl import *
 from amaranth.hdl._ast import *
-from amaranth.lib import memory, wiring
+from amaranth.lib import memory, wiring, data, enum
 
 from .utils import *
 
@@ -2010,6 +2010,67 @@ class PrintTestCase(RTLILTestCase):
         """)
 
 
+class DetailTestCase(RTLILTestCase):
+    def test_enum(self):
+        class MyEnum(enum.Enum, shape=unsigned(2)):
+            A = 0
+            B = 1
+            C = 2
+
+        sig = Signal(MyEnum)
+        m = Module()
+        m.d.comb += sig.eq(MyEnum.A)
+        self.assertRTLIL(m, [sig.as_value()], R"""
+        attribute \generator "Amaranth"
+        attribute \top 1
+        module \top
+            attribute \enum_base_type "MyEnum"
+            attribute \enum_value_00 "A"
+            attribute \enum_value_01 "B"
+            attribute \enum_value_10 "C"
+            wire width 2 output 0 \sig
+            connect \sig 2'00
+        end
+        """)
+
+    def test_struct(self):
+        class MyEnum(enum.Enum, shape=unsigned(2)):
+            A = 0
+            B = 1
+            C = 2
+
+        class Meow(data.Struct):
+            a: MyEnum
+            b: 3
+            c: signed(4)
+            d: data.ArrayLayout(2, 2)
+
+        sig = Signal(Meow)
+        m = Module()
+        self.assertRTLIL(m, [sig.as_value()], R"""
+        attribute \generator "Amaranth"
+        attribute \top 1
+        module \top
+            wire width 13 input 0 \sig
+            attribute \enum_base_type "MyEnum"
+            attribute \enum_value_00 "A"
+            attribute \enum_value_01 "B"
+            attribute \enum_value_10 "C"
+            wire width 2 \sig.a
+            wire width 3 \sig.b
+            wire width 4 signed \sig.c
+            wire width 4 \sig.d
+            wire width 2 \sig.d[0]
+            wire width 2 \sig.d[1]
+            connect \sig.a \sig [1:0]
+            connect \sig.b \sig [4:2]
+            connect \sig.c \sig [8:5]
+            connect \sig.d \sig [12:9]
+            connect \sig.d[0] \sig [10:9]
+            connect \sig.d[1] \sig [12:11]
+        end
+        """)
+
 class ComponentTestCase(RTLILTestCase):
     def test_component(self):
         class MyComponent(wiring.Component):
diff --git a/tests/test_hdl_ir.py b/tests/test_hdl_ir.py
index 6cb3071..4795551 100644
--- a/tests/test_hdl_ir.py
+++ b/tests/test_hdl_ir.py
@@ -7,6 +7,9 @@ from amaranth.hdl._cd import *
 from amaranth.hdl._dsl import *
 from amaranth.hdl._ir import *
 from amaranth.hdl._mem import *
+from amaranth.hdl._nir import SignalField
+
+from amaranth.lib import enum, data
 
 from .utils import *
 
@@ -3501,3 +3504,41 @@ class UndrivenTestCase(FHDLTestCase):
             (cell 3 0 (flipflop 3.0:5 10 pos 0 0))
         )
         """)
+
+
+class FieldsTestCase(FHDLTestCase):
+    def test_fields(self):
+        class MyEnum(enum.Enum, shape=unsigned(2)):
+            A = 0
+            B = 1
+            C = 2
+        l = data.StructLayout({"a": MyEnum, "b": signed(3)})
+        s1 = Signal(l)
+        s2 = Signal(MyEnum)
+        s3 = Signal(signed(3))
+        s4 = Signal(unsigned(4))
+        nl = build_netlist(Fragment.get(Module(), None), [
+            s1.as_value(), s2.as_value(), s3, s4,
+        ])
+        self.assertEqual(nl.signal_fields[s1.as_value()], {
+            (): SignalField(nl.signals[s1.as_value()], signed=False),
+            ('a',): SignalField(nl.signals[s1.as_value()][0:2], signed=False, enum_name="MyEnum", enum_variants={
+                0: "A",
+                1: "B",
+                2: "C",
+            }),
+            ('b',): SignalField(nl.signals[s1.as_value()][2:5], signed=True)
+        })
+        self.assertEqual(nl.signal_fields[s2.as_value()], {
+            (): SignalField(nl.signals[s2.as_value()], signed=False, enum_name="MyEnum", enum_variants={
+                0: "A",
+                1: "B",
+                2: "C",
+            }),
+        })
+        self.assertEqual(nl.signal_fields[s3], {
+            (): SignalField(nl.signals[s3], signed=True),
+        })
+        self.assertEqual(nl.signal_fields[s4], {
+            (): SignalField(nl.signals[s4], signed=False),
+        })
diff --git a/tests/test_lib_data.py b/tests/test_lib_data.py
index 3384935..b125803 100644
--- a/tests/test_lib_data.py
+++ b/tests/test_lib_data.py
@@ -647,6 +647,9 @@ class ViewTestCase(FHDLTestCase):
             def from_bits(self, bits):
                 return bits
 
+            def format(self, value, spec):
+                return Format("")
+
         v = Signal(data.StructLayout({
             "f": WrongCastable()
         }))