Skip to content

Commit

Permalink
RFC #65: Special formatting for structures and enums
Browse files Browse the repository at this point in the history
  • Loading branch information
whitequark authored Apr 8, 2024
2 parents ecfcd6f + e4af1ad commit 465657c
Showing 1 changed file with 165 additions and 0 deletions.
165 changes: 165 additions & 0 deletions text/0065-format-struct-enum.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
- Start Date: 2024-04-08
- RFC PR: [amaranth-lang/rfcs#65](https://github.com/amaranth-lang/rfcs/pull/65)
- Amaranth Issue: [amaranth-lang/amaranth#1293](https://github.com/amaranth-lang/amaranth/issues/1293)

# Special formatting for structures and enums

## Summary
[summary]: #summary

Extend `Format` machinery to support structured description of aggregate and enumerated data types.

## Motivation
[motivation]: #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](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.

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
[guide-level-explanation]: #guide-level-explanation

Shape-castables implementing enumerated types can return a `Format.Enum` instance instead of a `Format` from their `format` method:

```py
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:

```py
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`:

```py
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:

```py
class MyStruct(ShapeCastable):
...

def format(self, obj, spec):
assert spec == ""
return Format("{{a: {}, b: {}, c: {}}}", obj.a, obj.b, obj.c)
return Format.Struct(Value.cast(obj), {
# 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),
})
```

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
[reference-level-explanation]: #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 `Format` via substitution
- returned from `ShapeCastable.format`
- as a field format within `Format.Struct` or `Format.Array`

When used for formatting:

- `Format.Enum` will display as whichever string corresponds to current value of `value`, or as `"[unknown]"` otherwise.
- `Format.Struct` will display as `"{a: <formatted field a>, b: <formatted field b>}"`.
- `Format.Array` will 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:

1. When a signal or field's format is just `Format("{}", value)`: value is emitted as a bitvector to VCD.
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
3. When the custom format is `Format.Struct` or `Format.Array`:
- the `value` as a whole is emitted as a bitvector
- each field is emitted recursively (as a separate trace, perhaps with subtraces)

RTLIL output is done as follows:

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.
2. When a signal or field's format is a plain `Format` that doesn't conform to the above rule: no wire is created.
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.
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.

## Drawbacks
[drawbacks]: #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
[rationale-and-alternatives]: #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
[prior-art]: #prior-art

None.

## Unresolved questions
[unresolved-questions]: #unresolved-questions

None.

## Future possibilities
[future-possibilities]: #future-possibilities

The current `decoder` interface on `Signal` could be deprecated and retired.

0 comments on commit 465657c

Please sign in to comment.