Repository: amaranth-lang/amaranth (python)

Issue:
Infer Verilog ports on Interface objects
Consider the following `Component`, which is also an Interface Object:

```
from amaranth import Signal, Module
from amaranth.lib.wiring import Signature, In, Out, Component


class SeqSignature(Signature):
    def __init__(self):
        return super().__init__({
            "data": In(8),
            "addr": Out(10),
            "ready": Out(1),
            "valid": In(1)
        })


class RandSignature(Signature):
    def __init__(self):
        return super().__init__({
            "data": In(8),
            "addr": Out(14),
            "read_en": Out(1),
            "flush": Out(1),
            "valid": In(1)
        })


class PageBuffer(Component):
    rand: In(RandSignature().flip())
    seq: Out(SeqSignature())

    def elaborate(self, plat):
        m = Module()

        return m
```

Right now, if you want to convert such a module to Verilog, you must list all the I/O ports in `verilog.convert`:

```
m = PageBuffer()
ios = [m.rand.data, m.rand.addr, m.rand.read_en,
       m.rand.flush, m.rand.valid, m.seq.data,
       m.seq.addr, m.seq.ready, m.seq.valid]
verilog.convert(m, name="foo", ports=ios)
```

Because Interface objects have a `signature` attribute that describes their interface to the outside world, manually specifying these `Signal`s as top-level `ports` is redundant. It should be possible to use the `signature` attribute to infer all top-level ports in the generated Verilog if Interface objects are used. The `ports` argument can also be deprecated.



Pull request description:
back.{verilog,rtlil}: in convert(), accept a Component without ports
Fixes #883.
Depends on https://github.com/amaranth-lang/rfcs/pull/25.

Patch:
diff --git a/amaranth/back/rtlil.py b/amaranth/back/rtlil.py
index a9f0269..685bbf3 100644
--- a/amaranth/back/rtlil.py
+++ b/amaranth/back/rtlil.py
@@ -6,6 +6,7 @@ import re
 
 from .._utils import bits_for, flatten
 from ..hdl import ast, ir, mem, xfrm
+from ..lib import wiring
 
 
 __all__ = ["convert", "convert_fragment"]
@@ -863,49 +864,21 @@ def _convert_fragment(builder, fragment, name_map, hierarchy):
             if sub_name is None:
                 sub_name = module.anonymous()
 
-            sub_params = OrderedDict()
-            if hasattr(subfragment, "parameters"):
-                for param_name, param_value in subfragment.parameters.items():
-                    if isinstance(param_value, mem.Memory):
-                        memory = param_value
-                        if memory not in memories:
-                            memories[memory] = module.memory(width=memory.width, size=memory.depth,
-                                                             name=memory.name, attrs=memory.attrs)
-                            addr_bits = bits_for(memory.depth)
-                            data_parts = []
-                            data_mask = (1 << memory.width) - 1
-                            for addr in range(memory.depth):
-                                if addr < len(memory.init):
-                                    data = memory.init[addr] & data_mask
-                                else:
-                                    data = 0
-                                data_parts.append("{:0{}b}".format(data, memory.width))
-                            module.cell("$meminit", ports={
-                                "\\ADDR": rhs_compiler(ast.Const(0, addr_bits)),
-                                "\\DATA": "{}'".format(memory.width * memory.depth) +
-                                          "".join(reversed(data_parts)),
-                            }, params={
-                                "MEMID": memories[memory],
-                                "ABITS": addr_bits,
-                                "WIDTH": memory.width,
-                                "WORDS": memory.depth,
-                                "PRIORITY": 0,
-                            })
-
-                        param_value = memories[memory]
-
-                    sub_params[param_name] = param_value
+            sub_params = OrderedDict(getattr(subfragment, "parameters", {}))
 
             sub_type, sub_port_map = \
                 _convert_fragment(builder, subfragment, name_map,
                                   hierarchy=hierarchy + (sub_name,))
 
+            if sub_type == "$mem_v2" and "MEMID" not in sub_params:
+                sub_params["MEMID"] = "$" + sub_name
+            
             sub_ports = OrderedDict()
             for port, value in sub_port_map.items():
                 if not isinstance(subfragment, ir.Instance):
                     for signal in value._rhs_signals():
                         compiler_state.resolve_curr(signal, prefix=sub_name)
