|
| 1 | +- Start Date: 2024-04-08 |
| 2 | +- RFC PR: [amaranth-lang/rfcs#65](https://github.com/amaranth-lang/rfcs/pull/65) |
| 3 | +- Amaranth Issue: [amaranth-lang/amaranth#1293](https://github.com/amaranth-lang/amaranth/issues/1293) |
| 4 | + |
| 5 | +# Special formatting for structures and enums |
| 6 | + |
| 7 | +## Summary |
| 8 | +[summary]: #summary |
| 9 | + |
| 10 | +Extend `Format` machinery to support structured description of aggregate and enumerated data types. |
| 11 | + |
| 12 | +## Motivation |
| 13 | +[motivation]: #motivation |
| 14 | + |
| 15 | +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: |
| 16 | + |
| 17 | +- in pysim VCD output, it is useful to have each field as its own trace |
| 18 | +- in RTLIL output, it is useful to include a wire for each field (in particular, for CXXRTL simulation purposes) |
| 19 | + |
| 20 | +Likewise, `lib.enum.Enum`-typed signals benefit from being marked as enum-typed in RTLIL output. |
| 21 | + |
| 22 | +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](https://github.com/amaranth-lang/amaranth/issues/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. |
| 23 | + |
| 24 | +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`. |
| 25 | + |
| 26 | +## Guide-level explanation |
| 27 | +[guide-level-explanation]: #guide-level-explanation |
| 28 | + |
| 29 | +Shape-castables implementing enumerated types can return a `Format.Enum` instance instead of a `Format` from their `format` method: |
| 30 | + |
| 31 | +```py |
| 32 | +class MyEnum(ShapeCastable): |
| 33 | + ... |
| 34 | + |
| 35 | + def format(self, obj, spec): |
| 36 | + assert spec == "" |
| 37 | + return Format.Enum(Value.cast(obj), { |
| 38 | + 0: "ABC", |
| 39 | + 1: "DEF", |
| 40 | + 2: "GHI", |
| 41 | + }) |
| 42 | +``` |
| 43 | + |
| 44 | +For formatting purposes, this is equivalent to: |
| 45 | + |
| 46 | +```py |
| 47 | +class MyEnum(ShapeCastable): |
| 48 | + ... |
| 49 | + |
| 50 | + def format(self, obj, spec): |
| 51 | + assert spec == "" |
| 52 | + return Format( |
| 53 | + "{:s}", |
| 54 | + Mux(Value.cast(obj) == 0, int.from_bytes("ABC".encode(), "little"), |
| 55 | + Mux(Value.cast(obj) == 1, int.from_bytes("DEF".encode(), "little"), |
| 56 | + Mux(Value.cast(obj) == 2, int.from_bytes("GHI".encode(), "little"), |
| 57 | + int.from_bytes("[unknown]".encode(), "little") |
| 58 | + ))) |
| 59 | + ) |
| 60 | +``` |
| 61 | + |
| 62 | +with the added benefit that any `MyEnum`-shaped signals will be automatically marked as enums in RTLIL output. |
| 63 | + |
| 64 | +Likewise, shape-castables implementing aggregate types can return a `Format.Struct` instance instead of a `Format`: |
| 65 | + |
| 66 | +```py |
| 67 | +class MyStruct(ShapeCastable): |
| 68 | + ... |
| 69 | + |
| 70 | + def format(self, obj, spec): |
| 71 | + assert spec == "" |
| 72 | + return Format.Struct({ |
| 73 | + # Assume obj.a, obj.b, obj.c are accessors that return the struct fields as ValueLike. |
| 74 | + "a": Format("{}", obj.a), |
| 75 | + "b": Format("{}", obj.b), |
| 76 | + "c": Format("{}", obj.c), |
| 77 | + }) |
| 78 | +``` |
| 79 | + |
| 80 | +For formatting purposes, this is equivalent to: |
| 81 | + |
| 82 | +```py |
| 83 | +class MyStruct(ShapeCastable): |
| 84 | + ... |
| 85 | + |
| 86 | + def format(self, obj, spec): |
| 87 | + assert spec == "" |
| 88 | + return Format("{{a: {}, b: {}, c: {}}}", obj.a, obj.b, obj.c) |
| 89 | + return Format.Struct(Value.cast(obj), { |
| 90 | + # Assume obj.a, obj.b, obj.c are accessors that return the struct fields as ValueLike. |
| 91 | + "a": Format("{}", obj.a), |
| 92 | + "b": Format("{}", obj.b), |
| 93 | + "c": Format("{}", obj.c), |
| 94 | + }) |
| 95 | +``` |
| 96 | + |
| 97 | +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. |
| 98 | + |
| 99 | +Implementations of `format` are added as appropriate to `lib.enum` and `lib.data`, making use of the above features. |
| 100 | + |
| 101 | +## Reference-level explanation |
| 102 | +[reference-level-explanation]: #reference-level-explanation |
| 103 | + |
| 104 | +Three new classes are added: |
| 105 | + |
| 106 | +- `amaranth.hdl.Format.Enum(value: Value, /, variants: EnumType | dict[int, str])` |
| 107 | +- `amaranth.hdl.Format.Struct(value: Value, /, fields: dict[str, Format])` |
| 108 | +- `amaranth.hdl.Format.Array(value: Value, /, fields: list[Format])` |
| 109 | + |
| 110 | +Instances of these classes can be used wherever a `Format` can be used: |
| 111 | + |
| 112 | +- as an argument to `Print`, `Assert`, ... |
| 113 | +- included within another `Format` via substitution |
| 114 | +- returned from `ShapeCastable.format` |
| 115 | +- as a field format within `Format.Struct` or `Format.Array` |
| 116 | + |
| 117 | +When used for formatting: |
| 118 | + |
| 119 | +- `Format.Enum` will display as whichever string corresponds to current value of `value`, or as `"[unknown]"` otherwise. |
| 120 | +- `Format.Struct` will display as `"{a: <formatted field a>, b: <formatted field b>}"`. |
| 121 | +- `Format.Array` will display as `"[<formatted field 0>, <formatted field 1>]"` |
| 122 | + |
| 123 | +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. |
| 124 | + |
| 125 | +VCD output is done as follows: |
| 126 | + |
| 127 | +1. When a signal or field's format is just `Format("{}", value)`: value is emitted as a bitvector to VCD. |
| 128 | +2. Otherwise, when a signal or field's custom format is not `Format.Struct` nor `Format.Array`: the format is evaluated every time the value changes, and the result is emitted as a string to VCD |
| 129 | +3. When the custom format is `Format.Struct` or `Format.Array`: |
| 130 | + - the `value` as a whole is emitted as a bitvector |
| 131 | + - each field is emitted recursively (as a separate trace, perhaps with subtraces) |
| 132 | + |
| 133 | +RTLIL output is done as follows: |
| 134 | + |
| 135 | +1. When a signal or field's format is a plain `Format` and contains exactly one format specifier: a wire is created and assigned with the specifier's value. |
| 136 | +2. When a signal or field's format is a plain `Format` that doesn't conform to the above rule: no wire is created. |
| 137 | +3. When a signal or field's format is a `Format.Enum`: a wire is created and assigned with the format's `value`. RTLIL attributes are emitted on it, describing the enumeration values. |
| 138 | +4. When a signal or field's format is a `Format.Struct` or `Format.Array`: a wire is created and assigned with the format's `value`, representing the struct as a whole. For every field of the aggregate, the rules are applied recursively. |
| 139 | + |
| 140 | +## Drawbacks |
| 141 | +[drawbacks]: #drawbacks |
| 142 | + |
| 143 | +More language complexity. |
| 144 | + |
| 145 | +A shape-castable using `Format.Struct` to mark itself as aggregate is forced to use the fixed `Format.Struct` display when formatted. |
| 146 | + |
| 147 | +## Rationale and alternatives |
| 148 | +[rationale-and-alternatives]: #rationale-and-alternatives |
| 149 | + |
| 150 | +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. |
| 151 | + |
| 152 | +## Prior art |
| 153 | +[prior-art]: #prior-art |
| 154 | + |
| 155 | +None. |
| 156 | + |
| 157 | +## Unresolved questions |
| 158 | +[unresolved-questions]: #unresolved-questions |
| 159 | + |
| 160 | +None. |
| 161 | + |
| 162 | +## Future possibilities |
| 163 | +[future-possibilities]: #future-possibilities |
| 164 | + |
| 165 | +The current `decoder` interface on `Signal` could be deprecated and retired. |
0 commit comments