- Start Date: 2024-04-08
- RFC PR: amaranth-lang/rfcs#65
- Amaranth Issue: amaranth-lang/amaranth#1293
Special formatting for structures and enums
Summary
Extend Format machinery to support structured description of aggregate and enumerated data types.
Motivation
When a lib.data.Layout-typed signal is included in the design, it is often useful to treat it as multiple signals for debug purposes:
- in pysim VCD output, it is useful to have each field as its own trace
- in RTLIL output, it is useful to include a wire for each field (in particular, for CXXRTL simulation purposes)
Likewise, lib.enum.Enum-typed signals benefit from being marked as enum-typed in RTLIL output.
Currently, both of the above usecases are covered by the private ShapeCastable._value_repr interface. This interface has been added in a rush for the Amaranth 0.4 release as a stopgap measure, to ensure switching from hdl.rec to lib.data doesn't cause a functionality regression (amaranth-lang/amaranth#790). Such forbidden coupling between lib and hdl via private interfaces is frowned upon, and the time has come to atone for our sins and create a public interface that can be used for all sorts of aggregate and enumerated data types.
Since both needs relate to presentation of shape-castables, we propose piggybacking the functionality on top of ShapeCastable.format machinery. At the same time, we propose implementing formatting for lib.data and lib.enum.
Guide-level explanation
Shape-castables implementing enumerated types can return a Format.Enum instance instead of a Format from their format method:
class MyEnum(ShapeCastable):
...
def format(self, obj, spec):
assert spec == ""
return Format.Enum(Value.cast(obj), {
0: "ABC",
1: "DEF",
2: "GHI",
})
For formatting purposes, this is equivalent to:
class MyEnum(ShapeCastable):
...
def format(self, obj, spec):
assert spec == ""
return Format(
"{:s}",
Mux(Value.cast(obj) == 0, int.from_bytes("ABC".encode(), "little"),
Mux(Value.cast(obj) == 1, int.from_bytes("DEF".encode(), "little"),
Mux(Value.cast(obj) == 2, int.from_bytes("GHI".encode(), "little"),
int.from_bytes("[unknown]".encode(), "little")
)))
)
with the added benefit that any MyEnum-shaped signals will be automatically marked as enums in RTLIL output.
Likewise, shape-castables implementing aggregate types can return a Format.Struct instance instead of a Format:
class MyStruct(ShapeCastable):
...
def format(self, obj, spec):
assert spec == ""
return Format.Struct({
# Assume obj.a, obj.b, obj.c are accessors that return the struct fields as ValueLike.
"a": Format("{}", obj.a),
"b": Format("{}", obj.b),
"c": Format("{}", obj.c),
})
For formatting purposes, this is equivalent to:
class MyStruct(ShapeCastable):
...
def format(self, obj, spec):
assert spec == ""
return Format("{{a: {}, b: {}, c: {}}}", obj.a, obj.b, obj.c)
with the added benefit that any MyStruct-shaped signal will automatically have per-field traces included in VCD output and per-field wires included in RTLIL output.
Implementations of format are added as appropriate to lib.enum and lib.data, making use of the above features.
Reference-level explanation
Three new classes are added:
amaranth.hdl.Format.Enum(value: Value, /, variants: EnumType | dict[int, str])amaranth.hdl.Format.Struct(value: Value, /, fields: dict[str, Format])amaranth.hdl.Format.Array(value: Value, /, fields: list[Format])
Instances of these classes can be used wherever a Format can be used:
- as an argument to
Print,Assert, ... - included within another
Formatvia substitution - returned from
ShapeCastable.format - as a field format within
Format.StructorFormat.Array
When used for formatting:
Format.Enumwill display as whichever string corresponds to current value ofvalue, or as"[unknown]"otherwise.Format.Structwill display as"{a: <formatted field a>, b: <formatted field b>}".Format.Arraywill display as"[<formatted field 0>, <formatted field 1>]"
Whenever a signal is created with a shape-castable as its shape, its format method is called with spec="" and the result is stashed away.
VCD output is done as follows:
- When a signal or field's format is just
Format("{}", value): value is emitted as a bitvector to VCD. - Otherwise, when a signal or field's custom format is not
Format.StructnorFormat.Array: the format is evaluated every time the value changes, and the result is emitted as a string to VCD - When the custom format is
Format.StructorFormat.Array:- the
valueas a whole is emitted as a bitvector - each field is emitted recursively (as a separate trace, perhaps with subtraces)
- the
RTLIL output is done as follows:
- When a signal or field's format is a plain
Formatand contains exactly one format specifier: a wire is created and assigned with the specifier's value. - When a signal or field's format is a plain
Formatthat doesn't conform to the above rule: no wire is created. - When a signal or field's format is a
Format.Enum: a wire is created and assigned with the format'svalue. RTLIL attributes are emitted on it, describing the enumeration values. - When a signal or field's format is a
Format.StructorFormat.Array: a wire is created and assigned with the format'svalue, representing the struct as a whole. For every field of the aggregate, the rules are applied recursively.
Drawbacks
More language complexity.
A shape-castable using Format.Struct to mark itself as aggregate is forced to use the fixed Format.Struct display when formatted.
Rationale and alternatives
This design ties together concerns of formatting with structural description. An alternative would be to have separate hooks for those, like the current _value_repr interface.
Prior art
None.
Unresolved questions
None.
Future possibilities
The current decoder interface on Signal could be deprecated and retired.