-                if len(value) > 0:
+                if len(value) > 0 or sub_type == "$mem_v2":
                     sub_ports[port] = rhs_compiler(value)
 
             module.cell(sub_type, name=sub_name, ports=sub_ports, params=sub_params,
@@ -1031,7 +1004,18 @@ def convert_fragment(fragment, name="top", *, emit_src=True):
     return str(builder), name_map
 
 
-def convert(elaboratable, name="top", platform=None, *, ports, emit_src=True, **kwargs):
+def convert(elaboratable, name="top", platform=None, *, ports=None, emit_src=True, **kwargs):
+    if (ports is None and
+            hasattr(elaboratable, "signature") and
+            isinstance(elaboratable.signature, wiring.Signature)):
+        ports = []
+        for path, member, value in elaboratable.signature.flatten(elaboratable):
+            if isinstance(value, ast.ValueCastable):
+                value = value.as_value()
+            if isinstance(value, ast.Value):
+                ports.append(value)
+    elif ports is None:
+        raise TypeError("The `convert()` function requires a `ports=` argument")
     fragment = ir.Fragment.get(elaboratable, platform).prepare(ports=ports, **kwargs)
     il_text, name_map = convert_fragment(fragment, name, emit_src=emit_src)
     return il_text
diff --git a/amaranth/back/verilog.py b/amaranth/back/verilog.py
index b6425ea..6a9c247 100644
--- a/amaranth/back/verilog.py
+++ b/amaranth/back/verilog.py
@@ -1,7 +1,8 @@
 import warnings
 
 from .._toolchain.yosys import *
-from ..hdl import ir
+from ..hdl import ast, ir
+from ..lib import wiring
 from . import rtlil
 
 
@@ -45,8 +46,19 @@ def convert_fragment(*args, strip_internal_attrs=False, **kwargs):
     return _convert_rtlil_text(rtlil_text, strip_internal_attrs=strip_internal_attrs), name_map
 
 
-def convert(elaboratable, name="top", platform=None, *, ports, emit_src=True,
+def convert(elaboratable, name="top", platform=None, *, ports=None, emit_src=True,
             strip_internal_attrs=False, **kwargs):
+    if (ports is None and
+            hasattr(elaboratable, "signature") and
+            isinstance(elaboratable.signature, wiring.Signature)):
+        ports = []
+        for path, member, value in elaboratable.signature.flatten(elaboratable):
+            if isinstance(value, ast.ValueCastable):
+                value = value.as_value()
+            if isinstance(value, ast.Value):
+                ports.append(value)
+    elif ports is None:
+        raise TypeError("The `convert()` function requires a `ports=` argument")
     fragment = ir.Fragment.get(elaboratable, platform).prepare(ports=ports, **kwargs)
     verilog_text, name_map = convert_fragment(fragment, name, emit_src=emit_src, strip_internal_attrs=strip_internal_attrs)
     return verilog_text
diff --git a/amaranth/compat/fhdl/specials.py b/amaranth/compat/fhdl/specials.py
index 901fc67..b35ce90 100644
--- a/amaranth/compat/fhdl/specials.py
+++ b/amaranth/compat/fhdl/specials.py
@@ -83,12 +83,6 @@ class _MemoryPort(CompatModule):
         self.clock = ClockSignal(clock_domain)
 
 
-@extend(NativeMemory)
-@deprecated("it is not necessary or permitted to add Memory as a special or submodule")
-def elaborate(self, platform):
-    return Fragment()
-
-
 class CompatMemory(NativeMemory, Elaboratable):
     def __init__(self, width, depth, init=None, name=None):
         super().__init__(width=width, depth=depth, init=init, name=name)
diff --git a/amaranth/hdl/ast.py b/amaranth/hdl/ast.py
index e1ee694..ed93347 100644
--- a/amaranth/hdl/ast.py
+++ b/amaranth/hdl/ast.py
@@ -78,11 +78,15 @@ class Shape:
         If ``False``, the value is unsigned. If ``True``, the value is signed two's complement.
     """
     def __init__(self, width=1, signed=False):
-        if not isinstance(width, int) or width < 0:
-            raise TypeError("Width must be a non-negative integer, not {!r}"
-                            .format(width))
+        if not isinstance(width, int):
+            raise TypeError(f"Width must be an integer, not {width!r}")
+        if not signed and width < 0:
+            raise TypeError(f"Width of an unsigned value must be zero or a positive integer, "
+                            f"not {width}")
+        if signed and width <= 0:
+            raise TypeError(f"Width of a signed value must be a positive integer, not {width}")
         self.width = width
-        self.signed = signed
+        self.signed = bool(signed)
 
     # The algorithm for inferring shape for standard Python enumerations is factored out so that
     # `Shape.cast()` and Amaranth's `EnumMeta.as_shape()` can both use it.
@@ -116,7 +120,7 @@ class Shape:
                 return Shape(obj)
             elif isinstance(obj, range):
                 if len(obj) == 0:
-                    return Shape(0, obj.start < 0)
+                    return Shape(0)
                 signed = obj[0] < 0 or obj[-1] < 0
                 width  = max(bits_for(obj[0], signed),
                              bits_for(obj[-1], signed))
@@ -860,9 +864,14 @@ class Part(Value):
         if not isinstance(stride, int) or stride <= 0:
             raise TypeError("Part stride must be a positive integer, not {!r}".format(stride))
 
+        value = Value.cast(value)
+        offset = Value.cast(offset)
+        if offset.shape().signed:
+            raise TypeError("Part offset must be unsigned")
+
         super().__init__(src_loc_at=src_loc_at)
-        self.value  = Value.cast(value)
-        self.offset = Value.cast(offset)
+        self.value  = value
+        self.offset = offset
         self.width  = width
         self.stride = stride
 
diff --git a/amaranth/hdl/ir.py b/amaranth/hdl/ir.py
index c26e162..bd1c54f 100644
--- a/amaranth/hdl/ir.py
+++ b/amaranth/hdl/ir.py
@@ -181,7 +181,6 @@ class Fragment:
         assert mode in ("silent", "warn", "error")
 
         driver_subfrags = SignalDict()
-        memory_subfrags = OrderedDict()
         def add_subfrag(registry, entity, entry):
             # Because of missing domain insertion, at the point when this code runs, we have
             # a mixture of bound and unbound {Clock,Reset}Signals. Map the bound ones to
@@ -212,24 +211,16 @@ class Fragment:
                 flatten_subfrags.add((subfrag, subfrag_hierarchy))
 
             if isinstance(subfrag, Instance):
-                # For memories (which are subfragments, but semantically a part of superfragment),
-                # record that this fragment is driving it.
-                if subfrag.type in ("$memrd", "$memwr"):
-                    memory = subfrag.parameters["MEMID"]
-                    add_subfrag(memory_subfrags, memory, (None, hierarchy))
-
                 # Never flatten instances.
                 continue
 
             # First, recurse into subfragments and let them detect driver conflicts as well.
-            subfrag_drivers, subfrag_memories = \
+            subfrag_drivers = \
                 subfrag._resolve_hierarchy_conflicts(subfrag_hierarchy, mode)
 
-            # Second, classify subfragments by signals they drive and memories they use.
+            # Second, classify subfragments by signals they drive.
             for signal in subfrag_drivers:
                 add_subfrag(driver_subfrags, signal, (subfrag, subfrag_hierarchy))
-            for memory in subfrag_memories:
-                add_subfrag(memory_subfrags, memory, (subfrag, subfrag_hierarchy))
 
         # Find out the set of subfragments that needs to be flattened into this fragment
         # to resolve driver-driver conflicts.
@@ -253,20 +244,6 @@ class Fragment:
                 message += "; hierarchy will be flattened"
                 warnings.warn_explicit(message, DriverConflict, *signal.src_loc)
 
-        for memory, subfrags in memory_subfrags.items():
-            subfrag_names = flatten_subfrags_if_needed(subfrags)
-            if not subfrag_names:
-                continue
-
-            # While we're at it, show a message.
-            message = ("Memory '{}' is accessed from multiple fragments: {}"
-                       .format(memory.name, ", ".join(subfrag_names)))
-            if mode == "error":
-                raise DriverConflict(message)
-            elif mode == "warn":
-                message += "; hierarchy will be flattened"
-                warnings.warn_explicit(message, DriverConflict, *memory.src_loc)
-
         # Flatten hierarchy.
         for subfrag, subfrag_hierarchy in sorted(flatten_subfrags, key=lambda x: x[1]):
             self._merge_subfragment(subfrag)
@@ -282,8 +259,7 @@ class Fragment:
             return self._resolve_hierarchy_conflicts(hierarchy, mode)
 
         # Nothing was flattened, we're done!
-        return (SignalSet(driver_subfrags.keys()),
-                set(memory_subfrags.keys()))
+        return SignalSet(driver_subfrags.keys())
 
     def _propagate_domains_up(self, hierarchy=("top",)):
         from .xfrm import DomainRenamer
diff --git a/amaranth/hdl/mem.py b/amaranth/hdl/mem.py
index 58bf6da..3eada33 100644
--- a/amaranth/hdl/mem.py
+++ b/amaranth/hdl/mem.py
@@ -3,13 +3,13 @@ from collections import OrderedDict
 
 from .. import tracer
 from .ast import *
-from .ir import Elaboratable, Instance
+from .ir import Elaboratable, Instance, Fragment
 
 
 __all__ = ["Memory", "ReadPort", "WritePort", "DummyPort"]
 
 
-class Memory:
+class Memory(Elaboratable):
     """A word addressable storage.
 
     Parameters
@@ -58,6 +58,8 @@ class Memory:
                                           .format(name or "memory", addr)))
 
         self.init = init
