diff --git a/amaranth/hdl/__init__.py b/amaranth/hdl/__init__.py index de09d7524..bc0ebbfd3 100644 --- a/amaranth/hdl/__init__.py +++ b/amaranth/hdl/__init__.py @@ -7,7 +7,7 @@ from ._cd import DomainError, ClockDomain from ._ir import UnusedElaboratable, Elaboratable, DriverConflict, Fragment from ._ir import Instance, IOBufferInstance -from ._mem import MemoryIdentity, MemoryInstance, Memory, ReadPort, WritePort, DummyPort +from ._mem import MemoryData, MemoryInstance, Memory, ReadPort, WritePort, DummyPort from ._rec import Record from ._xfrm import DomainRenamer, ResetInserter, EnableInserter @@ -27,7 +27,7 @@ "UnusedElaboratable", "Elaboratable", "DriverConflict", "Fragment", "Instance", "IOBufferInstance", # _mem - "MemoryIdentity", "MemoryInstance", "Memory", "ReadPort", "WritePort", "DummyPort", + "MemoryData", "MemoryInstance", "Memory", "ReadPort", "WritePort", "DummyPort", # _rec "Record", # _xfrm diff --git a/amaranth/hdl/_ir.py b/amaranth/hdl/_ir.py index 10b9ab79a..cd2e3dec6 100644 --- a/amaranth/hdl/_ir.py +++ b/amaranth/hdl/_ir.py @@ -1134,9 +1134,9 @@ def emit_iobuffer(self, module_idx: int, instance: _ir.IOBufferInstance): def emit_memory(self, module_idx: int, fragment: '_mem.MemoryInstance', name: str): cell = _nir.Memory(module_idx, - width=fragment._width, - depth=fragment._depth, - init=fragment._init, + width=_ast.Shape.cast(fragment._data._shape).width, + depth=fragment._data._depth, + init=fragment._data._init._raw, name=name, attributes=fragment._attrs, src_loc=fragment.src_loc, diff --git a/amaranth/hdl/_mem.py b/amaranth/hdl/_mem.py index 3fd305808..7b32ca14a 100644 --- a/amaranth/hdl/_mem.py +++ b/amaranth/hdl/_mem.py @@ -1,33 +1,160 @@ import operator from collections import OrderedDict +from collections.abc import MutableSequence from .. import tracer from ._ast import * from ._ir import Elaboratable, Fragment from ..utils import ceil_log2 -from .._utils import deprecated +from .._utils import deprecated, final -__all__ = ["Memory", "ReadPort", "WritePort", "DummyPort"] +__all__ = ["MemoryData", "Memory", "ReadPort", "WritePort", "DummyPort"] -class MemoryIdentity: pass +@final +class FrozenError(Exception): + """This exception is raised when ports are added to a :class:`Memory` or its + :attr:`~Memory.init` attribute is changed after it has been elaborated once. + """ + + +@final +class MemoryData: + @final + class Init(MutableSequence): + """Memory initialization data. + + This is a special container used only for initial contents of memories. It is similar + to :class:`list`, but does not support inserting or deleting elements; its length is always + the same as the depth of the memory it belongs to. + + If :py:`shape` is a :ref:`custom shape-castable object `, then: + + * Each element must be convertible to :py:`shape` via :meth:`.ShapeCastable.const`, and + * Elements that are not explicitly initialized default to :py:`shape.const(None)`. + + Otherwise (when :py:`shape` is a :class:`.Shape`): + + * Each element must be an :class:`int`, and + * Elements that are not explicitly initialized default to :py:`0`. + """ + def __init__(self, elems, *, shape, depth): + Shape.cast(shape) + if not isinstance(depth, int) or depth < 0: + raise TypeError("Memory depth must be a non-negative integer, not {!r}" + .format(depth)) + self._shape = shape + self._depth = depth + self._frozen = False + + if isinstance(shape, ShapeCastable): + self._elems = [None] * depth + self._raw = [Const.cast(Const(None, shape)).value] * depth + else: + self._elems = [0] * depth + self._raw = self._elems # intentionally mutably aliased + elems = list(elems) + if len(elems) > depth: + raise ValueError(f"Memory initialization value count exceeds memory depth ({len(elems)} > {depth})") + for index, item in enumerate(elems): + try: + self[index] = item + except (TypeError, ValueError) as e: + raise type(e)(f"Memory initialization value at address {index:x}: {e}") from None + + @property + def shape(self): + return self._shape + + def __getitem__(self, index): + return self._elems[index] + + def __setitem__(self, index, value): + if self._frozen: + raise FrozenError("Cannot set 'init' on a memory that has already been elaborated") + + if isinstance(index, slice): + indices = range(*index.indices(len(self._elems))) + if len(value) != len(indices): + raise ValueError("Changing length of Memory.init is not allowed") + for actual_index, actual_value in zip(indices, value): + self[actual_index] = actual_value + else: + if isinstance(self._shape, ShapeCastable): + self._raw[index] = Const.cast(Const(value, self._shape)).value + else: + value = operator.index(value) + # self._raw[index] assigned by the following line + self._elems[index] = value + + def __delitem__(self, index): + raise TypeError("Deleting elements from Memory.init is not allowed") + + def insert(self, index, value): + """:meta private:""" + raise TypeError("Inserting elements into Memory.init is not allowed") + + def __len__(self): + return self._depth + + def __repr__(self): + return f"MemoryData.Init({self._elems!r}, shape={self._shape!r}, depth={self._depth})" + + + def __init__(self, *, shape, depth, init, src_loc_at=0): + # shape and depth validation is performed in MemoryData.Init() + self._shape = shape + self._depth = depth + self._init = MemoryData.Init(init, shape=shape, depth=depth) + self.src_loc = tracer.get_src_loc(src_loc_at=src_loc_at) + self.name = tracer.get_var_name(depth=2+src_loc_at, default="$memory") + self._frozen = False + + def freeze(self): + self._frozen = True + self._init._frozen = True + + @property + def shape(self): + return self._shape + + @property + def depth(self): + return self._depth + + @property + def init(self): + return self._init + + @init.setter + def init(self, init): + if self._frozen: + raise FrozenError("Cannot set 'init' on a memory that has already been elaborated") + self._init = MemoryData.Init(init, shape=self._shape, depth=self._depth) + + def __repr__(self): + return f"(memory-data {self.name})" + + def __getitem__(self, index): + """Simulation only.""" + return MemorySimRead(self, index) class MemorySimRead: - def __init__(self, identity, addr): - assert isinstance(identity, MemoryIdentity) - self._identity = identity + def __init__(self, memory, addr): + assert isinstance(memory, MemoryData) + self._memory = memory self._addr = Value.cast(addr) def eq(self, value): - return MemorySimWrite(self._identity, self._addr, value) + return MemorySimWrite(self._memory, self._addr, value) class MemorySimWrite: - def __init__(self, identity, addr, data): - assert isinstance(identity, MemoryIdentity) - self._identity = identity + def __init__(self, memory, addr, data): + assert isinstance(memory, MemoryData) + self._memory = memory self._addr = Value.cast(addr) self._data = Value.cast(data) @@ -66,26 +193,20 @@ def _granularity(self): return len(self._data) // len(self._en) - def __init__(self, *, identity, width, depth, init=None, attrs=None, src_loc=None): + def __init__(self, *, data, attrs=None, src_loc=None): super().__init__(src_loc=src_loc) - assert isinstance(identity, MemoryIdentity) - self._identity = identity - self._width = operator.index(width) - self._depth = operator.index(depth) - mask = (1 << self._width) - 1 - self._init = tuple(item & mask for item in init) if init is not None else () - assert len(self._init) <= self._depth - self._init += (0,) * (self._depth - len(self._init)) - for x in self._init: - assert isinstance(x, int) + assert isinstance(data, MemoryData) + data.freeze() + self._data = data self._attrs = attrs or {} self._read_ports: "list[MemoryInstance._ReadPort]" = [] self._write_ports: "list[MemoryInstance._WritePort]" = [] def read_port(self, *, domain, addr, data, en, transparent_for): port = self._ReadPort(domain=domain, addr=addr, data=data, en=en, transparent_for=transparent_for) - assert len(port._data) == self._width - assert len(port._addr) == ceil_log2(self._depth) + shape = Shape.cast(self._data.shape) + assert len(port._data) == shape.width + assert len(port._addr) == ceil_log2(self._data.depth) for idx in port._transparent_for: assert isinstance(idx, int) assert idx in range(len(self._write_ports)) @@ -96,8 +217,9 @@ def read_port(self, *, domain, addr, data, en, transparent_for): def write_port(self, *, domain, addr, data, en): port = self._WritePort(domain=domain, addr=addr, data=data, en=en) - assert len(port._data) == self._width - assert len(port._addr) == ceil_log2(self._depth) + shape = Shape.cast(self._data.shape) + assert len(port._data) == shape.width + assert len(port._addr) == ceil_log2(self._data.depth) self._write_ports.append(port) return len(self._write_ports) - 1 @@ -145,28 +267,17 @@ def __init__(self, *, width, depth, init=None, name=None, attrs=None, simulate=T self.depth = depth self.attrs = OrderedDict(() if attrs is None else attrs) - self.init = init self._read_ports = [] self._write_ports = [] - self._identity = MemoryIdentity() + self._data = MemoryData(shape=width, depth=depth, init=init or []) @property def init(self): - return self._init + return self._data.init @init.setter def init(self, new_init): - self._init = [] if new_init is None else list(new_init) - if len(self.init) > self.depth: - raise ValueError("Memory initialization value count exceed memory depth ({} > {})" - .format(len(self.init), self.depth)) - - try: - for addr, val in enumerate(self._init): - operator.index(val) - except TypeError as e: - raise TypeError("Memory initialization value at address {:x}: {}" - .format(addr, e)) from None + self._data.init = new_init def read_port(self, *, src_loc_at=0, **kwargs): """Get a read port. @@ -202,10 +313,10 @@ def write_port(self, *, src_loc_at=0, **kwargs): def __getitem__(self, index): """Simulation only.""" - return MemorySimRead(self._identity, index) + return MemorySimRead(self._data, index) def elaborate(self, platform): - f = MemoryInstance(identity=self._identity, width=self.width, depth=self.depth, init=self.init, attrs=self.attrs, src_loc=self.src_loc) + f = MemoryInstance(data=self._data, attrs=self.attrs, src_loc=self.src_loc) write_ports = {} for port in self._write_ports: port._MustUse__used = True diff --git a/amaranth/hdl/_xfrm.py b/amaranth/hdl/_xfrm.py index a193e8ae7..90c23b7d8 100644 --- a/amaranth/hdl/_xfrm.py +++ b/amaranth/hdl/_xfrm.py @@ -272,10 +272,7 @@ def map_memory_ports(self, fragment, new_fragment): def on_fragment(self, fragment): if isinstance(fragment, MemoryInstance): new_fragment = MemoryInstance( - identity=fragment._identity, - width=fragment._width, - depth=fragment._depth, - init=fragment._init, + data=fragment._data, attrs=fragment._attrs, src_loc=fragment.src_loc ) diff --git a/amaranth/lib/memory.py b/amaranth/lib/memory.py index ea3c3ce57..7ed9f15dd 100644 --- a/amaranth/lib/memory.py +++ b/amaranth/lib/memory.py @@ -2,8 +2,8 @@ from collections import OrderedDict from collections.abc import MutableSequence -from ..hdl import MemoryIdentity, MemoryInstance, Shape, ShapeCastable, Const -from ..hdl._mem import MemorySimRead +from ..hdl import MemoryData, MemoryInstance, Shape, ShapeCastable, Const +from ..hdl._mem import MemorySimRead, FrozenError from ..utils import ceil_log2 from .._utils import final from .. import tracer @@ -13,13 +13,6 @@ __all__ = ["Memory", "ReadPort", "WritePort"] -@final -class FrozenError(Exception): - """This exception is raised when ports are added to a :class:`Memory` or its - :attr:`~Memory.init` attribute is changed after it has been elaborated once. - """ - - class Memory(wiring.Component): """Addressable array of rows. @@ -55,116 +48,55 @@ class Memory(wiring.Component): :class:`Memory`, e.g. to instantiate library cells directly. """ - class Init(MutableSequence): - """Memory initialization data. - - This is a special container used only for the :attr:`Memory.init` attribute. It is similar - to :class:`list`, but does not support inserting or deleting elements; its length is always - the same as the depth of the memory it belongs to. - - If :py:`shape` is a :ref:`custom shape-castable object `, then: - - * Each element must be convertible to :py:`shape` via :meth:`.ShapeCastable.const`, and - * Elements that are not explicitly initialized default to :py:`shape.const(None)`. - - Otherwise (when :py:`shape` is a :class:`.Shape`): - - * Each element must be an :class:`int`, and - * Elements that are not explicitly initialized default to :py:`0`. - """ - def __init__(self, elems, *, shape, depth): - Shape.cast(shape) - if not isinstance(depth, int) or depth < 0: - raise TypeError("Memory depth must be a non-negative integer, not {!r}" - .format(depth)) - self._shape = shape - self._depth = depth - self._frozen = False - - if isinstance(shape, ShapeCastable): - self._elems = [None] * depth - self._raw = [Const.cast(Const(None, shape)).value] * depth - else: - self._elems = [0] * depth - self._raw = self._elems # intentionally mutably aliased - try: - for index, item in enumerate(elems): - self[index] = item - except (TypeError, ValueError) as e: - raise type(e)("Memory initialization value at address {:x}: {}" - .format(index, e)) from None - - @property - def shape(self): - return self._shape - - def __getitem__(self, index): - return self._elems[index] - - def __setitem__(self, index, value): - if self._frozen: - raise FrozenError("Cannot set 'init' on a memory that has already been elaborated") - - if isinstance(index, slice): - indices = range(*index.indices(len(self._elems))) - if len(value) != len(indices): - raise ValueError("Changing length of Memory.init is not allowed") - for actual_index, actual_value in zip(indices, value): - self[actual_index] = actual_value - else: - if isinstance(self._shape, ShapeCastable): - self._raw[index] = Const.cast(Const(value, self._shape)).value - else: - value = operator.index(value) - # self._raw[index] assigned by the following line - self._elems[index] = value - - def __delitem__(self, index): - raise TypeError("Deleting elements from Memory.init is not allowed") - - def insert(self, index, value): - """:meta private:""" - raise TypeError("Inserting elements into Memory.init is not allowed") - - def __len__(self): - return self._depth - - def __repr__(self): - return f"Memory.Init({self._elems!r}, shape={self._shape!r}, depth={self._depth})" - - - def __init__(self, *, shape, depth, init, attrs=None, src_loc_at=0): - # shape and depth validation is performed in Memory.Init() - self._shape = shape - self._depth = depth - self._init = Memory.Init(init, shape=shape, depth=depth) + Init = MemoryData.Init + + def __init__(self, data=None, *, shape=None, depth=None, init=None, attrs=None, src_loc_at=0): + if data is None: + if shape is None: + raise ValueError("Either 'data' or 'shape' needs to be given") + if depth is None: + raise ValueError("Either 'data' or 'depth' needs to be given") + if init is None: + raise ValueError("Either 'data' or 'init' needs to be given") + data = MemoryData(shape=shape, depth=depth, init=init, src_loc_at=1 + src_loc_at) + else: + if not isinstance(data, MemoryData): + raise TypeError(f"'data' must be a MemoryData instance, not {data!r}") + if shape is not None: + raise ValueError("'data' and 'shape' cannot be given at the same time") + if depth is not None: + raise ValueError("'data' and 'depth' cannot be given at the same time") + if init is not None: + raise ValueError("'data' and 'init' cannot be given at the same time") + self._data = data self._attrs = {} if attrs is None else dict(attrs) self.src_loc = tracer.get_src_loc(src_loc_at=src_loc_at) - self._identity = MemoryIdentity() self._read_ports: "list[ReadPort]" = [] self._write_ports: "list[WritePort]" = [] self._frozen = False super().__init__(wiring.Signature({})) + @property + def data(self): + return self._data + @property def shape(self): - return self._shape + return self._data.shape @property def depth(self): - return self._depth + return self._data.depth @property def init(self): - return self._init + return self._data.init @init.setter def init(self, init): - if self._frozen: - raise FrozenError("Cannot set 'init' on a memory that has already been elaborated") - self._init = Memory.Init(init, shape=self._shape, depth=self._depth) + self._data.init = init @property def attrs(self): @@ -245,13 +177,12 @@ def write_ports(self): def elaborate(self, platform): self._frozen = True - self._init._frozen = True + self._data.freeze() if hasattr(platform, "get_memory"): return platform.get_memory(self) shape = Shape.cast(self.shape) - instance = MemoryInstance(identity=self._identity, width=shape.width, depth=self.depth, - init=self.init._raw, attrs=self.attrs, src_loc=self.src_loc) + instance = MemoryInstance(data=self._data, attrs=self.attrs, src_loc=self.src_loc) write_ports = {} for port in self._write_ports: write_ports[port] = instance.write_port( @@ -265,7 +196,7 @@ def elaborate(self, platform): def __getitem__(self, index): """Simulation only.""" - return MemorySimRead(self._identity, index) + return self._data[index] class ReadPort: diff --git a/amaranth/sim/_base.py b/amaranth/sim/_base.py index 1d76e1a05..9b8f2917e 100644 --- a/amaranth/sim/_base.py +++ b/amaranth/sim/_base.py @@ -46,6 +46,9 @@ def reset(self): def get_signal(self, signal): raise NotImplementedError # :nocov: + def get_memory(self, memory): + raise NotImplementedError # :nocov: + slots = NotImplemented def add_trigger(self, process, signal, *, trigger=None): @@ -54,10 +57,10 @@ def add_trigger(self, process, signal, *, trigger=None): def remove_trigger(self, process, signal): raise NotImplementedError # :nocov: - def add_memory_trigger(self, process, identity): + def add_memory_trigger(self, process, memory): raise NotImplementedError # :nocov: - def remove_memory_trigger(self, process, identity): + def remove_memory_trigger(self, process, memory): raise NotImplementedError # :nocov: def wait_interval(self, process, interval): diff --git a/amaranth/sim/_pycoro.py b/amaranth/sim/_pycoro.py index a48d5506e..75e9d1908 100644 --- a/amaranth/sim/_pycoro.py +++ b/amaranth/sim/_pycoro.py @@ -135,7 +135,7 @@ def run(self): exec(_RHSValueCompiler.compile(self.state, command._addr, mode="curr"), self.exec_locals) addr = Const(self.exec_locals["result"], command._addr.shape()).value - index = self.state.memories[command._identity] + index = self.state.get_memory(command._memory) state = self.state.slots[index] assert isinstance(state, BaseMemoryState) response = state.read(addr) @@ -147,7 +147,7 @@ def run(self): exec(_RHSValueCompiler.compile(self.state, command._data, mode="curr"), self.exec_locals) data = Const(self.exec_locals["result"], command._data.shape()).value - index = self.state.memories[command._identity] + index = self.state.get_memory(command._memory) state = self.state.slots[index] assert isinstance(state, BaseMemoryState) state.write(addr, data) diff --git a/amaranth/sim/_pyrtl.py b/amaranth/sim/_pyrtl.py index 8edee139b..363cec604 100644 --- a/amaranth/sim/_pyrtl.py +++ b/amaranth/sim/_pyrtl.py @@ -481,7 +481,6 @@ def __call__(self, fragment): domains = set(fragment.statements) if isinstance(fragment, MemoryInstance): - self.state.add_memory(fragment) for port in fragment._read_ports: domains.add(port._domain) for port in fragment._write_ports: @@ -510,8 +509,8 @@ def __call__(self, fragment): _StatementCompiler(self.state, emitter, inputs=inputs)(domain_stmts) if isinstance(fragment, MemoryInstance): - self.state.add_memory_trigger(domain_process, fragment._identity) - memory_index = self.state.memories[fragment._identity] + self.state.add_memory_trigger(domain_process, fragment._data) + memory_index = self.state.get_memory(fragment._data) rhs = _RHSValueCompiler(self.state, emitter, mode="curr", inputs=inputs) lhs = _LHSValueCompiler(self.state, emitter, rhs=rhs) @@ -554,7 +553,7 @@ def __call__(self, fragment): emitter.append(f"next_{signal_index} = {signal.init}") if isinstance(fragment, MemoryInstance): - memory_index = self.state.memories[fragment._identity] + memory_index = self.state.get_memory(fragment._data) rhs = _RHSValueCompiler(self.state, emitter, mode="curr") lhs = _LHSValueCompiler(self.state, emitter, rhs=rhs) diff --git a/amaranth/sim/pysim.py b/amaranth/sim/pysim.py index d13eb8271..384c09d80 100644 --- a/amaranth/sim/pysim.py +++ b/amaranth/sim/pysim.py @@ -5,7 +5,7 @@ from ..hdl import * from ..hdl._repr import * -from ..hdl._mem import MemoryInstance, MemoryIdentity +from ..hdl._mem import MemoryInstance from ..hdl._ast import SignalDict, Slice, Operator from ._base import * from ._pyrtl import _FragmentCompiler @@ -75,7 +75,7 @@ def __init__(self, design, *, vcd_file, gtkw_file=None, traces=(), fs_per_delta= signal_names[signal] = set() signal_names[signal].add((*fragment_name, signal_name)) if isinstance(fragment, MemoryInstance): - memories[fragment._identity] = (fragment, fragment_name) + memories[fragment._data] = fragment_name trace_names = SignalDict() assigned_names = set() @@ -92,10 +92,16 @@ def __init__(self, design, *, vcd_file, gtkw_file=None, traces=(), fs_per_delta= trace_names[trace_signal] = {("bench", name)} assigned_names.add(name) self.traces.append(trace_signal) - elif hasattr(trace, "_identity") and isinstance(trace._identity, MemoryIdentity): - if not trace._identity in memories: - raise ValueError(f"{trace!r} is a memory not part of the elaborated design") - self.traces.append(trace._identity) + elif isinstance(trace, MemoryData): + if not trace in memories: + if trace.name not in assigned_names: + name = trace.name + else: + name = f"{trace.name}${len(assigned_names)}" + assert name not in assigned_names + memories[trace] = ("bench", name) + assigned_names.add(name) + self.traces.append(trace) else: raise TypeError(f"{trace!r} is not a traceable object") @@ -146,19 +152,20 @@ def __init__(self, design, *, vcd_file, gtkw_file=None, traces=(), fs_per_delta= self.vcd_signal_vars[signal].append((vcd_var, repr)) - for memory, memory_name in memories.values(): - self.vcd_memory_vars[memory._identity] = vcd_vars = [] - self.gtkw_memory_names[memory._identity] = gtkw_names = [] - if memory._width > 1: - suffix = f"[{memory._width - 1}:0]" + for memory, memory_name in memories.items(): + self.vcd_memory_vars[memory] = vcd_vars = [] + self.gtkw_memory_names[memory] = gtkw_names = [] + width = Shape.cast(memory.shape).width + if width > 1: + suffix = f"[{width - 1}:0]" else: suffix = "" - for idx, init in enumerate(memory._init): + for idx, init in enumerate(memory._init._raw): field_name = "\\" + memory_name[-1] + f"[{idx}]" var_scope = memory_name[:-1] vcd_var = self.vcd_writer.register_var( scope=var_scope, name=field_name, - var_type="wire", size=memory._width, init=init, + var_type="wire", size=width, init=init, ) vcd_vars.append(vcd_var) gtkw_field_name = field_name + suffix @@ -194,7 +201,7 @@ def update_signal(self, timestamp, signal, value): self.vcd_writer.change(vcd_var, timestamp, var_value) def update_memory(self, timestamp, memory, addr, value): - vcd_var = self.vcd_memory_vars[memory._identity][addr] + vcd_var = self.vcd_memory_vars[memory][addr] self.vcd_writer.change(vcd_var, timestamp, value) def update_process(self, timestamp, process, command): @@ -325,7 +332,7 @@ def __init__(self, memory, pending): self.reset() def reset(self): - self.data = list(self.memory._init) + self.data = list(self.memory._init._raw) self.write_queue = [] def commit(self): @@ -344,19 +351,19 @@ def commit(self): return awoken_any def read(self, addr): - if addr not in range(self.memory._depth): + if addr not in range(self.memory.depth): return 0 return self.data[addr] def write(self, addr, value, mask=None): - if addr not in range(self.memory._depth): + if addr not in range(self.memory.depth): return if mask == 0: return if mask is None: - mask = (1 << self.memory._width) - 1 + mask = (1 << Shape.cast(self.memory.shape).width) - 1 self.write_queue.append((addr, value, mask)) self.pending.add(self) @@ -370,10 +377,6 @@ def __init__(self): self.slots = [] self.pending = set() - def add_memory(self, fragment): - self.memories[fragment._identity] = len(self.slots) - self.slots.append(_PyMemoryState(fragment, self.pending)) - def reset(self): self.timeline.reset() for signal, index in self.signals.items(): @@ -395,6 +398,15 @@ def get_signal(self, signal): self.signals[signal] = index return index + def get_memory(self, memory): + try: + return self.memories[memory] + except KeyError: + index = len(self.slots) + self.slots.append(_PyMemoryState(memory, self.pending)) + self.memories[memory] = index + return index + def add_trigger(self, process, signal, *, trigger=None): index = self.get_signal(signal) assert (process not in self.slots[index].waiters or @@ -406,12 +418,12 @@ def remove_trigger(self, process, signal): assert process in self.slots[index].waiters del self.slots[index].waiters[process] - def add_memory_trigger(self, process, identity): - index = self.memories[identity] + def add_memory_trigger(self, process, memory): + index = self.get_memory(memory) self.slots[index].waiters[process] = None - def remove_memory_trigger(self, process, identity): - index = self.memories[identity] + def remove_memory_trigger(self, process, memory): + index = self.get_memory(memory) assert process in self.slots[index].waiters del self.slots[index].waiters[process] diff --git a/docs/changes.rst b/docs/changes.rst index 55c6ca80c..2675c3885 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -53,6 +53,7 @@ Implemented RFCs .. _RFC 51: https://amaranth-lang.org/rfcs/0051-const-from-bits.html .. _RFC 53: https://amaranth-lang.org/rfcs/0053-ioport.html .. _RFC 55: https://amaranth-lang.org/rfcs/0055-lib-io.html +.. _RFC 62: https://amaranth-lang.org/rfcs/0062-memory-data.html * `RFC 17`_: Remove ``log2_int`` * `RFC 27`_: Testbench processes for the simulator @@ -75,6 +76,7 @@ Language changes * Added: :class:`Format` objects, :class:`Print` statements, messages in :class:`Assert`, :class:`Assume` and :class:`Cover`. (`RFC 50`_) * Added: :meth:`ShapeCastable.from_bits` method. (`RFC 51`_) * Added: IO values, :class:`IOPort` objects, :class:`IOBufferInstance` objects. (`RFC 53`_) +* Added: :class:`MemoryData` objects. (`RFC 62`_) * Changed: ``m.Case()`` with no patterns is never active instead of always active. (`RFC 39`_) * Changed: ``Value.matches()`` with no patterns is ``Const(0)`` instead of ``Const(1)``. (`RFC 39`_) * Changed: ``Signal(range(stop), init=stop)`` warning has been changed into a hard error and made to trigger on any out-of range value. diff --git a/tests/test_hdl_mem.py b/tests/test_hdl_mem.py index e4f0f3382..716cb9d23 100644 --- a/tests/test_hdl_mem.py +++ b/tests/test_hdl_mem.py @@ -35,12 +35,12 @@ def test_geometry_wrong(self): def test_init(self): with _ignore_deprecated(): m = Memory(width=8, depth=4, init=range(4)) - self.assertEqual(m.init, [0, 1, 2, 3]) + self.assertEqual(list(m.init), [0, 1, 2, 3]) def test_init_wrong_count(self): with _ignore_deprecated(): with self.assertRaisesRegex(ValueError, - r"^Memory initialization value count exceed memory depth \(8 > 4\)$"): + r"^Memory initialization value count exceeds memory depth \(8 > 4\)$"): m = Memory(width=8, depth=4, init=range(8)) def test_init_wrong_type(self): diff --git a/tests/test_lib_memory.py b/tests/test_lib_memory.py index adae6db30..69946c9cf 100644 --- a/tests/test_lib_memory.py +++ b/tests/test_lib_memory.py @@ -287,13 +287,23 @@ def test_constructor(self): self.assertEqual(m.init.shape, 8) self.assertEqual(len(m.init), 4) self.assertEqual(m.attrs, {}) - self.assertIsInstance(m.init, memory.Memory.Init) + self.assertIsInstance(m.init, MemoryData.Init) self.assertEqual(list(m.init), [1, 2, 3, 0]) self.assertEqual(m.init._raw, [1, 2, 3, 0]) - self.assertRepr(m.init, "Memory.Init([1, 2, 3, 0], shape=8, depth=4)") + self.assertRepr(m.init, "MemoryData.Init([1, 2, 3, 0], shape=8, depth=4)") self.assertEqual(m.read_ports, ()) self.assertEqual(m.write_ports, ()) + data = MemoryData(shape=8, depth=4, init=[1, 2, 3]) + m = memory.Memory(data) + self.assertIs(m.data, data) + self.assertEqual(m.shape, 8) + self.assertEqual(m.depth, 4) + self.assertEqual(m.init.shape, 8) + self.assertEqual(len(m.init), 4) + self.assertEqual(m.attrs, {}) + self.assertEqual(list(m.init), [1, 2, 3, 0]) + def test_constructor_shapecastable(self): init = [ {"a": 0, "b": 1}, @@ -303,7 +313,7 @@ def test_constructor_shapecastable(self): self.assertEqual(m.shape, MyStruct) self.assertEqual(m.depth, 4) self.assertEqual(m.attrs, {"ram_style": "block"}) - self.assertIsInstance(m.init, memory.Memory.Init) + self.assertIsInstance(m.init, MemoryData.Init) self.assertEqual(list(m.init), [{"a": 0, "b": 1}, {"a": 2, "b": 3}, None, None]) self.assertEqual(m.init._raw, [8, 0x1a, 0, 0]) @@ -321,6 +331,28 @@ def test_constructor_wrong(self): (r"^Memory initialization value at address 1: " r"'str' object cannot be interpreted as an integer$")): memory.Memory(shape=8, depth=4, init=[1, "0"]) + with self.assertRaisesRegex(ValueError, + r"^Either 'data' or 'shape' needs to be given$"): + memory.Memory(depth=4, init=[]) + with self.assertRaisesRegex(ValueError, + r"^Either 'data' or 'depth' needs to be given$"): + memory.Memory(shape=8, init=[]) + with self.assertRaisesRegex(ValueError, + r"^Either 'data' or 'init' needs to be given$"): + memory.Memory(shape=8, depth=4) + data = MemoryData(shape=8, depth=4, init=[]) + with self.assertRaisesRegex(ValueError, + r"^'data' and 'shape' cannot be given at the same time$"): + memory.Memory(data, shape=8) + with self.assertRaisesRegex(ValueError, + r"^'data' and 'depth' cannot be given at the same time$"): + memory.Memory(data, depth=4) + with self.assertRaisesRegex(ValueError, + r"^'data' and 'init' cannot be given at the same time$"): + memory.Memory(data, init=[]) + with self.assertRaisesRegex(TypeError, + r"^'data' must be a MemoryData instance, not 'abc'$"): + memory.Memory("abc") def test_init_set(self): m = memory.Memory(shape=8, depth=4, init=[]) @@ -385,10 +417,8 @@ def test_elaborate(self): rp1 = m.read_port(domain="comb") f = m.elaborate(None) self.assertIsInstance(f, MemoryInstance) - self.assertIs(f._identity, m._identity) - self.assertEqual(f._depth, 4) - self.assertEqual(f._width, 5) - self.assertEqual(f._init, (0x11, 0, 0, 0)) + self.assertIs(f._data, m.data) + self.assertEqual(f._data._init._raw, [0x11, 0, 0, 0]) self.assertEqual(f._write_ports[0]._domain, "sync") self.assertEqual(f._write_ports[0]._granularity, 5) self.assertIs(f._write_ports[0]._addr, wp.addr)