Skip to content

Commit 1ed318f

Browse files
committed
Implement RFC 58: Core support for ValueCastable formatting.
1 parent f71bee4 commit 1ed318f

File tree

3 files changed

+133
-5
lines changed

3 files changed

+133
-5
lines changed

amaranth/hdl/_ast.py

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,27 @@ def __call__(self, *args, **kwargs):
385385
"""
386386
return super().__call__(*args, **kwargs) # :nocov:
387387

388+
def format(self, obj, spec):
389+
"""Format a value.
390+
391+
This method is called by the Amaranth language to implement formatting for custom
392+
shapes. Whenever :py:`"{obj:spec}"` is encountered by :class:`Format`, and :py:`obj`
393+
has a custom shape that has a :meth:`format` method, :py:`obj.shape().format(obj, "spec")`
394+
is called, and the format specifier is replaced with the result.
395+
396+
The default :meth:`format` implementation is:
397+
398+
.. code::
399+
400+
def format(self, obj, spec):
401+
return Format(f"{{:{spec}}}", Value.cast(obj))
402+
403+
Returns
404+
-------
405+
:class:`Format`
406+
"""
407+
return Format(f"{{:{spec}}}", Value.cast(obj))
408+
388409
# TODO: write an RFC for turning this into a proper interface method
389410
def _value_repr(self, value):
390411
return (_repr.Repr(_repr.FormatInt(), value),)
@@ -2576,14 +2597,26 @@ def subformat(sub_string):
25762597
chunks.append(literal)
25772598
if field_name is not None:
25782599
obj = get_field(field_name)
2579-
obj = fmt.convert_field(obj, conversion)
2600+
if conversion == "v":
2601+
obj = Value.cast(obj)
2602+
else:
2603+
obj = fmt.convert_field(obj, conversion)
25802604
format_spec = subformat(format_spec)
25812605
if isinstance(obj, Value):
25822606
# Perform validation.
25832607
self._parse_format_spec(format_spec, obj.shape())
25842608
chunks.append((obj, format_spec))
25852609
elif isinstance(obj, ValueCastable):
2586-
raise TypeError("'ValueCastable' formatting is not supported")
2610+
shape = obj.shape()
2611+
if isinstance(shape, ShapeCastable):
2612+
fmt = shape.format(obj, format_spec)
2613+
if not isinstance(fmt, Format):
2614+
raise TypeError(f"`ShapeCastable.format` must return a 'Format' instance, not {fmt!r}")
2615+
chunks += fmt._chunks
2616+
else:
2617+
obj = Value.cast(obj)
2618+
self._parse_format_spec(format_spec, obj.shape())
2619+
chunks.append((obj, format_spec))
25872620
elif isinstance(obj, Format):
25882621
if format_spec != "":
25892622
raise ValueError(f"Format specifiers ({format_spec!r}) cannot be used for 'Format' objects")

docs/changes.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ Implemented RFCs
5454
.. _RFC 51: https://amaranth-lang.org/rfcs/0051-const-from-bits.html
5555
.. _RFC 53: https://amaranth-lang.org/rfcs/0053-ioport.html
5656
.. _RFC 55: https://amaranth-lang.org/rfcs/0055-lib-io.html
57+
.. _RFC 58: https://amaranth-lang.org/rfcs/0058-valuecastable-format.html
5758
.. _RFC 59: https://amaranth-lang.org/rfcs/0059-no-domain-upwards-propagation.html
5859
.. _RFC 62: https://amaranth-lang.org/rfcs/0062-memory-data.html
5960

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

7173

tests/test_hdl_ast.py

Lines changed: 96 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1412,6 +1412,79 @@ def as_value(self):
14121412
return self.dest
14131413

14141414

1415+
class MockShapeCastableFormat(ShapeCastable):
1416+
def __init__(self, dest):
1417+
self.dest = dest
1418+
1419+
def as_shape(self):
1420+
return self.dest
1421+
1422+
def __call__(self, value):
1423+
return value
1424+
1425+
def const(self, init):
1426+
return Const(init, self.dest)
1427+
1428+
def from_bits(self, bits):
1429+
return bits
1430+
1431+
def format(self, value, format_desc):
1432+
return Format("_{}_{}_", Value.cast(value), format_desc)
1433+
1434+
1435+
class MockValueCastableFormat(ValueCastable):
1436+
def __init__(self, dest):
1437+
self.dest = dest
1438+
1439+
def shape(self):
1440+
return MockShapeCastableFormat(Value.cast(self.dest).shape())
1441+
1442+
def as_value(self):
1443+
return self.dest
1444+
1445+
1446+
class MockValueCastableNoFormat(ValueCastable):
1447+
def __init__(self, dest):
1448+
self.dest = dest
1449+
1450+
def shape(self):
1451+
return MockShapeCastable(Value.cast(self.dest).shape())
1452+
1453+
def as_value(self):
1454+
return self.dest
1455+
1456+
1457+
class MockShapeCastableFormatWrong(ShapeCastable):
1458+
def __init__(self, dest):
1459+
self.dest = dest
1460+
1461+
def as_shape(self):
1462+
return self.dest
1463+
1464+
def __call__(self, value):
1465+
return value
1466+
1467+
def const(self, init):
1468+
return Const(init, self.dest)
1469+
1470+
def from_bits(self, bits):
1471+
return bits
1472+
1473+
def format(self, value, format_desc):
1474+
return Value.cast(value)
1475+
1476+
1477+
class MockValueCastableFormatWrong(ValueCastable):
1478+
def __init__(self, dest):
1479+
self.dest = dest
1480+
1481+
def shape(self):
1482+
return MockShapeCastableFormatWrong(Value.cast(self.dest).shape())
1483+
1484+
def as_value(self):
1485+
return self.dest
1486+
1487+
14151488
class MockValueCastableChanges(ValueCastable):
14161489
def __init__(self, width=0):
14171490
self.width = width
@@ -1567,6 +1640,21 @@ def test_construct(self):
15671640
fmt = Format("sub: {}, c: {:4x}", subfmt, c)
15681641
self.assertRepr(fmt, "(format 'sub: a: {:2x}, b: {:3x}, c: {:4x}' (sig a) (sig b) (sig c))")
15691642

1643+
def test_construct_valuecastable(self):
1644+
a = Signal()
1645+
b = MockValueCastable(a)
1646+
fmt = Format("{:x}", b)
1647+
self.assertRepr(fmt, "(format '{:x}' (sig a))")
1648+
c = MockValueCastableFormat(a)
1649+
fmt = Format("{:meow}", c)
1650+
self.assertRepr(fmt, "(format '_{}_meow_' (sig a))")
1651+
d = MockValueCastableNoFormat(a)
1652+
fmt = Format("{:x}", d)
1653+
self.assertRepr(fmt, "(format '{:x}' (sig a))")
1654+
e = MockValueCastableFormat(a)
1655+
fmt = Format("{!v:x}", e)
1656+
self.assertRepr(fmt, "(format '{:x}' (sig a))")
1657+
15701658
def test_construct_wrong(self):
15711659
a = Signal()
15721660
b = Signal(signed(16))
@@ -1576,9 +1664,6 @@ def test_construct_wrong(self):
15761664
with self.assertRaisesRegex(ValueError,
15771665
r"^cannot switch from automatic field numbering to manual field specification$"):
15781666
Format("{}, {1}", a, b)
1579-
with self.assertRaisesRegex(TypeError,
1580-
r"^'ValueCastable' formatting is not supported$"):
1581-
Format("{}", MockValueCastable(Const(0)))
15821667
with self.assertRaisesRegex(ValueError,
15831668
r"^Format specifiers \('s'\) cannot be used for 'Format' objects$"):
15841669
Format("{:s}", Format(""))
@@ -1622,6 +1707,14 @@ def test_construct_wrong(self):
16221707
r"^Cannot specify '_' with format specifier 'c'$"):
16231708
Format("{a:_c}", a=a)
16241709

1710+
def test_construct_valuecastable_wrong(self):
1711+
a = Signal()
1712+
b = MockValueCastableFormatWrong(a)
1713+
with self.assertRaisesRegex(TypeError,
1714+
r"^`ShapeCastable.format` must return a 'Format' instance, "
1715+
r"not \(sig a\)$"):
1716+
fmt = Format("{:x}", b)
1717+
16251718
def test_plus(self):
16261719
a = Signal()
16271720
b = Signal()

0 commit comments

Comments
 (0)