+        self._read_ports = []
+        self._write_ports = []
 
     @property
     def init(self):
@@ -116,6 +118,96 @@ class Memory:
         """Simulation only."""
         return self._array[index]
 
+    def elaborate(self, platform):
+        init = "".join(format(Const(elem, unsigned(self.width)).value, f"0{self.width}b") for elem in reversed(self.init))
+        init = Const(int(init or "0", 2), len(self.init) * self.width)
+        rd_clk = []
+        rd_clk_enable = 0
+        rd_transparency_mask = 0
+        for index, port in enumerate(self._read_ports):
+            if port.domain != "comb":
+                rd_clk.append(ClockSignal(port.domain))
+                rd_clk_enable |= 1 << index
+                if port.transparent:
+                    for write_index, write_port in enumerate(self._write_ports):
+                        if port.domain == write_port.domain:
+                            rd_transparency_mask |= 1 << (index * len(self._write_ports) + write_index)
+            else:
+                rd_clk.append(Const(0, 1))
+        f = Instance("$mem_v2",
+            *(("a", attr, value) for attr, value in self.attrs.items()),
+            p_SIZE=self.depth,
+            p_OFFSET=0,
+            p_ABITS=Shape.cast(range(self.depth)).width,
+            p_WIDTH=self.width,
+            p_INIT=init,
+            p_RD_PORTS=len(self._read_ports),
+            p_RD_CLK_ENABLE=Const(rd_clk_enable, len(self._read_ports)) if self._read_ports else Const(0, 1),
+            p_RD_CLK_POLARITY=Const(-1, unsigned(len(self._read_ports))) if self._read_ports else Const(0, 1),
+            p_RD_TRANSPARENCY_MASK=Const(rd_transparency_mask, max(1, len(self._read_ports) * len(self._write_ports))),
+            p_RD_COLLISION_X_MASK=Const(0, max(1, len(self._read_ports) * len(self._write_ports))),
+            p_RD_WIDE_CONTINUATION=Const(0, len(self._read_ports)) if self._read_ports else Const(0, 1),
+            p_RD_CE_OVER_SRST=Const(0, len(self._read_ports)) if self._read_ports else Const(0, 1),
+            p_RD_ARST_VALUE=Const(0, len(self._read_ports) * self.width),
+            p_RD_SRST_VALUE=Const(0, len(self._read_ports) * self.width),
+            p_RD_INIT_VALUE=Const(0, len(self._read_ports) * self.width),
+            p_WR_PORTS=len(self._write_ports),
+            p_WR_CLK_ENABLE=Const(-1, unsigned(len(self._write_ports))) if self._write_ports else Const(0, 1),
+            p_WR_CLK_POLARITY=Const(-1, unsigned(len(self._write_ports))) if self._write_ports else Const(0, 1),
+            p_WR_PRIORITY_MASK=Const(0, len(self._write_ports) * len(self._write_ports)) if self._write_ports else Const(0, 1),
+            p_WR_WIDE_CONTINUATION=Const(0, len(self._write_ports)) if self._write_ports else Const(0, 1),
+            i_RD_CLK=Cat(rd_clk),
+            i_RD_EN=Cat(port.en for port in self._read_ports),
+            i_RD_ARST=Const(0, len(self._read_ports)),
+            i_RD_SRST=Const(0, len(self._read_ports)),
+            i_RD_ADDR=Cat(port.addr for port in self._read_ports),
+            o_RD_DATA=Cat(port.data for port in self._read_ports),
+            i_WR_CLK=Cat(ClockSignal(port.domain) for port in self._write_ports),
+            i_WR_EN=Cat(Cat(en_bit.replicate(port.granularity) for en_bit in port.en) for port in self._write_ports),
+            i_WR_ADDR=Cat(port.addr for port in self._write_ports),
+            i_WR_DATA=Cat(port.data for port in self._write_ports),
+        )
+        for port in self._read_ports:
+            port._MustUse__used = True
+            if port.domain == "comb":
+                # Asynchronous port
+                f.add_statements(port.data.eq(self._array[port.addr]))
+                f.add_driver(port.data)
+            else:
+                # Synchronous port
+                data = self._array[port.addr]
+                for write_port in self._write_ports:
+                    if port.domain == write_port.domain and port.transparent:
+                        if len(write_port.en) > 1:
+                            parts = []
+                            for index, en_bit in enumerate(write_port.en):
+                                offset = index * write_port.granularity
+                                bits   = slice(offset, offset + write_port.granularity)
+                                cond = en_bit & (port.addr == write_port.addr)
+                                parts.append(Mux(cond, write_port.data[bits], data[bits]))
+                            data = Cat(parts)
+                        else:
+                            data = Mux(write_port.en, write_port.data, data)
+                f.add_statements(
+                    Switch(port.en, {
+                        1: port.data.eq(data)
+                    })
+                )
+                f.add_driver(port.data, port.domain)
+        for port in self._write_ports:
+            port._MustUse__used = True
+            if len(port.en) > 1:
+                for index, en_bit in enumerate(port.en):
+                    offset = index * port.granularity
+                    bits   = slice(offset, offset + port.granularity)
+                    write_data = self._array[port.addr][bits].eq(port.data[bits])
+                    f.add_statements(Switch(en_bit, { 1: write_data }))
+            else:
+                write_data = self._array[port.addr].eq(port.data)
+                f.add_statements(Switch(port.en, { 1: write_data }))
+            for signal in self._array:
+                f.add_driver(signal, port.domain)
+        return f
 
 class ReadPort(Elaboratable):
     """A memory read port.
@@ -142,9 +234,7 @@ class ReadPort(Elaboratable):
     data : Signal(memory.width), out
         Read data.
     en : Signal or Const, in
-        Read enable. If asserted, ``data`` is updated with the word stored at ``addr``. Note that
-        transparent ports cannot assign ``en`` (which is hardwired to 1 instead), as doing so is
-        currently not supported by Yosys.
+        Read enable. If asserted, ``data`` is updated with the word stored at ``addr``.
 
     Exceptions
     ----------
@@ -162,59 +252,19 @@ class ReadPort(Elaboratable):
                            name="{}_r_addr".format(memory.name), src_loc_at=1 + src_loc_at)
         self.data = Signal(memory.width,
                            name="{}_r_data".format(memory.name), src_loc_at=1 + src_loc_at)
-        if self.domain != "comb" and not transparent:
+        if self.domain != "comb":
             self.en = Signal(name="{}_r_en".format(memory.name), reset=1,
                              src_loc_at=1 + src_loc_at)
         else:
             self.en = Const(1)
 
+        memory._read_ports.append(self)
+
     def elaborate(self, platform):
-        f = Instance("$memrd",
-            p_MEMID=self.memory,
-            p_ABITS=self.addr.width,
-            p_WIDTH=self.data.width,
-            p_CLK_ENABLE=self.domain != "comb",
-            p_CLK_POLARITY=1,
-            p_TRANSPARENT=self.transparent,
-            i_CLK=ClockSignal(self.domain) if self.domain != "comb" else Const(0),
-            i_EN=self.en,
-            i_ADDR=self.addr,
-            o_DATA=self.data,
-        )
-        if self.domain == "comb":
-            # Asynchronous port
-            f.add_statements(self.data.eq(self.memory._array[self.addr]))
-            f.add_driver(self.data)
-        elif not self.transparent:
-            # Synchronous, read-before-write port
-            f.add_statements(
-                Switch(self.en, {
-                    1: self.data.eq(self.memory._array[self.addr])
-                })
-            )
-            f.add_driver(self.data, self.domain)
+        if self is self.memory._read_ports[0]:
+            return self.memory
         else:
-            # Synchronous, write-through port
-            # This model is a bit unconventional. We model transparent ports as asynchronous ports
-            # that are latched when the clock is high. This isn't exactly correct, but it is very
-            # close to the correct behavior of a transparent port, and the difference should only
-            # be observable in pathological cases of clock gating. A register is injected to
-            # the address input to achieve the correct address-to-data latency. Also, the reset
-            # value of the data output is forcibly set to the 0th initial value, if any--note that
-            # many FPGAs do not guarantee this behavior!
-            if len(self.memory.init) > 0:
-                self.data.reset = operator.index(self.memory.init[0])
-            latch_addr = Signal.like(self.addr)
-            f.add_statements(
-                latch_addr.eq(self.addr),
-                Switch(ClockSignal(self.domain), {
-                    0: self.data.eq(self.data),
-                    1: self.data.eq(self.memory._array[latch_addr]),
-                }),
-            )
-            f.add_driver(latch_addr, self.domain)
-            f.add_driver(self.data)
-        return f
+            return Fragment()
 
 
 class WritePort(Elaboratable):
@@ -272,31 +322,13 @@ class WritePort(Elaboratable):
         self.en   = Signal(memory.width // granularity,
                            name="{}_w_en".format(memory.name), src_loc_at=1 + src_loc_at)
 
+        memory._write_ports.append(self)
+
     def elaborate(self, platform):
-        f = Instance("$memwr",
-            p_MEMID=self.memory,
-            p_ABITS=self.addr.width,
-            p_WIDTH=self.data.width,
-            p_CLK_ENABLE=1,
-            p_CLK_POLARITY=1,
-            p_PRIORITY=0,
-            i_CLK=ClockSignal(self.domain),
-            i_EN=Cat(en_bit.replicate(self.granularity) for en_bit in self.en),
-            i_ADDR=self.addr,
-            i_DATA=self.data,
-        )
-        if len(self.en) > 1:
-            for index, en_bit in enumerate(self.en):
-                offset = index * self.granularity
-                bits   = slice(offset, offset + self.granularity)
-                write_data = self.memory._array[self.addr][bits].eq(self.data[bits])
-                f.add_statements(Switch(en_bit, { 1: write_data }))
+        if not self.memory._read_ports and self is self.memory._write_ports[0]:
+            return self.memory
         else:
-            write_data = self.memory._array[self.addr].eq(self.data)
-            f.add_statements(Switch(self.en, { 1: write_data }))
-        for signal in self.memory._array:
-            f.add_driver(signal, self.domain)
-        return f
+            return Fragment()
 
 
 class DummyPort:
diff --git a/amaranth/hdl/xfrm.py b/amaranth/hdl/xfrm.py
index f48de3d..72189cb 100644
--- a/amaranth/hdl/xfrm.py
+++ b/amaranth/hdl/xfrm.py
@@ -720,10 +720,14 @@ class EnableInserter(_ControlInserter):
 
     def on_fragment(self, fragment):
         new_fragment = super().on_fragment(fragment)
-        if isinstance(new_fragment, Instance) and new_fragment.type in ("$memrd", "$memwr"):
-            clk_port, clk_dir = new_fragment.named_ports["CLK"]
-            if isinstance(clk_port, ClockSignal) and clk_port.domain in self.controls:
-                en_port, en_dir = new_fragment.named_ports["EN"]
-                en_port = Mux(self.controls[clk_port.domain], en_port, Const(0, len(en_port)))
-                new_fragment.named_ports["EN"] = en_port, en_dir
+        if isinstance(new_fragment, Instance) and new_fragment.type == "$mem_v2":
+            for kind in ["RD", "WR"]:
+                clk_parts = new_fragment.named_ports[kind + "_CLK"][0].parts
+                en_parts = new_fragment.named_ports[kind + "_EN"][0].parts
+                new_en = []
+                for clk, en in zip(clk_parts, en_parts):
+                    if isinstance(clk, ClockSignal) and clk.domain in self.controls:
+                        en = Mux(self.controls[clk.domain], en, Const(0, len(en)))
+                    new_en.append(en)
+                new_fragment.named_ports[kind + "_EN"] = Cat(new_en), "i"
         return new_fragment
diff --git a/amaranth/lib/wiring.py b/amaranth/lib/wiring.py
index c8a4287..6534541 100644
--- a/amaranth/lib/wiring.py
+++ b/amaranth/lib/wiring.py
@@ -371,6 +371,33 @@ class Signature(metaclass=SignatureMeta):
         self.members.freeze()
         return self
 
+    def flatten(self, obj):
+        for name, member in self.members.items():
+            path = (name,)
+            value = getattr(obj, name)
+
+            def iter_member(value, *, path):
+                if member.is_port:
+                    yield path, Member(member.flow, member.shape, reset=member.reset), value
+                elif member.is_signature:
+                    for sub_path, sub_member, sub_value in member.signature.flatten(value):
+                        if member.flow == In:
+                            sub_member = sub_member.flip()
+                        yield ((*path, *sub_path), sub_member, sub_value)
+                else:
+                    assert False # :nocov:
+
+            def iter_dimensions(value, dimensions, *, path):
+                if not dimensions:
+                    yield from iter_member(value, path=path)
+                else:
+                    dimension, *rest_of_dimensions = dimensions
+                    for index in range(dimension):
+                        yield from iter_dimensions(value[index], rest_of_dimensions,
+                                                   path=(path, index))
+
+            yield from iter_dimensions(value, dimensions=member.dimensions, path=path)
+
     def is_compliant(self, obj, *, reasons=None, path=("obj",)):
         def check_attr_value(member, attr_value, *, path):
             if member.is_port:
diff --git a/docs/changes.rst b/docs/changes.rst
index dd6584d..a677776 100644
--- a/docs/changes.rst
+++ b/docs/changes.rst
@@ -35,6 +35,7 @@ Apply the following changes to code written against Amaranth 0.3 to migrate it t
 
 While code that uses the features listed as deprecated below will work in Amaranth 0.4, they will be removed in the next version.
 
+
 Implemented RFCs
 ----------------
 
@@ -78,6 +79,7 @@ Language changes
 * Added: :meth:`Const.cast`. (`RFC 4`_)
 * Added: :meth:`Value.matches` and ``with m.Case():`` accept any constant-castable objects. (`RFC 4`_)
 * Added: :meth:`Value.replicate`, superseding :class:`Repl`. (`RFC 10`_)
+* Added: :class:`Memory` supports transparent read ports with read enable.
 * Changed: creating a :class:`Signal` with a shape that is a :class:`ShapeCastable` implementing :meth:`ShapeCastable.__call__` wraps the returned object using that method. (`RFC 15`_)
 * Changed: :meth:`Value.cast` casts :class:`ValueCastable` objects recursively.
 * Changed: :meth:`Value.cast` treats instances of classes derived from both :class:`enum.Enum` and :class:`int` (including :class:`enum.IntEnum`) as enumerations rather than integers.
diff --git a/docs/lang.rst b/docs/lang.rst
index 6a1c110..9a87443 100644
--- a/docs/lang.rst
+++ b/docs/lang.rst
@@ -153,8 +153,8 @@ Shapes from ranges
 
 Casting a shape from a :class:`range` ``r`` produces a shape that:
 
-  * has a width large enough to represent both ``min(r)`` and ``max(r)``, and
-  * is signed if either ``min(r)`` or ``max(r)`` are negative, unsigned otherwise.
+  * has a width large enough to represent both ``min(r)`` and ``max(r)``, but not larger, and
+  * is signed if ``r`` contains any negative values, unsigned otherwise.
 
 Specifying a shape with a range is convenient for counters, indexes, and all other values whose width is derived from a set of numbers they must be able to fit:
 
@@ -184,6 +184,16 @@ Specifying a shape with a range is convenient for counters, indexes, and all oth
 
    Amaranth detects uses of :class:`Const` and :class:`Signal` that invoke such an off-by-one error, and emits a diagnostic message.
 
+.. note::
+
+   An empty range always casts to an ``unsigned(0)``, even if both of its bounds are negative.
+   This happens because, being empty, it does not contain any negative values.
+
+   .. doctest::
+
+      >>> Shape.cast(range(-1, -1))
+      unsigned(0)
+
 
 .. _lang-shapeenum:
 


Test patch:
diff --git a/tests/test_hdl_ast.py b/tests/test_hdl_ast.py
index 0cb8865..8745a53 100644
--- a/tests/test_hdl_ast.py
+++ b/tests/test_hdl_ast.py
@@ -42,11 +42,20 @@ class ShapeTestCase(FHDLTestCase):
         s3 = Shape(3, True)
         self.assertEqual(s3.width, 3)
         self.assertEqual(s3.signed, True)
+        s4 = Shape(0)
+        self.assertEqual(s4.width, 0)
+        self.assertEqual(s4.signed, False)
 
     def test_make_wrong(self):
         with self.assertRaisesRegex(TypeError,
-                r"^Width must be a non-negative integer, not -1$"):
-            Shape(-1)
+                r"^Width must be an integer, not 'a'$"):
+            Shape("a")
+        with self.assertRaisesRegex(TypeError,
+                r"^Width of an unsigned value must be zero or a positive integer, not -1$"):
+            Shape(-1, signed=False)
+        with self.assertRaisesRegex(TypeError,
+                r"^Width of a signed value must be a positive integer, not 0$"):
+            Shape(0, signed=True)
 
     def test_compare_non_shape(self):
         self.assertNotEqual(Shape(1, True), "hi")
@@ -87,7 +96,7 @@ class ShapeTestCase(FHDLTestCase):
 
     def test_cast_int_wrong(self):
         with self.assertRaisesRegex(TypeError,
-                r"^Width must be a non-negative integer, not -1$"):
+                r"^Width of an unsigned value must be zero or a positive integer, not -1$"):
             Shape.cast(-1)
 
     def test_cast_tuple_wrong(self):
@@ -116,7 +125,7 @@ class ShapeTestCase(FHDLTestCase):
         self.assertEqual(s6.signed, False)
         s7 = Shape.cast(range(-1, -1))
         self.assertEqual(s7.width, 0)
-        self.assertEqual(s7.signed, True)
+        self.assertEqual(s7.signed, False)
         s8 = Shape.cast(range(0, 10, 3))
         self.assertEqual(s8.width, 4)
         self.assertEqual(s8.signed, False)
@@ -386,7 +395,7 @@ class ConstTestCase(FHDLTestCase):
 
     def test_shape_wrong(self):
         with self.assertRaisesRegex(TypeError,
-                r"^Width must be a non-negative integer, not -1$"):
+                r"^Width of an unsigned value must be zero or a positive integer, not -1$"):
             Const(1, -1)
 
     def test_wrong_fencepost(self):
@@ -785,6 +794,11 @@ class BitSelectTestCase(FHDLTestCase):
         s = self.c.bit_select(self.s, 2)
         self.assertEqual(repr(s), "(part (const 8'd0) (sig s) 2 1)")
 
+    def test_offset_wrong(self):
+        with self.assertRaisesRegex(TypeError,
+                r"^Part offset must be unsigned$"):
+            self.c.bit_select(self.s.as_signed(), 1)
+
 
 class WordSelectTestCase(FHDLTestCase):
     def setUp(self):
@@ -817,6 +831,11 @@ class WordSelectTestCase(FHDLTestCase):
         s = self.c.word_select(self.s, 2)
         self.assertEqual(repr(s), "(part (const 8'd0) (sig s) 2 2)")
 
+    def test_offset_wrong(self):
+        with self.assertRaisesRegex(TypeError,
+                r"^Part offset must be unsigned$"):
+            self.c.word_select(self.s.as_signed(), 1)
+
 
 class CatTestCase(FHDLTestCase):
     def test_shape(self):
@@ -1012,7 +1031,7 @@ class SignalTestCase(FHDLTestCase):
 
     def test_shape_wrong(self):
         with self.assertRaisesRegex(TypeError,
-                r"^Width must be a non-negative integer, not -10$"):
+                r"^Width of an unsigned value must be zero or a positive integer, not -10$"):
             Signal(-10)
 
     def test_name(self):
diff --git a/tests/test_hdl_ir.py b/tests/test_hdl_ir.py
index 0efa80f..19bf03b 100644
--- a/tests/test_hdl_ir.py
+++ b/tests/test_hdl_ir.py
@@ -678,43 +678,6 @@ class FragmentHierarchyConflictTestCase(FHDLTestCase):
         )
         """)
 
