sim: use Format.* for VCD output, remove hdl._repr.

This also changes `decoder` a bit: when an enum is used as a decoder,
it is converted to a `Format.Enum` instead. The original enum is still
stored on the `decoder` attribute, so that it can be propagated
on `Signal.like`.
This commit is contained in:
Wanda 2024-04-12 23:08:41 +02:00 committed by Catherine
parent 122be7849c
commit 8bf4f77616
12 changed files with 88 additions and 179 deletions

View file

@ -5,7 +5,7 @@ import io
from ..utils import bits_for
from .._utils import to_binary
from ..lib import wiring
from ..hdl import _repr, _ast, _ir, _nir
from ..hdl import _ast, _ir, _nir
__all__ = ["convert", "convert_fragment"]

View file

@ -406,10 +406,6 @@ class ShapeCastable:
"""
return Format(f"{{:{spec}}}", Value.cast(obj))
# TODO: write an RFC for turning this into a proper interface method
def _value_repr(self, value):
return (_repr.Repr(_repr.FormatInt(), value),)
class _ShapeLikeMeta(type):
def __subclasscheck__(cls, subclass):
@ -2097,46 +2093,15 @@ class Signal(Value, DUID, metaclass=_SignalMeta):
if isinstance(orig_shape, ShapeCastable):
self._format = orig_shape.format(orig_shape(self), "")
elif isinstance(orig_shape, type) and issubclass(orig_shape, Enum):
self._format = Format.Enum(self, orig_shape, name=orig_shape.__name__)
else:
self._format = Format("{}", self)
if decoder is not None:
# The value representation is specified explicitly. Since we do not expose `hdl._repr`,
# this is the only way to add a custom filter to the signal right now.
if isinstance(decoder, type) and issubclass(decoder, Enum):
self._value_repr = (_repr.Repr(_repr.FormatEnum(decoder), self),)
else:
self._value_repr = (_repr.Repr(_repr.FormatCustom(decoder), self),)
else:
# If it's an enum, expose it via `self.decoder` for compatibility, whether it's a Python
# enum or an Amaranth enum. This also sets the value representation, even for custom
# shape-castables that implement their own `_value_repr`.
if isinstance(orig_shape, type) and issubclass(orig_shape, Enum):
decoder = orig_shape
else:
decoder = None
# The value representation is specified implicitly in the shape of the signal.
if isinstance(orig_shape, ShapeCastable):
# A custom shape-castable always has a `_value_repr`, at least the default one.
self._value_repr = tuple(orig_shape._value_repr(self))
elif isinstance(orig_shape, type) and issubclass(orig_shape, Enum):
# A non-Amaranth enum needs a value repr constructed for it.
self._value_repr = (_repr.Repr(_repr.FormatEnum(orig_shape), self),)
else:
# Any other case is formatted as a plain integer.
self._value_repr = (_repr.Repr(_repr.FormatInt(), self),)
# Compute the value representation that will be used by Amaranth.
if isinstance(decoder, type) and issubclass(decoder, Enum):
# Violence. In the name of backwards compatibility!
def enum_decoder(value):
try:
return "{0.name:}/{0.value:}".format(decoder(value))
except ValueError:
return str(value)
self._decoder = enum_decoder
else:
self._decoder = decoder
self._format = Format.Enum(self, decoder, name=decoder.__name__)
self._decoder = decoder
def shape(self):
return Shape(self._width, self._signed)
@ -3348,6 +3313,3 @@ class SignalDict(_MappedKeyDict):
class SignalSet(_MappedKeySet):
_map_key = SignalKey
_unmap_key = lambda self, key: key.signal
from . import _repr

View file

@ -1,58 +0,0 @@
from abc import ABCMeta, abstractmethod
__all__ = ["Format", "FormatInt", "FormatEnum", "FormatCustom", "Repr"]
class Format(metaclass=ABCMeta):
@abstractmethod
def format(self, value):
raise NotImplementedError
class FormatInt(Format):
def format(self, value):
return f"{value:d}"
def __repr__(self):
return f"FormatInt()"
class FormatEnum(Format):
def __init__(self, enum):
self.enum = enum
def format(self, value):
try:
return f"{self.enum(value).name}/{value:d}"
except ValueError:
return f"?/{value:d}"
def __repr__(self):
return f"FormatEnum({self.enum.__name__})"
class FormatCustom(Format):
def __init__(self, formatter):
self.formatter = formatter
def format(self, value):
return self.formatter(value)
def __repr__(self):
return f"FormatCustom({self.formatter})"
class Repr:
def __init__(self, format, value, *, path=()):
from ._ast import Value # avoid a circular dependency
assert isinstance(format, Format)
assert isinstance(value, Value)
assert isinstance(path, tuple) and all(isinstance(part, (str, int)) for part in path)
self.format = format
self.value = value
self.path = path
def __repr__(self):
return f"Repr({self.format!r}, {self.value!r}, {self.path!r})"

View file

@ -6,7 +6,6 @@ import operator
from amaranth._utils import final
from amaranth.hdl import *
from amaranth.hdl._repr import Repr, FormatInt, FormatEnum
from amaranth import hdl
@ -264,21 +263,6 @@ class Layout(ShapeCastable, metaclass=ABCMeta):
fields[str(key)] = Format("{}", field_value)
return Format.Struct(value, fields)
def _value_repr(self, value):
yield Repr(FormatInt(), value)
for key, field in self:
shape = Shape.cast(field.shape)
field_value = value[field.offset:field.offset+shape.width]
if shape.signed:
field_value = field_value.as_signed()
if isinstance(field.shape, ShapeCastable):
for repr in field.shape._value_repr(field_value):
yield Repr(repr.format, repr.value, path=(key,) + repr.path)
elif isinstance(field.shape, type) and issubclass(field.shape, Enum):
yield Repr(FormatEnum(field.shape), field_value, path=(key,))
else:
yield Repr(FormatInt(), field_value, path=(key,))
class StructLayout(Layout):
"""Description of a structure layout.

