- Start Date: (fill me in with today's date, 2024-03-04)
- RFC PR: amaranth-lang/rfcs#51
- Amaranth Issue: amaranth-lang/amaranth#1187
Add ShapeCastable.from_bits and amaranth.lib.data.Const
Summary
A new user-definable method is added to ShapeCastable: from_bits(bits: int). It is used to convert constants into user-defined types, for example when a user-typed value is read in simulation.
A new type is added: amaranth.lib.data.Const. It is used as the return type of Layout.from_bits. Layout.const is also changed to return it instead of View.
Motivation
The current Amaranth model provides a useful set of primitives for manipulating opaque signals and values of custom types, via ShapeCastable and ValueCastable. However, there is little support for manipulating constants of custom types.
This is reasonable, as Amaranth mostly deals with elaborating a design hierarchy that operates on symbolic values that won't have actual values assigned before the circuit is operational. However, this need comes up in the Amaranth simulator, where the values have to be actually materialized.
Specifically, the simulator requires two operations to usefully support ValueCastable expressions:
- Conversion from user-facing constant value to raw bits, used when a
ValueCastableexperession is set to a value, or a cell ofShapeCastable-typedMemoryis written by a user process. This need is already covered by the existingShapeCastable.const. - Conversion from raw bits to user-facing constant value, used when a
ValueCastableexpression is requested, or a cell ofShapeCastable-typeMemoryis read by a user process. This need will be covered by the proposedfrom_bitsmethod.
For amaranth.lib.enum, the implementation of from_bits is trival: the value is looked up in the enum, and the enum value is returned. However, we have no good type to return from from_bits for amaranth.lib.data. Thus, a new lib.data.Const class is proposed.
Guide-level explanation
A new method is added to ShapeCastable: from_bits(bits: int), which, given a constant, interprets the constant as a value of the given ShapeCastable and returns an arbitrary Python value. The returned value must be round-trippable back to the original constant via ShapeCastable.const. This method will be used whenever there is a need to translate a raw bit pattern through a ShapeCastable, such as when reading a ShapeCastable-typed value in simulation.
>>> class Abc(amaranth.lib.enum.Enum, shape=unsigned(2)):
... X = 0
... Y = 1
... Z = 2
...
>>> Abc.from_bits(2)
<Abc.Z: 2>
To support this functionality in amaranth.lib.data, a new lib.data.Const class is added, which wraps a constant. Like View, this class provides attribute-based and indexing-based access to Layout fields. However, when a field is accessed, lib.data.Const returns a raw int value (for plain Shape-typed fields), or calls from_bits on the underlying bit pattern (for ShapeCastable fields). The class is also ValueCastable.
>>> class Def(amaranth.lib.data.Struct):
... a: Abc
... b: unsigned(2)
...
>>> a = Def.from_bits(9)
>>> a
Const(StructLayout({'a': <enum 'Abc'>, 'b': unsigned(2)}), 9)
>>> a.a
<Abc.Y: 1>
>>> a.b
2
Since it is a better fit, the return type of Layout.const is changed to lib.data.Const.
Reference-level explanation
A new method is added on ShapeCastable: from_bits(bits: int). The default implementation raises an error. A warning is raised when this method is not overriden in a subclass. The warning will become a hard error in Amaranth 0.6.
The argument to the method must be an int that is a valid constant value of the ShapeCastable's underlying shape.
The method must return something acceptable to the const method on the same ShapeCastable, and the value must round-trip, that is:
# assume `bits` is an int that is a valid const of `shape_castable.as_shape()`
value = shape_castable.const(shape_castable.from_bits(bits)).as_value()
assert isinstance(value, Const)
assert value.value == bits
It is recommended, but not strictly required, that the returned value is a ValueLike.
The sim.get method in the upcoming RFC 36 will call this method when called on a ValueCastable with a custom shape. The sim.set method will likewise call ShapeCastable.const.
A from_bits implementation is added onto amaranth.lib.enum.EnumMeta. If the given bit pattern has a corresponding enum value, the enum value is returned. Otherwise, the argument is returned unchanged.
A new class is added:
amaranth.lib.data.Const(layout, bits: int): bits must be a valid constant of the underlying shape; derives fromValueCastableshape(): returns the underlying shapeas_value(): returnsConst(bits, len(layout))__getitem__:- resolves the field like
View.__getitem__ - extracts the corresponding field from
bits - if the field is a
ShapeCastable, returns the result of calling itsfrom_bitswith the extracted value - otherwise, returns the raw field value as an
int
- resolves the field like
__getattr__: resolves the field likeView.__getattr__, then proceeds as above__eq__and__ne__:- if the other value is a
lib.data.Constwith the same layout, compares the underlyingbitsand returns a Pythonbool - if the other value is a
Viewwith the same layout, delegates toValue.__eq__(and thus returns a 1-bitValue) - otherwise, raises
TypeError
- if the other value is a
- all other operators: raises
TypeError(to prevent the defaultValueoperator implementations)
A from_bits implementation is added to Layout, Struct, and Union. It returns a lib.data.Const instance.
Layout.const, Struct.const, and Union.const are changed to return a lib.data.Const of matching layout. They are also changed to accept such constants if given, and return them unchanged.
Drawbacks
Adds an extra hook onto ShapeCastable that everyone must implement. However, it is not clear how this could be avoided.
When a Struct (or Union) is involved, isinstance(sim.get(my_struct_signal), MyStruct) will be false. This could be avoided by horrifying metaclass hacks, but it's unclear if it's a good idea.
The change to Layout.const etc is not fully backwards-compatible. It is believed that this is not much of an issue, since its main user is Signal init= argument, for which it doesn't matter.
Rationale and alternatives
The proposed design of from_bits is essentially the minimal interface needed to support the needs of simulation. Additionally, it is designed so that it can be called essentially recursively by aggregate ShapeCastables, such as Layout.
An alternative (proposed originally by RFC 36) would be to let ValueCastable implement get/set methods for simulation. However, this has obvious drawbacks:
- two methods needed per
ShapeCastable - doesn't work for reading or writing memories, as they don't have underlying signals
- when the value is an aggregate, there's no obvious way to delegate to construct the values of fields
The lib.data.Const design closely follows View. An alternative would be for from_bits to return a list or dict of fields (recursively processed by from_bits), paralleling the accepted argument format of const. However, this results in inferior user experience.
lib.data.Const conceivably could be made mutable, providing __setattr__ / __setitem__ in the future. However, Amaranth tries to avoid mutable data types, and it was felt that keeping it explicitly immutable was the best way forward. Its main use is believed to be simulation, and in simulation one can just call sim.set() on the View field directly if needed. Outside of simulation, we can add a .like()-like constructor for lib.data.Const to modify some fields of the aggregate.
Prior art
The author believes user-defined types usually come in three parts:
- a
ShapeCastable, ie. the type itself - a
ValueCastableproxying arbitraryValues - a class representing constant values of the type
For enums, these correspond to EnumMeta, EnumView, and the Enum itself. For plain values, they correspond to Shape, Value, and int. The proposed RFC 41 likewise includes fixed.Shape, fixed.Value, and fixed.Const.
The first half of this RFC provides a generic way to construct the third part from a bit pattern. The second half of this RFC fills the missing third part for amaranth.lib.data.
Unresolved questions
None.
Future possibilities
The lib.data.Const class can be expanded in functionality:
- alternative constructors could be added, to let user create and manipulate const struct-typed values