-    def setUp_memory(self):
-        self.m = Memory(width=8, depth=4)
-        self.fr = self.m.read_port().elaborate(platform=None)
-        self.fw = self.m.write_port().elaborate(platform=None)
-        self.f1 = Fragment()
-        self.f2 = Fragment()
-        self.f2.add_subfragment(self.fr)
-        self.f1.add_subfragment(self.f2)
-        self.f3 = Fragment()
-        self.f3.add_subfragment(self.fw)
-        self.f1.add_subfragment(self.f3)
-
-    def test_conflict_memory(self):
-        self.setUp_memory()
-
-        self.f1._resolve_hierarchy_conflicts(mode="silent")
-        self.assertEqual(self.f1.subfragments, [
-            (self.fr, None),
-            (self.fw, None),
-        ])
-
-    def test_conflict_memory_error(self):
-        self.setUp_memory()
-
-        with self.assertRaisesRegex(DriverConflict,
-                r"^Memory 'm' is accessed from multiple fragments: top\.<unnamed #0>, "
-                    r"top\.<unnamed #1>$"):
-            self.f1._resolve_hierarchy_conflicts(mode="error")
-
-    def test_conflict_memory_warning(self):
-        self.setUp_memory()
-
-        with self.assertWarnsRegex(DriverConflict,
-                (r"^Memory 'm' is accessed from multiple fragments: top.<unnamed #0>, "
-                    r"top.<unnamed #1>; hierarchy will be flattened$")):
-            self.f1._resolve_hierarchy_conflicts(mode="warn")
-
     def test_explicit_flatten(self):
         self.f1 = Fragment()
         self.f2 = Fragment()
