Skip to content

Implement RFC 58: Core support for ValueCastable formatting. #1274

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 35 additions & 2 deletions amaranth/hdl/_ast.py
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,27 @@ def __call__(self, *args, **kwargs):
"""
return super().__call__(*args, **kwargs) # :nocov:

def format(self, obj, spec):
"""Format a value.

This method is called by the Amaranth language to implement formatting for custom
shapes. Whenever :py:`"{obj:spec}"` is encountered by :class:`Format`, and :py:`obj`
has a custom shape that has a :meth:`format` method, :py:`obj.shape().format(obj, "spec")`
is called, and the format specifier is replaced with the result.

The default :meth:`format` implementation is:

.. code::

def format(self, obj, spec):
return Format(f"{{:{spec}}}", Value.cast(obj))

Returns
-------
:class:`Format`
"""
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),)
Expand Down Expand Up @@ -2576,14 +2597,26 @@ def subformat(sub_string):
chunks.append(literal)
if field_name is not None:
obj = get_field(field_name)
obj = fmt.convert_field(obj, conversion)
if conversion == "v":
obj = Value.cast(obj)
else:
obj = fmt.convert_field(obj, conversion)
format_spec = subformat(format_spec)
if isinstance(obj, Value):
# Perform validation.
self._parse_format_spec(format_spec, obj.shape())
chunks.append((obj, format_spec))
elif isinstance(obj, ValueCastable):
raise TypeError("'ValueCastable' formatting is not supported")
shape = obj.shape()
if isinstance(shape, ShapeCastable):
fmt = shape.format(obj, format_spec)
if not isinstance(fmt, Format):
raise TypeError(f"`ShapeCastable.format` must return a 'Format' instance, not {fmt!r}")
chunks += fmt._chunks
else:
obj = Value.cast(obj)
self._parse_format_spec(format_spec, obj.shape())
chunks.append((obj, format_spec))
elif isinstance(obj, Format):
if format_spec != "":
raise ValueError(f"Format specifiers ({format_spec!r}) cannot be used for 'Format' objects")
Expand Down
2 changes: 2 additions & 0 deletions docs/changes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,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 58: https://amaranth-lang.org/rfcs/0058-valuecastable-format.html
.. _RFC 59: https://amaranth-lang.org/rfcs/0059-no-domain-upwards-propagation.html
.. _RFC 62: https://amaranth-lang.org/rfcs/0062-memory-data.html

Expand All @@ -66,6 +67,7 @@ Implemented RFCs
* `RFC 50`_: ``Print`` statement and string formatting
* `RFC 51`_: Add ``ShapeCastable.from_bits`` and ``amaranth.lib.data.Const``
* `RFC 53`_: Low-level I/O primitives
* `RFC 58`_: Core support for ``ValueCastable`` formatting
* `RFC 59`_: Get rid of upwards propagation of clock domains


Expand Down
99 changes: 96 additions & 3 deletions tests/test_hdl_ast.py
Original file line number Diff line number Diff line change
Expand Up @@ -1412,6 +1412,79 @@ def as_value(self):
return self.dest


class MockShapeCastableFormat(ShapeCastable):
def __init__(self, dest):
self.dest = dest

def as_shape(self):
return self.dest

def __call__(self, value):
return value

def const(self, init):
return Const(init, self.dest)

def from_bits(self, bits):
return bits

def format(self, value, format_desc):
return Format("_{}_{}_", Value.cast(value), format_desc)


class MockValueCastableFormat(ValueCastable):
def __init__(self, dest):
self.dest = dest

def shape(self):
return MockShapeCastableFormat(Value.cast(self.dest).shape())

def as_value(self):
return self.dest


class MockValueCastableNoFormat(ValueCastable):
def __init__(self, dest):
self.dest = dest

def shape(self):
return MockShapeCastable(Value.cast(self.dest).shape())

def as_value(self):
return self.dest


class MockShapeCastableFormatWrong(ShapeCastable):
def __init__(self, dest):
self.dest = dest

def as_shape(self):
return self.dest

def __call__(self, value):
return value

def const(self, init):
return Const(init, self.dest)

def from_bits(self, bits):
return bits

def format(self, value, format_desc):
return Value.cast(value)


class MockValueCastableFormatWrong(ValueCastable):
def __init__(self, dest):
self.dest = dest

def shape(self):
return MockShapeCastableFormatWrong(Value.cast(self.dest).shape())

def as_value(self):
return self.dest


class MockValueCastableChanges(ValueCastable):
def __init__(self, width=0):
self.width = width
Expand Down Expand Up @@ -1567,6 +1640,21 @@ def test_construct(self):
fmt = Format("sub: {}, c: {:4x}", subfmt, c)
self.assertRepr(fmt, "(format 'sub: a: {:2x}, b: {:3x}, c: {:4x}' (sig a) (sig b) (sig c))")

def test_construct_valuecastable(self):
a = Signal()
b = MockValueCastable(a)
fmt = Format("{:x}", b)
self.assertRepr(fmt, "(format '{:x}' (sig a))")
c = MockValueCastableFormat(a)
fmt = Format("{:meow}", c)
self.assertRepr(fmt, "(format '_{}_meow_' (sig a))")
d = MockValueCastableNoFormat(a)
fmt = Format("{:x}", d)
self.assertRepr(fmt, "(format '{:x}' (sig a))")
e = MockValueCastableFormat(a)
fmt = Format("{!v:x}", e)
self.assertRepr(fmt, "(format '{:x}' (sig a))")

def test_construct_wrong(self):
a = Signal()
b = Signal(signed(16))
Expand All @@ -1576,9 +1664,6 @@ def test_construct_wrong(self):
with self.assertRaisesRegex(ValueError,
r"^cannot switch from automatic field numbering to manual field specification$"):
Format("{}, {1}", a, b)
with self.assertRaisesRegex(TypeError,
r"^'ValueCastable' formatting is not supported$"):
Format("{}", MockValueCastable(Const(0)))
with self.assertRaisesRegex(ValueError,
r"^Format specifiers \('s'\) cannot be used for 'Format' objects$"):
Format("{:s}", Format(""))
Expand Down Expand Up @@ -1622,6 +1707,14 @@ def test_construct_wrong(self):
r"^Cannot specify '_' with format specifier 'c'$"):
Format("{a:_c}", a=a)

def test_construct_valuecastable_wrong(self):
a = Signal()
b = MockValueCastableFormatWrong(a)
with self.assertRaisesRegex(TypeError,
r"^`ShapeCastable.format` must return a 'Format' instance, "
r"not \(sig a\)$"):
fmt = Format("{:x}", b)

def test_plus(self):
a = Signal()
b = Signal()
Expand Down