View file

@ -3,7 +3,6 @@ import warnings
import operator
from ..hdl import Value, ValueCastable, Shape, ShapeCastable, Const, SyntaxWarning, Format
from ..hdl._repr import Repr, FormatEnum
__all__ = py_enum.__all__ + ["EnumView", "FlagView"]
@ -181,9 +180,6 @@ class EnumType(ShapeCastable, py_enum.EnumMeta):
raise ValueError(f"Format specifier {format_spec!r} is not supported for enums")
return Format.Enum(value, cls, name=cls.__name__)
def _value_repr(cls, value):
yield Repr(FormatEnum(cls), value)
# In 3.11, Python renamed EnumMeta to EnumType. Like Python itself, we support both for
# compatibility.

View file

@ -2,12 +2,12 @@ from contextlib import contextmanager
import itertools
import re
import os.path
import enum as py_enum
from ..hdl import *
from ..hdl._repr import *
from ..hdl._mem import MemoryInstance
from ..hdl._ast import SignalDict, Slice, Operator
from ..hdl._ast import SignalDict
from ._base import *
from ._pyeval import eval_format, eval_value
from ._pyrtl import _FragmentCompiler
from ._pycoro import PyCoroProcess
from ._pyclock import PyClockProcess
@ -21,23 +21,8 @@ class _VCDWriter:
def decode_to_vcd(format, value):
return format.format(value).expandtabs().replace(" ", "_")
@staticmethod
def eval_field(field, signal, value):
if isinstance(field, Signal):
assert field is signal
return value
elif isinstance(field, Const):
return field.value
elif isinstance(field, Slice):
sub = _VCDWriter.eval_field(field.value, signal, value)
return (sub >> field.start) & ((1 << (field.stop - field.start)) - 1)
elif isinstance(field, Operator) and field.operator in ('s', 'u'):
sub = _VCDWriter.eval_field(field.operands[0], signal, value)
return Const(sub, field.shape()).value
else:
raise NotImplementedError # :nocov:
def __init__(self, design, *, vcd_file, gtkw_file=None, traces=(), fs_per_delta=0, processes=()):
def __init__(self, state, design, *, vcd_file, gtkw_file=None, traces=(), fs_per_delta=0, processes=()):
self.state = state
self.fs_per_delta = fs_per_delta
# Although pyvcd is a mandatory dependency, be resilient and import it as needed, so that
@ -123,16 +108,8 @@ class _VCDWriter:
for signal, names in itertools.chain(signal_names.items(), trace_names.items()):
self.vcd_signal_vars[signal] = []
self.gtkw_signal_names[signal] = []
for repr in signal._value_repr:
var_init = self.eval_field(repr.value, signal, signal.init)
if isinstance(repr.format, FormatInt):
var_type = "wire"
var_size = repr.value.shape().width
else:
var_type = "string"
var_size = 1
var_init = self.decode_to_vcd(repr.format, var_init)
def add_var(path, var_type, var_size, var_init, value):
vcd_var = None
for (*var_scope, var_name) in names:
if re.search(r"[ \t\r\n]", var_name):
@ -140,12 +117,12 @@ class _VCDWriter:
.format(".".join(var_scope), var_name))
field_name = var_name
for item in repr.path:
for item in path:
if isinstance(item, int):
field_name += f"[{item}]"
else:
field_name += f".{item}"
if repr.path:
if path:
field_name = "\\" + field_name
if vcd_var is None:
@ -162,7 +139,35 @@ class _VCDWriter:
scope=var_scope, name=field_name,
var=vcd_var)
self.vcd_signal_vars[signal].append((vcd_var, repr))
self.vcd_signal_vars[signal].append((vcd_var, value))
def add_wire_var(path, value):
add_var(path, "wire", len(value), eval_value(self.state, value), value)
def add_format_var(path, fmt):
add_var(path, "string", 1, eval_format(self.state, fmt), fmt)
def add_format(path, fmt):
if isinstance(fmt, Format.Struct):
add_wire_var(path, fmt._value)
for name, subfmt in fmt._fields.items():
add_format(path + (name,), subfmt)
elif isinstance(fmt, Format.Array):
add_wire_var(path, fmt._value)
for idx, subfmt in enumerate(fmt._fields):
add_format(path + (idx,), subfmt)
elif (isinstance(fmt, Format) and
len(fmt._chunks) == 1 and
isinstance(fmt._chunks[0], tuple) and
fmt._chunks[0][1] == ""):
add_wire_var(path, fmt._chunks[0][0])
else:
add_format_var(path, fmt)
if signal._decoder is not None and not isinstance(signal._decoder, py_enum.EnumMeta):
add_var((), "string", 1, signal._decoder(signal._init), signal._decoder)
else:
add_format((), signal._format)
for memory, memory_name in memories.items():
self.vcd_memory_vars[memory] = vcd_vars = []
@ -205,11 +210,15 @@ class _VCDWriter:
except KeyError:
pass # try another name
def update_signal(self, timestamp, signal, value):
def update_signal(self, timestamp, signal):
for (vcd_var, repr) in self.vcd_signal_vars.get(signal, ()):
var_value = self.eval_field(repr.value, signal, value)
if not isinstance(repr.format, FormatInt):
var_value = self.decode_to_vcd(repr.format, var_value)
if isinstance(repr, Value):
var_value = eval_value(self.state, repr)
elif isinstance(repr, (Format, Format.Enum)):
var_value = eval_format(self.state, repr)
else:
# decoder
var_value = repr(eval_value(self.state, signal))
self.vcd_writer.change(vcd_var, timestamp, var_value)
def update_memory(self, timestamp, memory, addr, value):
@ -512,7 +521,7 @@ class PySimEngine(BaseEngine):
if isinstance(change, _PySignalState):
signal_state = change
vcd_writer.update_signal(now_plus_deltas,
signal_state.signal, signal_state.curr)
signal_state.signal)
elif isinstance(change, _PyMemoryChange):
vcd_writer.update_memory(now_plus_deltas, change.state.memory,
change.addr, change.state.data[change.addr])
@ -559,7 +568,7 @@ class PySimEngine(BaseEngine):
@contextmanager
def write_vcd(self, *, vcd_file, gtkw_file, traces, fs_per_delta):
vcd_writer = _VCDWriter(self._design,
vcd_writer = _VCDWriter(self._state, self._design,
vcd_file=vcd_file, gtkw_file=gtkw_file, traces=traces, fs_per_delta=fs_per_delta,
processes=self._testbenches)
try:

View file

@ -61,6 +61,7 @@ Implemented RFCs
.. _RFC 59: https://amaranth-lang.org/rfcs/0059-no-domain-upwards-propagation.html
.. _RFC 62: https://amaranth-lang.org/rfcs/0062-memory-data.html
.. _RFC 63: https://amaranth-lang.org/rfcs/0063-remove-lib-coding.html
.. _RFC 65: https://amaranth-lang.org/rfcs/0065-format-struct-enum.html
* `RFC 17`_: Remove ``log2_int``
* `RFC 27`_: Testbench processes for the simulator
@ -75,6 +76,7 @@ Implemented RFCs
* `RFC 59`_: Get rid of upwards propagation of clock domains
* `RFC 62`_: The ``MemoryData`` class
* `RFC 63`_: Remove ``amaranth.lib.coding``
* `RFC 65`_: Special formatting for structures and enums
Language changes

View file

@ -1326,17 +1326,15 @@ class SignalTestCase(FHDLTestCase):
RED = 1
BLUE = 2
s = Signal(decoder=Color)
self.assertEqual(s.decoder(1), "RED/1")
self.assertEqual(s.decoder(3), "3")
self.assertRepr(s._value_repr, "(Repr(FormatEnum(Color), (sig s), ()),)")
self.assertEqual(s.decoder, Color)
self.assertRepr(s._format, "(format-enum (sig s) 'Color' (1 'RED') (2 'BLUE'))")
def test_enum(self):
s1 = Signal(UnsignedEnum)
self.assertEqual(s1.shape(), unsigned(2))
s2 = Signal(SignedEnum)
self.assertEqual(s2.shape(), signed(2))
self.assertEqual(s2.decoder(SignedEnum.FOO), "FOO/-1")
self.assertRepr(s2._value_repr, "(Repr(FormatEnum(SignedEnum), (sig s2), ()),)")
self.assertRepr(s2._format, "(format-enum (sig s2) 'SignedEnum' (-1 'FOO') (0 'BAR') (1 'BAZ'))")
def test_const_wrong(self):
s1 = Signal()
@ -1344,9 +1342,9 @@ class SignalTestCase(FHDLTestCase):
r"^Value \(sig s1\) cannot be converted to an Amaranth constant$"):
Const.cast(s1)
def test_value_repr(self):
def test_format_simple(self):
s = Signal()
self.assertRepr(s._value_repr, "(Repr(FormatInt(), (sig s), ()),)")
self.assertRepr(s._format, "(format '{}' (sig s))")
class ClockSignalTestCase(FHDLTestCase):