diff --git a/tests/test_hdl_mem.py b/tests/test_hdl_mem.py
index ebb90dd..b371362 100644
--- a/tests/test_hdl_mem.py
+++ b/tests/test_hdl_mem.py
@@ -58,8 +58,8 @@ class MemoryTestCase(FHDLTestCase):
         self.assertEqual(len(rdport.addr), 2)
         self.assertEqual(len(rdport.data), 8)
         self.assertEqual(len(rdport.en), 1)
-        self.assertIsInstance(rdport.en, Const)
-        self.assertEqual(rdport.en.value, 1)
+        self.assertIsInstance(rdport.en, Signal)
+        self.assertEqual(rdport.en.reset, 1)
 
     def test_read_port_non_transparent(self):
         mem    = Memory(width=8, depth=4)
diff --git a/tests/test_hdl_xfrm.py b/tests/test_hdl_xfrm.py
index a864819..f450d00 100644
--- a/tests/test_hdl_xfrm.py
+++ b/tests/test_hdl_xfrm.py
@@ -547,16 +547,18 @@ class EnableInserterTestCase(FHDLTestCase):
 
     def test_enable_read_port(self):
         mem = Memory(width=8, depth=4)
-        f = EnableInserter(self.c1)(mem.read_port(transparent=False)).elaborate(platform=None)
-        self.assertRepr(f.named_ports["EN"][0], """
-        (m (sig c1) (sig mem_r_en) (const 1'd0))
+        mem.read_port(transparent=False)
+        f = EnableInserter(self.c1)(mem).elaborate(platform=None)
+        self.assertRepr(f.named_ports["RD_EN"][0], """
+        (cat (m (sig c1) (sig mem_r_en) (const 1'd0)))
         """)
 
     def test_enable_write_port(self):
         mem = Memory(width=8, depth=4)
-        f = EnableInserter(self.c1)(mem.write_port()).elaborate(platform=None)
-        self.assertRepr(f.named_ports["EN"][0], """
-        (m
+        mem.write_port()
+        f = EnableInserter(self.c1)(mem).elaborate(platform=None)
+        self.assertRepr(f.named_ports["WR_EN"][0], """
+        (cat (m
             (sig c1)
             (cat
                 (cat
@@ -571,7 +573,7 @@ class EnableInserterTestCase(FHDLTestCase):
                 )
             )
             (const 8'd0)
-        )
+        ))
         """)
 
 
diff --git a/tests/test_lib_wiring.py b/tests/test_lib_wiring.py
index d33ac7e..ccdc3d1 100644
--- a/tests/test_lib_wiring.py
+++ b/tests/test_lib_wiring.py
@@ -372,6 +372,36 @@ class SignatureTestCase(unittest.TestCase):
                 r"^Cannot add members to a frozen signature$"):
             sig.members += {"b": Out(1)}
 
+    def assertFlattenedSignature(self, actual, expected):
+        for (a_path, a_member, a_value), (b_path, b_member, b_value) in zip(actual, expected):
+            self.assertEqual(a_path, b_path)
+            self.assertEqual(a_member, b_member)
+            self.assertIs(a_value, b_value)
+
+    def test_flatten(self):
+        sig = Signature({"a": In(1), "b": Out(2).array(2)})
+        intf = sig.create()
+        self.assertFlattenedSignature(sig.flatten(intf), [
+            (("a",), In(1), intf.a),
+            ((("b",), 0), Out(2), intf.b[0]),
+            ((("b",), 1), Out(2), intf.b[1])
+        ])
+
+    def test_flatten_sig(self):
+        sig = Signature({
+            "a": Out(Signature({"p": Out(1)})),
+            "b": Out(Signature({"q": In (1)})),
+            "c": In( Signature({"r": Out(1)})),
+            "d": In( Signature({"s": In (1)})),
+        })
+        intf = sig.create()
+        self.assertFlattenedSignature(sig.flatten(intf), [
+            (("a", "p"), Out(1), intf.a.p),
+            (("b", "q"), In (1), intf.b.q),
+            (("c", "r"), Out(1), intf.c.r),
+            (("d", "s"), In (1), intf.d.s),
+        ])
+
     def assertNotCompliant(self, reason_regex, sig, obj):
         self.assertFalse(sig.is_compliant(obj))
         reasons = []
diff --git a/tests/test_sim.py b/tests/test_sim.py
index e9f46fb..facd8ec 100644
--- a/tests/test_sim.py
+++ b/tests/test_sim.py
@@ -697,7 +697,6 @@ class SimulatorIntegrationTestCase(FHDLTestCase):
         self.setUp_memory()
         with self.assertSimulation(self.m) as sim:
             def process():
-                self.assertEqual((yield self.rdport.data), 0xaa)
                 yield self.rdport.addr.eq(1)
                 yield
                 yield
@@ -807,6 +806,7 @@ class SimulatorIntegrationTestCase(FHDLTestCase):
         self.m.submodules.rdport = self.rdport = self.memory.read_port()
         with self.assertSimulation(self.m) as sim:
             def process():
+                yield
                 self.assertEqual((yield self.rdport.data), 0xaa)
                 yield self.rdport.addr.eq(1)
                 yield
@@ -815,6 +815,51 @@ class SimulatorIntegrationTestCase(FHDLTestCase):
             sim.add_clock(1e-6)
             sim.add_sync_process(process)
 
+    def test_memory_transparency(self):
+        m = Module()
+        init = [0x11111111, 0x22222222, 0x33333333, 0x44444444]
+        m.submodules.memory = memory = Memory(width=32, depth=4, init=init)
+        rdport = memory.read_port()
+        wrport = memory.write_port(granularity=8)
+        with self.assertSimulation(m) as sim:
+            def process():
+                yield rdport.addr.eq(0)
+                yield
+                yield Settle()
+                self.assertEqual((yield rdport.data), 0x11111111)
+                yield rdport.addr.eq(1)
+                yield
+                yield Settle()
+                self.assertEqual((yield rdport.data), 0x22222222)
+                yield wrport.addr.eq(0)
+                yield wrport.data.eq(0x44444444)
+                yield wrport.en.eq(1)
+                yield
+                yield Settle()
+                self.assertEqual((yield rdport.data), 0x22222222)
+                yield wrport.addr.eq(1)
+                yield wrport.data.eq(0x55555555)
+                yield wrport.en.eq(1)
+                yield
+                yield Settle()
+                self.assertEqual((yield rdport.data), 0x22222255)
+                yield wrport.addr.eq(1)
+                yield wrport.data.eq(0x66666666)
+                yield wrport.en.eq(2)
+                yield rdport.en.eq(0)
+                yield
+                yield Settle()
+                self.assertEqual((yield rdport.data), 0x22222255)
+                yield wrport.addr.eq(1)
+                yield wrport.data.eq(0x77777777)
+                yield wrport.en.eq(4)
+                yield rdport.en.eq(1)
+                yield
+                yield Settle()
+                self.assertEqual((yield rdport.data), 0x22776655)
+            sim.add_clock(1e-6)
+            sim.add_sync_process(process)
+
     @_ignore_deprecated
     def test_sample_helpers(self):
         m = Module()