hdl._mem: add MemoryData class.

This implements half of RFC 62. The `MemoryData._Row` class will be
implemented later, as a follow-up.
This commit is contained in:
Wanda 2024-03-28 03:06:39 +01:00 committed by Catherine
parent 1deaf70ad9
commit 09d5540430
12 changed files with 280 additions and 195 deletions

View file

@ -7,7 +7,7 @@ from ._dsl import SyntaxError, SyntaxWarning, Module
from ._cd import DomainError, ClockDomain from ._cd import DomainError, ClockDomain
from ._ir import UnusedElaboratable, Elaboratable, DriverConflict, Fragment from ._ir import UnusedElaboratable, Elaboratable, DriverConflict, Fragment
from ._ir import Instance, IOBufferInstance 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 ._rec import Record
from ._xfrm import DomainRenamer, ResetInserter, EnableInserter from ._xfrm import DomainRenamer, ResetInserter, EnableInserter
@ -27,7 +27,7 @@ __all__ = [
"UnusedElaboratable", "Elaboratable", "DriverConflict", "Fragment", "UnusedElaboratable", "Elaboratable", "DriverConflict", "Fragment",
"Instance", "IOBufferInstance", "Instance", "IOBufferInstance",
# _mem # _mem
"MemoryIdentity", "MemoryInstance", "Memory", "ReadPort", "WritePort", "DummyPort", "MemoryData", "MemoryInstance", "Memory", "ReadPort", "WritePort", "DummyPort",
# _rec # _rec
"Record", "Record",
# _xfrm # _xfrm

View file

@ -1134,9 +1134,9 @@ class NetlistEmitter:
def emit_memory(self, module_idx: int, fragment: '_mem.MemoryInstance', name: str): def emit_memory(self, module_idx: int, fragment: '_mem.MemoryInstance', name: str):
cell = _nir.Memory(module_idx, cell = _nir.Memory(module_idx,
width=fragment._width, width=_ast.Shape.cast(fragment._data._shape).width,
depth=fragment._depth, depth=fragment._data._depth,
init=fragment._init, init=fragment._data._init._raw,
name=name, name=name,
attributes=fragment._attrs, attributes=fragment._attrs,
src_loc=fragment.src_loc, src_loc=fragment.src_loc,

View file

@ -1,33 +1,160 @@
import operator import operator
from collections import OrderedDict from collections import OrderedDict
from collections.abc import MutableSequence
from .. import tracer from .. import tracer
from ._ast import * from ._ast import *
from ._ir import Elaboratable, Fragment from ._ir import Elaboratable, Fragment
from ..utils import ceil_log2 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 <lang-shapecustom>`, 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: class MemorySimRead:
def __init__(self, identity, addr): def __init__(self, memory, addr):
assert isinstance(identity, MemoryIdentity) assert isinstance(memory, MemoryData)
self._identity = identity self._memory = memory
self._addr = Value.cast(addr) self._addr = Value.cast(addr)
def eq(self, value): def eq(self, value):
return MemorySimWrite(self._identity, self._addr, value) return MemorySimWrite(self._memory, self._addr, value)
class MemorySimWrite: class MemorySimWrite:
def __init__(self, identity, addr, data): def __init__(self, memory, addr, data):
assert isinstance(identity, MemoryIdentity) assert isinstance(memory, MemoryData)
self._identity = identity self._memory = memory
self._addr = Value.cast(addr) self._addr = Value.cast(addr)
self._data = Value.cast(data) self._data = Value.cast(data)
@ -66,26 +193,20 @@ class MemoryInstance(Fragment):
return len(self._data) // len(self._en) 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) super().__init__(src_loc=src_loc)
assert isinstance(identity, MemoryIdentity) assert isinstance(data, MemoryData)
self._identity = identity data.freeze()
self._width = operator.index(width) self._data = data
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)
self._attrs = attrs or {} self._attrs = attrs or {}
self._read_ports: "list[MemoryInstance._ReadPort]" = [] self._read_ports: "list[MemoryInstance._ReadPort]" = []
self._write_ports: "list[MemoryInstance._WritePort]" = [] self._write_ports: "list[MemoryInstance._WritePort]" = []
def read_port(self, *, domain, addr, data, en, transparent_for): 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) port = self._ReadPort(domain=domain, addr=addr, data=data, en=en, transparent_for=transparent_for)
assert len(port._data) == self._width shape = Shape.cast(self._data.shape)
assert len(port._addr) == ceil_log2(self._depth) assert len(port._data) == shape.width
assert len(port._addr) == ceil_log2(self._data.depth)
for idx in port._transparent_for: for idx in port._transparent_for:
assert isinstance(idx, int) assert isinstance(idx, int)
assert idx in range(len(self._write_ports)) assert idx in range(len(self._write_ports))
@ -96,8 +217,9 @@ class MemoryInstance(Fragment):
def write_port(self, *, domain, addr, data, en): def write_port(self, *, domain, addr, data, en):
port = self._WritePort(domain=domain, addr=addr, data=data, en=en) port = self._WritePort(domain=domain, addr=addr, data=data, en=en)
assert len(port._data) == self._width shape = Shape.cast(self._data.shape)
assert len(port._addr) == ceil_log2(self._depth) assert len(port._data) == shape.width
assert len(port._addr) == ceil_log2(self._data.depth)
self._write_ports.append(port) self._write_ports.append(port)
return len(self._write_ports) - 1 return len(self._write_ports) - 1
@ -145,28 +267,17 @@ class Memory(Elaboratable):
self.depth = depth self.depth = depth
self.attrs = OrderedDict(() if attrs is None else attrs) self.attrs = OrderedDict(() if attrs is None else attrs)
self.init = init
self._read_ports = [] self._read_ports = []
self._write_ports = [] self._write_ports = []
self._identity = MemoryIdentity() self._data = MemoryData(shape=width, depth=depth, init=init or [])
@property @property
def init(self): def init(self):
return self._init return self._data.init
@init.setter @init.setter
def init(self, new_init): def init(self, new_init):
self._init = [] if new_init is None else list(new_init) self._data.init = 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
def read_port(self, *, src_loc_at=0, **kwargs): def read_port(self, *, src_loc_at=0, **kwargs):
"""Get a read port. """Get a read port.
@ -202,10 +313,10 @@ class Memory(Elaboratable):
def __getitem__(self, index): def __getitem__(self, index):
"""Simulation only.""" """Simulation only."""
return MemorySimRead(self._identity, index) return MemorySimRead(self._data, index)
def elaborate(self, platform): 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 = {} write_ports = {}
for port in self._write_ports: for port in self._write_ports:
port._MustUse__used = True port._MustUse__used = True

View file

@ -272,10 +272,7 @@ class FragmentTransformer:
def on_fragment(self, fragment): def on_fragment(self, fragment):
if isinstance(fragment, MemoryInstance): if isinstance(fragment, MemoryInstance):
new_fragment = MemoryInstance( new_fragment = MemoryInstance(
identity=fragment._identity, data=fragment._data,
width=fragment._width,
depth=fragment._depth,
init=fragment._init,
attrs=fragment._attrs, attrs=fragment._attrs,
src_loc=fragment.src_loc src_loc=fragment.src_loc
) )

View file

@ -2,8 +2,8 @@ import operator
from collections import OrderedDict from collections import OrderedDict
from collections.abc import MutableSequence from collections.abc import MutableSequence
from ..hdl import MemoryIdentity, MemoryInstance, Shape, ShapeCastable, Const from ..hdl import MemoryData, MemoryInstance, Shape, ShapeCastable, Const
from ..hdl._mem import MemorySimRead from ..hdl._mem import MemorySimRead, FrozenError
from ..utils import ceil_log2 from ..utils import ceil_log2
from .._utils import final from .._utils import final
from .. import tracer from .. import tracer
@ -13,13 +13,6 @@ from . import wiring, data
__all__ = ["Memory", "ReadPort", "WritePort"] __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): class Memory(wiring.Component):
"""Addressable array of rows. """Addressable array of rows.
@ -55,116 +48,55 @@ class Memory(wiring.Component):
:class:`Memory`, e.g. to instantiate library cells directly. :class:`Memory`, e.g. to instantiate library cells directly.
""" """
class Init(MutableSequence): Init = MemoryData.Init
"""Memory initialization data.
This is a special container used only for the :attr:`Memory.init` attribute. It is similar def __init__(self, data=None, *, shape=None, depth=None, init=None, attrs=None, src_loc_at=0):
to :class:`list`, but does not support inserting or deleting elements; its length is always if data is None:
the same as the depth of the memory it belongs to. if shape is None:
raise ValueError("Either 'data' or 'shape' needs to be given")
If :py:`shape` is a :ref:`custom shape-castable object <lang-shapecustom>`, then: if depth is None:
raise ValueError("Either 'data' or 'depth' needs to be given")
* Each element must be convertible to :py:`shape` via :meth:`.ShapeCastable.const`, and if init is None:
* Elements that are not explicitly initialized default to :py:`shape.const(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)
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: else:
self._elems = [0] * depth if not isinstance(data, MemoryData):
self._raw = self._elems # intentionally mutably aliased raise TypeError(f"'data' must be a MemoryData instance, not {data!r}")
try: if shape is not None:
for index, item in enumerate(elems): raise ValueError("'data' and 'shape' cannot be given at the same time")
self[index] = item if depth is not None:
except (TypeError, ValueError) as e: raise ValueError("'data' and 'depth' cannot be given at the same time")
raise type(e)("Memory initialization value at address {:x}: {}" if init is not None:
.format(index, e)) from None raise ValueError("'data' and 'init' cannot be given at the same time")
self._data = data
@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)
self._attrs = {} if attrs is None else dict(attrs) self._attrs = {} if attrs is None else dict(attrs)
self.src_loc = tracer.get_src_loc(src_loc_at=src_loc_at) self.src_loc = tracer.get_src_loc(src_loc_at=src_loc_at)
self._identity = MemoryIdentity()
self._read_ports: "list[ReadPort]" = [] self._read_ports: "list[ReadPort]" = []
self._write_ports: "list[WritePort]" = [] self._write_ports: "list[WritePort]" = []
self._frozen = False self._frozen = False
super().__init__(wiring.Signature({})) super().__init__(wiring.Signature({}))
@property
def data(self):
return self._data
@property @property
def shape(self): def shape(self):
return self._shape return self._data.shape
@property @property
def depth(self): def depth(self):
return self._depth return self._data.depth
@property @property
def init(self): def init(self):
return self._init return self._data.init
@init.setter @init.setter
def init(self, init): def init(self, init):
if self._frozen: self._data.init = init
raise FrozenError("Cannot set 'init' on a memory that has already been elaborated")
self._init = Memory.Init(init, shape=self._shape, depth=self._depth)
@property @property
def attrs(self): def attrs(self):
@ -245,13 +177,12 @@ class Memory(wiring.Component):
def elaborate(self, platform): def elaborate(self, platform):
self._frozen = True self._frozen = True
self._init._frozen = True self._data.freeze()
if hasattr(platform, "get_memory"): if hasattr(platform, "get_memory"):
return platform.get_memory(self) return platform.get_memory(self)
shape = Shape.cast(self.shape) shape = Shape.cast(self.shape)
instance = MemoryInstance(identity=self._identity, width=shape.width, depth=self.depth, instance = MemoryInstance(data=self._data, attrs=self.attrs, src_loc=self.src_loc)
init=self.init._raw, attrs=self.attrs, src_loc=self.src_loc)
write_ports = {} write_ports = {}
for port in self._write_ports: for port in self._write_ports:
write_ports[port] = instance.write_port( write_ports[port] = instance.write_port(
@ -265,7 +196,7 @@ class Memory(wiring.Component):
def __getitem__(self, index): def __getitem__(self, index):
"""Simulation only.""" """Simulation only."""
return MemorySimRead(self._identity, index) return self._data[index]
class ReadPort: class ReadPort:

View file

@ -46,6 +46,9 @@ class BaseSimulation:
def get_signal(self, signal): def get_signal(self, signal):
raise NotImplementedError # :nocov: raise NotImplementedError # :nocov:
def get_memory(self, memory):
raise NotImplementedError # :nocov:
slots = NotImplemented slots = NotImplemented
def add_trigger(self, process, signal, *, trigger=None): def add_trigger(self, process, signal, *, trigger=None):
@ -54,10 +57,10 @@ class BaseSimulation:
def remove_trigger(self, process, signal): def remove_trigger(self, process, signal):
raise NotImplementedError # :nocov: raise NotImplementedError # :nocov:
def add_memory_trigger(self, process, identity): def add_memory_trigger(self, process, memory):
raise NotImplementedError # :nocov: raise NotImplementedError # :nocov:
def remove_memory_trigger(self, process, identity): def remove_memory_trigger(self, process, memory):
raise NotImplementedError # :nocov: raise NotImplementedError # :nocov:
def wait_interval(self, process, interval): def wait_interval(self, process, interval):

View file

@ -135,7 +135,7 @@ class PyCoroProcess(BaseProcess):
exec(_RHSValueCompiler.compile(self.state, command._addr, mode="curr"), exec(_RHSValueCompiler.compile(self.state, command._addr, mode="curr"),
self.exec_locals) self.exec_locals)
addr = Const(self.exec_locals["result"], command._addr.shape()).value 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] state = self.state.slots[index]
assert isinstance(state, BaseMemoryState) assert isinstance(state, BaseMemoryState)
response = state.read(addr) response = state.read(addr)
@ -147,7 +147,7 @@ class PyCoroProcess(BaseProcess):
exec(_RHSValueCompiler.compile(self.state, command._data, mode="curr"), exec(_RHSValueCompiler.compile(self.state, command._data, mode="curr"),
self.exec_locals) self.exec_locals)
data = Const(self.exec_locals["result"], command._data.shape()).value 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] state = self.state.slots[index]
assert isinstance(state, BaseMemoryState) assert isinstance(state, BaseMemoryState)
state.write(addr, data) state.write(addr, data)

View file

@ -481,7 +481,6 @@ class _FragmentCompiler:
domains = set(fragment.statements) domains = set(fragment.statements)
if isinstance(fragment, MemoryInstance): if isinstance(fragment, MemoryInstance):
self.state.add_memory(fragment)
for port in fragment._read_ports: for port in fragment._read_ports:
domains.add(port._domain) domains.add(port._domain)
for port in fragment._write_ports: for port in fragment._write_ports:
@ -510,8 +509,8 @@ class _FragmentCompiler:
_StatementCompiler(self.state, emitter, inputs=inputs)(domain_stmts) _StatementCompiler(self.state, emitter, inputs=inputs)(domain_stmts)
if isinstance(fragment, MemoryInstance): if isinstance(fragment, MemoryInstance):
self.state.add_memory_trigger(domain_process, fragment._identity) self.state.add_memory_trigger(domain_process, fragment._data)
memory_index = self.state.memories[fragment._identity] memory_index = self.state.get_memory(fragment._data)
rhs = _RHSValueCompiler(self.state, emitter, mode="curr", inputs=inputs) rhs = _RHSValueCompiler(self.state, emitter, mode="curr", inputs=inputs)
lhs = _LHSValueCompiler(self.state, emitter, rhs=rhs) lhs = _LHSValueCompiler(self.state, emitter, rhs=rhs)
@ -554,7 +553,7 @@ class _FragmentCompiler:
emitter.append(f"next_{signal_index} = {signal.init}") emitter.append(f"next_{signal_index} = {signal.init}")
if isinstance(fragment, MemoryInstance): 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") rhs = _RHSValueCompiler(self.state, emitter, mode="curr")
lhs = _LHSValueCompiler(self.state, emitter, rhs=rhs) lhs = _LHSValueCompiler(self.state, emitter, rhs=rhs)

View file

@ -5,7 +5,7 @@ import os.path
from ..hdl import * from ..hdl import *
from ..hdl._repr import * from ..hdl._repr import *
from ..hdl._mem import MemoryInstance, MemoryIdentity from ..hdl._mem import MemoryInstance
from ..hdl._ast import SignalDict, Slice, Operator from ..hdl._ast import SignalDict, Slice, Operator
from ._base import * from ._base import *
from ._pyrtl import _FragmentCompiler from ._pyrtl import _FragmentCompiler
@ -75,7 +75,7 @@ class _VCDWriter:
signal_names[signal] = set() signal_names[signal] = set()
signal_names[signal].add((*fragment_name, signal_name)) signal_names[signal].add((*fragment_name, signal_name))
if isinstance(fragment, MemoryInstance): if isinstance(fragment, MemoryInstance):
memories[fragment._identity] = (fragment, fragment_name) memories[fragment._data] = fragment_name
trace_names = SignalDict() trace_names = SignalDict()
assigned_names = set() assigned_names = set()
@ -92,10 +92,16 @@ class _VCDWriter:
trace_names[trace_signal] = {("bench", name)} trace_names[trace_signal] = {("bench", name)}
assigned_names.add(name) assigned_names.add(name)
self.traces.append(trace_signal) self.traces.append(trace_signal)
elif hasattr(trace, "_identity") and isinstance(trace._identity, MemoryIdentity): elif isinstance(trace, MemoryData):
if not trace._identity in memories: if not trace in memories:
raise ValueError(f"{trace!r} is a memory not part of the elaborated design") if trace.name not in assigned_names:
self.traces.append(trace._identity) 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: else:
raise TypeError(f"{trace!r} is not a traceable object") raise TypeError(f"{trace!r} is not a traceable object")
@ -146,19 +152,20 @@ class _VCDWriter:
self.vcd_signal_vars[signal].append((vcd_var, repr)) self.vcd_signal_vars[signal].append((vcd_var, repr))
for memory, memory_name in memories.values(): for memory, memory_name in memories.items():
self.vcd_memory_vars[memory._identity] = vcd_vars = [] self.vcd_memory_vars[memory] = vcd_vars = []
self.gtkw_memory_names[memory._identity] = gtkw_names = [] self.gtkw_memory_names[memory] = gtkw_names = []
if memory._width > 1: width = Shape.cast(memory.shape).width
suffix = f"[{memory._width - 1}:0]" if width > 1:
suffix = f"[{width - 1}:0]"
else: else:
suffix = "" suffix = ""
for idx, init in enumerate(memory._init): for idx, init in enumerate(memory._init._raw):
field_name = "\\" + memory_name[-1] + f"[{idx}]" field_name = "\\" + memory_name[-1] + f"[{idx}]"
var_scope = memory_name[:-1] var_scope = memory_name[:-1]
vcd_var = self.vcd_writer.register_var( vcd_var = self.vcd_writer.register_var(
scope=var_scope, name=field_name, 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) vcd_vars.append(vcd_var)
gtkw_field_name = field_name + suffix gtkw_field_name = field_name + suffix
@ -194,7 +201,7 @@ class _VCDWriter:
self.vcd_writer.change(vcd_var, timestamp, var_value) self.vcd_writer.change(vcd_var, timestamp, var_value)
def update_memory(self, timestamp, memory, addr, 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) self.vcd_writer.change(vcd_var, timestamp, value)
def update_process(self, timestamp, process, command): def update_process(self, timestamp, process, command):
@ -325,7 +332,7 @@ class _PyMemoryState(BaseMemoryState):
self.reset() self.reset()
def reset(self): def reset(self):
self.data = list(self.memory._init) self.data = list(self.memory._init._raw)
self.write_queue = [] self.write_queue = []
def commit(self): def commit(self):
@ -344,19 +351,19 @@ class _PyMemoryState(BaseMemoryState):
return awoken_any return awoken_any
def read(self, addr): def read(self, addr):
if addr not in range(self.memory._depth): if addr not in range(self.memory.depth):
return 0 return 0
return self.data[addr] return self.data[addr]
def write(self, addr, value, mask=None): def write(self, addr, value, mask=None):
if addr not in range(self.memory._depth): if addr not in range(self.memory.depth):
return return
if mask == 0: if mask == 0:
return return
if mask is None: 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.write_queue.append((addr, value, mask))
self.pending.add(self) self.pending.add(self)
@ -370,10 +377,6 @@ class _PySimulation(BaseSimulation):
self.slots = [] self.slots = []
self.pending = set() 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): def reset(self):
self.timeline.reset() self.timeline.reset()
for signal, index in self.signals.items(): for signal, index in self.signals.items():
@ -395,6 +398,15 @@ class _PySimulation(BaseSimulation):
self.signals[signal] = index self.signals[signal] = index
return 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): def add_trigger(self, process, signal, *, trigger=None):
index = self.get_signal(signal) index = self.get_signal(signal)
assert (process not in self.slots[index].waiters or assert (process not in self.slots[index].waiters or
@ -406,12 +418,12 @@ class _PySimulation(BaseSimulation):
assert process in self.slots[index].waiters assert process in self.slots[index].waiters
del self.slots[index].waiters[process] del self.slots[index].waiters[process]
def add_memory_trigger(self, process, identity): def add_memory_trigger(self, process, memory):
index = self.memories[identity] index = self.get_memory(memory)
self.slots[index].waiters[process] = None self.slots[index].waiters[process] = None
def remove_memory_trigger(self, process, identity): def remove_memory_trigger(self, process, memory):
index = self.memories[identity] index = self.get_memory(memory)
assert process in self.slots[index].waiters assert process in self.slots[index].waiters
del self.slots[index].waiters[process] del self.slots[index].waiters[process]

View file

@ -53,6 +53,7 @@ Implemented RFCs
.. _RFC 51: https://amaranth-lang.org/rfcs/0051-const-from-bits.html .. _RFC 51: https://amaranth-lang.org/rfcs/0051-const-from-bits.html
.. _RFC 53: https://amaranth-lang.org/rfcs/0053-ioport.html .. _RFC 53: https://amaranth-lang.org/rfcs/0053-ioport.html
.. _RFC 55: https://amaranth-lang.org/rfcs/0055-lib-io.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 17`_: Remove ``log2_int``
* `RFC 27`_: Testbench processes for the simulator * `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: :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: :meth:`ShapeCastable.from_bits` method. (`RFC 51`_)
* Added: IO values, :class:`IOPort` objects, :class:`IOBufferInstance` objects. (`RFC 53`_) * 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: ``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: ``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. * Changed: ``Signal(range(stop), init=stop)`` warning has been changed into a hard error and made to trigger on any out-of range value.

View file

@ -35,12 +35,12 @@ class MemoryTestCase(FHDLTestCase):
def test_init(self): def test_init(self):
with _ignore_deprecated(): with _ignore_deprecated():
m = Memory(width=8, depth=4, init=range(4)) 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): def test_init_wrong_count(self):
with _ignore_deprecated(): with _ignore_deprecated():
with self.assertRaisesRegex(ValueError, 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)) m = Memory(width=8, depth=4, init=range(8))
def test_init_wrong_type(self): def test_init_wrong_type(self):

View file

@ -287,13 +287,23 @@ class MemoryTestCase(FHDLTestCase):
self.assertEqual(m.init.shape, 8) self.assertEqual(m.init.shape, 8)
self.assertEqual(len(m.init), 4) self.assertEqual(len(m.init), 4)
self.assertEqual(m.attrs, {}) 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(list(m.init), [1, 2, 3, 0])
self.assertEqual(m.init._raw, [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.read_ports, ())
self.assertEqual(m.write_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): def test_constructor_shapecastable(self):
init = [ init = [
{"a": 0, "b": 1}, {"a": 0, "b": 1},
@ -303,7 +313,7 @@ class MemoryTestCase(FHDLTestCase):
self.assertEqual(m.shape, MyStruct) self.assertEqual(m.shape, MyStruct)
self.assertEqual(m.depth, 4) self.assertEqual(m.depth, 4)
self.assertEqual(m.attrs, {"ram_style": "block"}) 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(list(m.init), [{"a": 0, "b": 1}, {"a": 2, "b": 3}, None, None])
self.assertEqual(m.init._raw, [8, 0x1a, 0, 0]) self.assertEqual(m.init._raw, [8, 0x1a, 0, 0])
@ -321,6 +331,28 @@ class MemoryTestCase(FHDLTestCase):
(r"^Memory initialization value at address 1: " (r"^Memory initialization value at address 1: "
r"'str' object cannot be interpreted as an integer$")): r"'str' object cannot be interpreted as an integer$")):
memory.Memory(shape=8, depth=4, init=[1, "0"]) 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): def test_init_set(self):
m = memory.Memory(shape=8, depth=4, init=[]) m = memory.Memory(shape=8, depth=4, init=[])
@ -385,10 +417,8 @@ class MemoryTestCase(FHDLTestCase):
rp1 = m.read_port(domain="comb") rp1 = m.read_port(domain="comb")
f = m.elaborate(None) f = m.elaborate(None)
self.assertIsInstance(f, MemoryInstance) self.assertIsInstance(f, MemoryInstance)
self.assertIs(f._identity, m._identity) self.assertIs(f._data, m.data)
self.assertEqual(f._depth, 4) self.assertEqual(f._data._init._raw, [0x11, 0, 0, 0])
self.assertEqual(f._width, 5)
self.assertEqual(f._init, (0x11, 0, 0, 0))
self.assertEqual(f._write_ports[0]._domain, "sync") self.assertEqual(f._write_ports[0]._domain, "sync")
self.assertEqual(f._write_ports[0]._granularity, 5) self.assertEqual(f._write_ports[0]._granularity, 5)
self.assertIs(f._write_ports[0]._addr, wp.addr) self.assertIs(f._write_ports[0]._addr, wp.addr)