Repository: amaranth-lang/amaranth
Pull request #895 (2023-09-02 21:28:29)

vendor.GowinPlatform: oscillator improvements
cc @bl0x 

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/amaranth-lang/amaranth/pull/895?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=amaranth-lang) Report
> Merging [#895](https://app.codecov.io/gh/amaranth-lang/amaranth/pull/895?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=amaranth-lang) (301a66f) into [main](https://app.codecov.io/gh/amaranth-lang/amaranth/commit/fc85feb30d5fc8ada299f4ef32d502945eadf942?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=amaranth-lang) (fc85feb) will **decrease** coverage by `0.39%`.
> The diff coverage is `n/a`.

```diff
@@            Coverage Diff             @@
##             main     #895      +/-   ##
==========================================
- Coverage   83.94%   83.55%   -0.39%     
==========================================
  Files          54       54              
  Lines        7612     7603       -9     
  Branches     1844     1531     -313     
==========================================
- Hits         6390     6353      -37     
- Misses       1026     1055      +29     
+ Partials      196      195       -1     
```


[see 10 files with indirect coverage changes](https://app.codecov.io/gh/amaranth-lang/amaranth/pull/895/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=amaranth-lang)

:mega: We’re building smart automated test selection to slash your CI/CD build times. [Learn more](https://about.codecov.io/iterative-testing/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=amaranth-lang)


Patch:
diff --git a/.git_archival.txt b/.git_archival.txt
deleted file mode 100644
index 8fb235d..0000000
--- a/.git_archival.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-node: $Format:%H$
-node-date: $Format:%cI$
-describe-name: $Format:%(describe:tags=true,match=*[0-9]*)$
-ref-names: $Format:%D$
diff --git a/.gitattributes b/.gitattributes
index 5f2f45f..208af66 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1,2 +1,1 @@
-.git_archival.txt export-subst
 /amaranth/vendor/* -linguist-vendored
diff --git a/amaranth/back/rtlil.py b/amaranth/back/rtlil.py
index a9f0269..52fa91f 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"]
@@ -819,11 +820,10 @@ def _convert_fragment(builder, fragment, name_map, hierarchy):
         else:
             return "\\{}".format(fragment.type), port_map
 
-    module_name  = hierarchy[-1] or "anonymous"
+    module_name  = ".".join(name or "anonymous" for name in hierarchy)
     module_attrs = OrderedDict()
     if len(hierarchy) == 1:
         module_attrs["top"] = 1
-    module_attrs["amaranth.hierarchy"] = ".".join(name or "anonymous" for name in hierarchy)
 
     with builder.module(module_name, attrs=module_attrs) as module:
         compiler_state = _ValueCompilerState(module)
@@ -831,11 +831,6 @@ def _convert_fragment(builder, fragment, name_map, hierarchy):
         lhs_compiler   = _LHSValueCompiler(compiler_state)
         stmt_compiler  = _StatementCompiler(compiler_state, rhs_compiler, lhs_compiler)
 
-        # If the fragment is completely empty, add a dummy wire to it, or Yosys will interpret
-        # it as a black box by default (when read as Verilog).
-        if not fragment.ports and not fragment.statements and not fragment.subfragments:
-            module.wire(1, name="$empty_module_filler")
-
         # Register all signals driven in the current fragment. This must be done first, as it
         # affects further codegen; e.g. whether \sig$next signals will be generated and used.
         for domain, signal in fragment.iter_drivers():
@@ -860,52 +855,30 @@ def _convert_fragment(builder, fragment, name_map, hierarchy):
         # name) names.
         memories = OrderedDict()
         for subfragment, sub_name in fragment.subfragments:
+            if not (subfragment.ports or subfragment.statements or subfragment.subfragments):
+                # If the fragment is completely empty, skip translating it, otherwise synthesis
+                # tools (including Yosys and Vivado) will treat it as a black box when it is
+                # loaded after conversion to Verilog.
+                continue
+
             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"] = builder._make_name(sub_name, local=False)
+            
             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/build/res.py b/amaranth/build/res.py
index 41a3004..0202953 100644
--- a/amaranth/build/res.py
+++ b/amaranth/build/res.py
@@ -122,29 +122,36 @@ class ResourceManager:
                     fields[sub.name] = resolve(sub, dir[sub.name], xdr[sub.name],
                                                name="{}__{}".format(name, sub.name),
                                                attrs={**attrs, **sub.attrs})
-                return Record([
+                rec = Record([
                     (f_name, f.layout) for (f_name, f) in fields.items()
                 ], fields=fields, name=name)
+                rec.signature = wiring.Signature({
+                    f_name: wiring.Out(f.signature) for (f_name, f) in fields.items()
+                })
+                return rec
 
             elif isinstance(resource.ios[0], (Pins, DiffPairs)):
                 phys = resource.ios[0]
+                # The flow is `In` below regardless of requested pin direction. The flow should
+                # never be used as it's not used internally and anyone using `dir="-"` should
+                # ignore it as well.
                 if isinstance(phys, Pins):
                     phys_names = phys.names
-                    port = Record([("io", len(phys))], name=name)
+                    port = wiring.Signature({"io": wiring.In(len(phys))}).create(path=(name,))
                 if isinstance(phys, DiffPairs):
                     phys_names = []
-                    record_fields = []
+                    members = {}
                     if not self.should_skip_port_component(None, attrs, "p"):
                         phys_names += phys.p.names
-                        record_fields.append(("p", len(phys)))
+                        members["p"] = wiring.In(len(phys))
                     if not self.should_skip_port_component(None, attrs, "n"):
                         phys_names += phys.n.names
-                        record_fields.append(("n", len(phys)))
-                    port = Record(record_fields, name=name)
+                        members["n"] = wiring.In(len(phys))
+                    port = wiring.Signature(members).create(path=(name,))
                 if dir == "-":
                     pin = None
                 else:
-                    pin = Pin(len(phys), dir, xdr=xdr, name=name)
+                    pin = wiring.flipped(Pin(len(phys), dir, xdr=xdr, name=name))
 
                 for phys_name in phys_names:
                     if phys_name in self._phys_reqd:
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/_rec.py b/amaranth/hdl/_rec.py
index 2033ed0..2e88c58 100644
--- a/amaranth/hdl/_rec.py
+++ b/amaranth/hdl/_rec.py
@@ -7,6 +7,7 @@ from functools import reduce, wraps
 from .. import tracer
 from .._utils import union
 from .ast import *
+from ..lib import wiring
 
 
 __all__ = ["Direction", "DIR_NONE", "DIR_FANOUT", "DIR_FANIN", "Layout", "Record"]
@@ -132,6 +133,8 @@ class Record(ValueCastable):
             if fields is not None and field_name in fields:
                 field = fields[field_name]
                 if isinstance(field_shape, Layout):
+                    if isinstance(field, wiring.FlippedInterface):
+                        field = wiring.flipped(field)
                     assert isinstance(field, Record) and field_shape == field.layout
                 else:
                     assert isinstance(field, Signal) and Shape.cast(field_shape) == field.shape()
diff --git a/amaranth/hdl/ast.py b/amaranth/hdl/ast.py
index e1ee694..45941e6 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
 
@@ -1047,12 +1056,15 @@ class Signal(Value, DUID, metaclass=_SignalMeta):
                                  .format(orig_shape, Shape.cast(orig_shape),
                                          reset.shape()))
         else:
+            if reset is None:
+                reset = 0
             try:
-                reset = Const.cast(reset or 0)
+                reset = Const.cast(reset)
             except TypeError:
                 raise TypeError("Reset value must be a constant-castable expression, not {!r}"
                                 .format(orig_reset))
-        if orig_reset not in (None, 0, -1): # Avoid false positives for all-zeroes and all-ones
+        # Avoid false positives for all-zeroes and all-ones
+        if orig_reset is not None and not (isinstance(orig_reset, int) and orig_reset in (0, -1)):
             if reset.shape().signed and not self.signed:
                 warnings.warn(
                     message="Reset value {!r} is signed, but the signal shape is {!r}"
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..dd402ed 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), self.depth * 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/io.py b/amaranth/lib/io.py
index 2bb3615..a1b278d 100644
--- a/amaranth/lib/io.py
+++ b/amaranth/lib/io.py
@@ -4,18 +4,13 @@ from .. import *
 with warnings.catch_warnings():
     warnings.filterwarnings(action="ignore", category=DeprecationWarning)
     from ..hdl.rec import *
+from ..lib.wiring import In, Out, Signature
 
 
 __all__ = ["pin_layout", "Pin"]
 
 
-def pin_layout(width, dir, xdr=0):
-    """
-    Layout of the platform interface of a pin or several pins, which may be used inside
-    user-defined records.
-
-    See :class:`Pin` for details.
-    """
+def _pin_signature(width, dir, xdr=0):
     if not isinstance(width, int) or width < 0:
         raise TypeError("Width must be a non-negative integer, not {!r}"
                         .format(width))
@@ -26,29 +21,42 @@ def pin_layout(width, dir, xdr=0):
         raise TypeError("Gearing ratio must be a non-negative integer, not {!r}"
                         .format(xdr))
 
-    fields = []
+    members = {}
     if dir in ("i", "io"):
         if xdr > 0:
-            fields.append(("i_clk", 1))
+            members["i_clk"] = In(1)
         if xdr > 2:
-            fields.append(("i_fclk", 1))
+            members["i_fclk"] = In(1)
         if xdr in (0, 1):
-            fields.append(("i", width))
+            members["i"] = In(width)
         else:
             for n in range(xdr):
-                fields.append(("i{}".format(n), width))
+                members["i{}".format(n)] = In(width)
     if dir in ("o", "oe", "io"):
         if xdr > 0:
-            fields.append(("o_clk", 1))
+            members["o_clk"] = Out(1)
         if xdr > 2:
-            fields.append(("o_fclk", 1))
+            members["o_fclk"] = Out(1)
         if xdr in (0, 1):
-            fields.append(("o", width))
+            members["o"] = Out(width)
         else:
             for n in range(xdr):
-                fields.append(("o{}".format(n), width))
+                members["o{}".format(n)] = Out(width)
     if dir in ("oe", "io"):
-        fields.append(("oe", 1))
+        members["oe"] = In(1)
+    return Signature(members)
+
+
+def pin_layout(width, dir, xdr=0):
+    """
+    Layout of the platform interface of a pin or several pins, which may be used inside
+    user-defined records.
+
+    See :class:`Pin` for details.
+    """
+    fields = []
+    for name, member in _pin_signature(width, dir, xdr).members.items():
+        fields.append((name, member.shape))
     return Layout(fields)
 
 
@@ -118,3 +126,13 @@ class Pin(Record):
 
         super().__init__(pin_layout(self.width, self.dir, self.xdr),
                          name=name, src_loc_at=src_loc_at + 1)
+
+    @property
+    def signature(self):
+        return _pin_signature(self.width, self.dir, self.xdr)
+
+    def eq(self, other):
+        first_field, _, _ = next(iter(Pin(1, dir="o").layout))
+        warnings.warn(f"`pin.eq(...)` is deprecated; use `pin.{first_field}.eq(...)` here",
+                      DeprecationWarning, stacklevel=2)
+        return super().eq(other)
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/amaranth/vendor/_gowin.py b/amaranth/vendor/_gowin.py
index 247664e..9ea8f76 100644
--- a/amaranth/vendor/_gowin.py
+++ b/amaranth/vendor/_gowin.py
@@ -1,5 +1,6 @@
 from abc import abstractproperty
 from fractions import Fraction
+import math
 import re
 
 from ..hdl import *
@@ -160,19 +161,27 @@ class GowinPlatform(TemplatedPlatform):
 
     @property
     def _osc_div(self):
-        div_min  = 2
-        div_max  = 128
-        div_step = 2
-        div_frac = Fraction(self._osc_base_freq, self.osc_frequency)
-
-        if div_frac.denominator != 1 or div_frac not in range(div_min, div_max, div_step):
+        div_range = range(2, 130, 2)
+        div_frac  = Fraction(self._osc_base_freq, self.osc_frequency)
+
+        # Check that the requested frequency is within 50 ppm. This takes care of small mismatches
+        # arising due to rounding. The tolerance of a typical crystal oscillator is 50 ppm.
+        if (abs(round(div_frac) - div_frac) > Fraction(50, 1_000_000) or
+                int(div_frac) not in div_range):
+            achievable = (
+                min((frac for frac in div_range if frac > div_frac), default=None),
+                max((frac for frac in div_range if frac < div_frac), default=None)
+            )
             raise ValueError(
-                "On-chip oscillator frequency (platform.osc_frequency) must be chosen such that "
-                "the oscillator divider, calculated as ({}/{}), is an integer between {} and {} in "
-                "steps of {}"
-                .format(div_frac.numerator, div_frac.denominator, div_min, div_max, div_step))
-
-        return div_frac.numerator
+                f"On-chip oscillator frequency (platform.osc_frequency) must be chosen such that "
+                f"the base frequency of {self._osc_base_freq} Hz is divided by an integer factor "
+                f"between {div_range.start} and {div_range.stop} in steps of {div_range.step}; "
+                f"the divider for the requested frequency of {self.osc_frequency} Hz was "
+                f"calculated as ({div_frac.numerator}/{div_frac.denominator}), and the closest "
+                f"achievable frequencies are " +
+                ", ".join(str(self._osc_base_freq // frac) for frac in achievable if frac))
+
+        return int(div_frac)
 
     # Common templates
 
diff --git a/amaranth/vendor/_lattice_ice40.py b/amaranth/vendor/_lattice_ice40.py
index c186988..790268b 100644
--- a/amaranth/vendor/_lattice_ice40.py
+++ b/amaranth/vendor/_lattice_ice40.py
@@ -160,6 +160,7 @@ class LatticeICE40Platform(TemplatedPlatform):
         r"""
         {{invoke_tool("icepack")}}
             {{verbose("-v")}}
+            {{get_override("icepack_opts")|options}}
             {{name}}.asc
             {{name}}.bin
         """
diff --git a/docs/changes.rst b/docs/changes.rst
index dd6584d..6986ddb 100644
--- a/docs/changes.rst
+++ b/docs/changes.rst
@@ -31,10 +31,12 @@ Apply the following changes to code written against Amaranth 0.3 to migrate it t
 * Replace uses of ``Const.normalize(value, shape)`` with ``Const(value, shape).value``.
 * Replace uses of ``Repl(value, count)`` with ``value.replicate(count)``.
 * Replace uses of ``Record`` with :mod:`amaranth.lib.data` and :mod:`amaranth.lib.wiring`. The appropriate replacement depends on the use case. If ``Record`` was being used for data storage and accessing the bit-level representation, use :mod:`amaranth.lib.data`. If ``Record`` was being used for connecting design components together, use :mod:`amaranth.lib.wiring`.
+* Ensure the ``Pin`` instance returned by ``platform.request`` is not cast to value directly, but used for its fields. Replace code like ``leds = Cat(platform.request(led, n) for n in range(4))`` with ``leds = Cat(platform.request(led, n).o for n in range(4))`` (note the ``.o``).
 * Remove uses of ``amaranth.lib.scheduler.RoundRobin`` by inlining or copying the implementation of that class.
 
 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 +80,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.
@@ -122,6 +125,7 @@ Platform integration changes
 
 .. currentmodule:: amaranth.vendor
 
+* Added: ``icepack_opts`` override in :class:`vendor.LatticeICE40Platform`.
 * Added: ``OSCH`` as ``default_clk`` clock source in :class:`vendor.LatticeMachXO2Platform`, :class:`vendor.LatticeMachXO3LPlatform`.
 * Added: Xray toolchain support in :class:`vendor.XilinxPlatform`.
 * Added: :class:`vendor.GowinPlatform`.
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:
 
diff --git a/pdm_build.py b/pdm_build.py
new file mode 100644
index 0000000..bd0a54b
--- /dev/null
+++ b/pdm_build.py
@@ -0,0 +1,13 @@
+from pdm.backend._vendor.packaging.version import Version
+
+
+# This is done in a PDM build hook without specifying `dynamic = [..., "version"]` to put all
+# of the static metadata into pyproject.toml. Tools other than PDM will not execute this script
+# and will use the generic version of the documentation URL (which redirects to /latest).
+def pdm_build_initialize(context):
+    version = Version(context.config.metadata["version"])
+    if version.is_prerelease:
+        url_version = "latest"
+    else:
+        url_version = f"v{version}"
+    context.config.metadata["urls"]["Documentation"] += url_version
diff --git a/pyproject.toml b/pyproject.toml
index 45c8041..8e07f7f 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,7 +1,10 @@
 # Project metadata
 
+[tool.pdm.version]
+source = "scm"
+
 [project]
-dynamic = ["version", "urls"]
+dynamic = ["version"]
 
 name = "amaranth"
 description = "Amaranth hardware definition language"
@@ -23,21 +26,24 @@ remote-build  = ["paramiko~=2.7"]
 [project.scripts]
 amaranth-rpc = "amaranth.rpc:main"
 
+[project.urls]
+"Homepage" = "https://amaranth-lang.org/"
+"Documentation" = "https://amaranth-lang.org/docs/amaranth/" # modified in pdm_build.py
+"Source Code" = "https://github.com/amaranth-lang/amaranth"
+"Bug Tracker" = "https://github.com/amaranth-lang/amaranth/issues"
+
 # Build system configuration
 
 [build-system]
-requires = ["wheel", "setuptools>=67.0", "setuptools_scm[toml]>=6.2"]
-build-backend = "setuptools.build_meta"
+requires = ["pdm-backend"]
+build-backend = "pdm.backend"
 
-[tool.setuptools]
+[tool.pdm.build]
 # If amaranth 0.3 is checked out with git (e.g. as a part of a persistent editable install or
 # a git worktree cached by tools like poetry), it can have an empty `nmigen` directory left over,
 # which causes a hard error because setuptools cannot determine the top-level package.
 # Add a workaround to improve experience for people upgrading from old checkouts.
-packages = ["amaranth"]
-
-[tool.setuptools_scm]
-local_scheme = "node-and-timestamp"
+includes = ["amaranth/"]
 
 # Development workflow configuration
 
diff --git a/setup.py b/setup.py
deleted file mode 100644
index eb91960..0000000
--- a/setup.py
+++ /dev/null
@@ -1,25 +0,0 @@
-from setuptools import setup
-
-
-def doc_version():
-    try:
-        from setuptools_scm.git import parse as parse_git
-        git = parse_git(".")
-        if git.exact:
-            return git.format_with("v{tag}")
-        else:
-            return "latest"
-    except ImportError:
-        # PEP 517 compliant build tools will never reach this code path.
-        # Poetry reaches this code path.
-        return ""
-
-
-setup(
-    project_urls={
-        "Homepage": "https://amaranth-lang.org/",
-        "Documentation": "https://amaranth-lang.org/docs/amaranth/{}".format(doc_version()),
-        "Source Code": "https://github.com/amaranth-lang/amaranth",
-        "Bug Tracker": "https://github.com/amaranth-lang/amaranth/issues",
-    },
-)


Test patch:
diff --git a/tests/test_build_res.py b/tests/test_build_res.py
index 4acd55c..b017c6e 100644
--- a/tests/test_build_res.py
+++ b/tests/test_build_res.py
@@ -6,6 +6,7 @@ from amaranth import *
 with warnings.catch_warnings():
     warnings.filterwarnings(action="ignore", category=DeprecationWarning)
     from amaranth.hdl.rec import *
+from amaranth.lib.wiring import *
 from amaranth.lib.io import *
 from amaranth.build.dsl import *
 from amaranth.build.res import *
@@ -62,7 +63,7 @@ class ResourceManagerTestCase(FHDLTestCase):
         r = self.cm.lookup("user_led", 0)
         user_led = self.cm.request("user_led", 0)
 
-        self.assertIsInstance(user_led, Pin)
+        self.assertIsInstance(flipped(user_led), Pin)
         self.assertEqual(user_led.name, "user_led_0")
         self.assertEqual(user_led.width, 1)
         self.assertEqual(user_led.dir, "o")
@@ -91,11 +92,11 @@ class ResourceManagerTestCase(FHDLTestCase):
         self.assertEqual(ports[1].width, 1)
 
         scl_info, sda_info = self.cm.iter_single_ended_pins()
-        self.assertIs(scl_info[0], i2c.scl)
+        self.assertIs(flipped(scl_info[0]), i2c.scl)
         self.assertIs(scl_info[1].io, scl)
         self.assertEqual(scl_info[2], {})
         self.assertEqual(scl_info[3], False)
-        self.assertIs(sda_info[0], i2c.sda)
+        self.assertIs(flipped(sda_info[0]), i2c.sda)
         self.assertIs(sda_info[1].io, sda)
 
         self.assertEqual(list(self.cm.iter_port_constraints()), [
@@ -105,7 +106,7 @@ class ResourceManagerTestCase(FHDLTestCase):
 
     def test_request_diffpairs(self):
         clk100 = self.cm.request("clk100", 0)
-        self.assertIsInstance(clk100, Pin)
+        self.assertIsInstance(flipped(clk100), Pin)
         self.assertEqual(clk100.dir, "i")
         self.assertEqual(clk100.width, 1)
 
@@ -155,7 +156,6 @@ class ResourceManagerTestCase(FHDLTestCase):
 
     def test_request_raw(self):
         clk50 = self.cm.request("clk50", 0, dir="-")
-        self.assertIsInstance(clk50, Record)
         self.assertIsInstance(clk50.io, Signal)
 
         ports = list(self.cm.iter_ports())
@@ -164,7 +164,6 @@ class ResourceManagerTestCase(FHDLTestCase):
 
     def test_request_raw_diffpairs(self):
         clk100 = self.cm.request("clk100", 0, dir="-")
-        self.assertIsInstance(clk100, Record)
         self.assertIsInstance(clk100.p, Signal)
         self.assertIsInstance(clk100.n, Signal)
 
diff --git a/tests/test_hdl_ast.py b/tests/test_hdl_ast.py
index 0cb8865..c1ad320 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):
@@ -1034,6 +1053,10 @@ class SignalTestCase(FHDLTestCase):
                 r"not <StringEnum\.FOO: 'a'>$"):
             Signal(1, reset=StringEnum.FOO)
 
+    def test_reset_const_castable(self):
+        s1 = Signal(4, reset=Cat(Const(0, 1), Const(1, 1), Const(0, 2)))
+        self.assertEqual(s1.reset, 2)
+
     def test_reset_shape_castable_const(self):
         class CastableFromHex(ShapeCastable):
             def as_shape(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()