|
| 1 | +- Start Date: 2023-12-04 |
| 2 | +- RFC PR: [amaranth-lang/rfcs#35](https://github.com/amaranth-lang/rfcs/pull/35) |
| 3 | +- Amaranth Issue: [amaranth-lang/amaranth#0000](https://github.com/amaranth-lang/amaranth/issues/0000) |
| 4 | + |
| 5 | +# Add `ShapeLike`, `ValueLike` |
| 6 | + |
| 7 | +## Summary |
| 8 | +[summary]: #summary |
| 9 | + |
| 10 | +Two special classes are added to the language: `ShapeLike` and `ValueLike`. They cannot be constructed, but can be used to determine with `isinstance` and `issubclass` to determine whether something can be cast to `Shape` or a `Value`, respectively. |
| 11 | + |
| 12 | +## Motivation |
| 13 | +[motivation]: #motivation |
| 14 | + |
| 15 | +As it stands, we have multiple types of objects that can be used as shapes (`Shape`, `ShapeCastable`, `int`, `range`, `EnumMeta`) and values (`Value`, `ValueCastable`, `int`, `Enum`). These types have no common superclass, so there's no easy way to check if an object can be used as a shape or a value, save for actually calling `Shape.cast` or `Value.cast`. Introducing `ShapeLike` and `ValueLike` provides an idiomatic way to perform such a check. |
| 16 | + |
| 17 | +Additionally, when type annotations are in use, there is currently no simple type that can be used for an argument that takes an arbitrary shape- or value-castable object. These new classes provide such a simple type. |
| 18 | + |
| 19 | +## Guide-level explanation |
| 20 | +[guide-level-explanation]: #guide-level-explanation |
| 21 | + |
| 22 | +In Amaranth, multiple types of objects can be cast to shapes: |
| 23 | + |
| 24 | +- actual `Shape` objects |
| 25 | +- `ShapeCastable` objects |
| 26 | +- non-negative integers |
| 27 | +- `range` objects |
| 28 | +- `Enum` subclasses with const-castable values |
| 29 | + |
| 30 | +To check whether an object is of a type that can be cast to a shape, `isinstance(obj, ShapeLike)` can be used. To check whether a type can be, in general, cast to a shape, `issubclass(cls, ShapeLike)` can be used. |
| 31 | + |
| 32 | +Likewise, multiple types of objects can be cast to values: |
| 33 | + |
| 34 | +- actual `Value` objects |
| 35 | +- `ValueCastable` objects |
| 36 | +- integers |
| 37 | +- values of `Enum` subclasses with const-castable values |
| 38 | + |
| 39 | +To check whether an object is of a type that can be cast to a value, `isinstance(obj, ValueLike)` can be used. To check whether a type can be, in general, cast to a value, `issubclass(cls, ValueLike)` can be used. |
| 40 | + |
| 41 | +## Reference-level explanation |
| 42 | +[reference-level-explanation]: #reference-level-explanation |
| 43 | + |
| 44 | +A `ShapeLike` class is provided. It cannot be constructed, and can only be used with `isinstance` and `issubclass`, which are overriden by a custom metaclass. |
| 45 | + |
| 46 | +`issubclass(cls, ShapeLike)` returns `True` for: |
| 47 | + |
| 48 | +- `Shape` |
| 49 | +- `ShapeCastable` and its subclasses |
| 50 | +- `int` and its subclasses |
| 51 | +- `range` and its subclasses |
| 52 | +- `enum.EnumMeta` and its subclasses |
| 53 | + |
| 54 | +`isinstance(obj, ShapeLike)` returns `True` for: |
| 55 | + |
| 56 | +- instances of `Shape` |
| 57 | +- instances of `ShapeCastable` and its subclasses |
| 58 | +- non-negative `int` values (and `int` subclasses) |
| 59 | +- `enum.Enum` subclasses where every value is a `ValueLike` |
| 60 | + |
| 61 | +Similarly, a `ValueLike` class is provided. |
| 62 | + |
| 63 | +`issubclass(cls, ValueLike)` returns `True` for: |
| 64 | + |
| 65 | +- `Value` and its subclasses |
| 66 | +- `ValueCastable` and its subclasses |
| 67 | +- `int` and its subclasses |
| 68 | +- `enum.Enum` subclasses where every value is a `ValueLike` |
| 69 | + |
| 70 | +`isinstance(obj, ValueLike)` returns `True` iff `issubclass(type(obj), ValueLike)` returns `True`. |
| 71 | + |
| 72 | +## Drawbacks |
| 73 | +[drawbacks]: #drawbacks |
| 74 | + |
| 75 | +More moving parts in the language. |
| 76 | + |
| 77 | +`isinstance(obj, ShapeLike)` does not actually guarantee that `Shape.cast(obj)` will succeed — the instance check looks only at surface-level information, and an exception can still be thrown. `issubclass(cls, ShapeLike)` is, by necessity, even more inaccurate. |
| 78 | + |
| 79 | +## Rationale and alternatives |
| 80 | +[rationale-and-alternatives]: #rationale-and-alternatives |
| 81 | + |
| 82 | +There are many ways to implement the instance and subclass checks, some more precise (and complex) than others. The semantics described above are a compromise. |
| 83 | + |
| 84 | +For `isinstance`, a simple variant would be to just try `Shape.cast` or `Value.cast` and see if it raises an exception. However, this will sometimes result in `isinstance(MyShapeCastable(), ShapeLike)` returning `False`, which may be very unintuitive and hide bugs. |
| 85 | + |
| 86 | +The check for a valid shape-castable enum described above is an approximation — the actual logic used requires all values of an enum to be *const*-castable, not just value-castable. However, there is no way to check this without actually invoking `Value.cast` on the enum members. |
| 87 | + |
| 88 | +## Prior art |
| 89 | +[prior-art]: #prior-art |
| 90 | + |
| 91 | +Python has the concept of abstract base classes, such as `collections.abc.Sequence`, which can be used for subclass checking even if they are not actual superclasses of the types involved. `ShapeLike` and `ValueLike` are effectively ABCs, though they do not use the actual ABC machinery (due to having custom logic in instance checking). |
| 92 | + |
| 93 | +## Unresolved questions |
| 94 | +[unresolved-questions]: #unresolved-questions |
| 95 | + |
| 96 | +- Should the exact details of the instance and subclass checks be changed? |
| 97 | + |
| 98 | +## Future possibilities |
| 99 | +[future-possibilities]: #future-possibilities |
| 100 | + |
| 101 | +A similar ABC-like class has been proposed for `lib.wiring` interfaces. |
0 commit comments