View file

@ -203,7 +203,7 @@ class RecordTestCase(FHDLTestCase):
def test_enum_decoder(self):
r1 = Record([("a", UnsignedEnum)])
self.assertEqual(r1.a.decoder(UnsignedEnum.FOO), "FOO/1")
self.assertRepr(r1.a._format, "(format-enum (sig r1__a) 'UnsignedEnum' (1 'FOO') (2 'BAR') (3 'BAZ'))")
def test_operators(self):
r1 = Record([("a", 1)])

View file

@ -531,10 +531,11 @@ class ViewTestCase(FHDLTestCase):
self.assertIsInstance(cv, Signal)
self.assertEqual(cv.shape(), unsigned(3))
self.assertEqual(cv.name, "v")
self.assertRepr(cv._value_repr, """
(Repr(FormatInt(), (sig v), ()),
Repr(FormatInt(), (slice (sig v) 0:1), ('a',)),
Repr(FormatInt(), (slice (sig v) 1:3), ('b',)))
self.assertRepr(cv._format, """
(format-struct (sig v)
('a' (format '{}' (slice (sig v) 0:1)))
('b' (format '{}' (slice (sig v) 1:3)))
)
""")
def test_construct_signal_init(self):

View file

@ -138,7 +138,7 @@ class EnumTestCase(FHDLTestCase):
B = -3
a = Signal(EnumA)
self.assertRepr(a, "(sig a)")
self.assertRepr(a._value_repr, "(Repr(FormatEnum(EnumA), (sig a), ()),)")
self.assertRepr(a._format, "(format-enum (sig a) 'EnumA' (0 'A') (-3 'B'))")
def test_enum_view(self):
class EnumA(Enum, shape=signed(4)):

View file

@ -469,10 +469,10 @@ class SimulatorUnitTestCase(FHDLTestCase):
class SimulatorIntegrationTestCase(FHDLTestCase):
@contextmanager
def assertSimulation(self, module, *, deadline=None):
def assertSimulation(self, module, *, deadline=None, traces=[]):
sim = Simulator(module)
yield sim
with sim.write_vcd("test.vcd", "test.gtkw"):
with sim.write_vcd("test.vcd", "test.gtkw", traces=traces):
if deadline is None:
sim.run()
else:
@ -1336,10 +1336,25 @@ class SimulatorIntegrationTestCase(FHDLTestCase):
self.assertEqual(eval_format(state, Format("{:s}", sig5)), "ABCD")
self.assertEqual(eval_format(state, Format("{:<5s}", sig5)), "ABCD ")
sim = Simulator(Module())
sim.add_testbench(testbench)
with sim.write_vcd("test.vcd", fs_per_delta=1):
sim.run()
with self.assertSimulation(Module(), traces=[sig, sig2, sig3, sig4, sig5]) as sim:
sim.add_testbench(testbench)
def test_decoder(self):
def decoder(val):
return f".oO{val}Oo."
sig = Signal(2, decoder=decoder)
def testbench():
yield Delay(1e-6)
yield sig.eq(1)
yield Delay(1e-6)
yield sig.eq(2)
yield Delay(1e-6)
with self.assertSimulation(Module(), traces=[sig]) as sim:
sim.add_testbench(testbench)
class SimulatorRegressionTestCase(FHDLTestCase):
def test_bug_325(self):