diff --git a/amaranth/hdl/_ast.py b/amaranth/hdl/_ast.py index d60e6af51..a0d4ebe8c 100644 --- a/amaranth/hdl/_ast.py +++ b/amaranth/hdl/_ast.py @@ -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),) @@ -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") diff --git a/docs/changes.rst b/docs/changes.rst index 354df3ca6..42d85cc70 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -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 @@ -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 diff --git a/tests/test_hdl_ast.py b/tests/test_hdl_ast.py index 583de3e67..1e9737d01 100644 --- a/tests/test_hdl_ast.py +++ b/tests/test_hdl_ast.py @@ -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 @@ -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)) @@ -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("")) @@ -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()