- Start Date: 2023-02-07
- RFC PR: amaranth-lang/rfcs#4
- Amaranth Issue: amaranth-lang/amaranth#755
Define a subset of expressions that are "const-castable" and accept them in contexts where currently only integers and enumerations with integer values are accepted.
In certain contexts, Amaranth requires a constant to be used. These contexts are:
Value.matches(...), and the value of an enumeration member that is being cast to a value.
Currently, only integers and enumeration members with integer values are considered constants. However, this is too limiting. For example, when developing a CPU, one might want to define control signals for several functional units and combine them into instructions, or conversely, match an instruction against a combination of control signals:
class Func(Enum): ADD = 0 SUB = 1 class Src(Enum): MEM = 0 REG = 1 class Instr(Enum): ADD = Cat(Func.ADD, Src.MEM) ADDI = Cat(Func.ADD, Src.REG) ... with m.Switch(instr): with m.Case(Cat(Func.ADD, Src.MEM)): ...
Currently, all of these cases would produce syntax errors.
There is a private
Value._as_const method. It is not used internally, however Amaranth developers have started using it due to unmet needs. Removing it without providing a replacement would be disruptive, and will result in downstream codebases defining their own equivalent.
In any context where a constant is accepted (
Value.matches(...), and the value of an enumeration member), a "const-castable" expression can be used. The following expressions are const-castable:
Catwhere all operands are const-castable;
- instance of a
Enumsubclass where the value is const-castable.
A const-castable expression can be converted to a
>>> Const.cast(1) (const 1'd1) >>> Const.cast(Cat(1, 0, 1)) (const 3'd5) >>> Const.cast(Cat(Func.ADD, Src.REG)) (const 2'd2)
Const.cast static method casts its argument to a value and examines it. If it is a
Const, it is returned. If it is a const-castable expression, the operands are recursively cast to constants, and the expression is evaluated.
The list of const-castable expressions is:
m.Case(...) (through the
Switch() constructor) and
Value.matches(...) methods accept two kinds of operands: const-castable expressions, or a string containing a bit pattern (a sequence of
- meaning a wildcard).
Shape.cast method accepts enumerations where all members have const-castable expressions as their values. The shape of an enumeration is a shape with the smallest width that can represent the value of any enumeration member.
RFC 3: The
EnumMeta.__new__ method accepts enumerations where all members have const-castable expressions as their values. The values of members are the user-provided const-castable expressions, and not the result of casting them to a constant.
- A new language-level concept makes it harder to learn the language.
- Most developers already have an intuitive understanding of which expressions are const-castable.
Const.castshadows an existing
- No one is calling
Const.casthas a compatible interface (it returns a
Value) and performs a similar function (it calls
Value.castfirst). However, it's not Liskov-compatible.
- No one is calling
Rationale and alternatives
- Do not add this functionality. Developers will define their own const-casting functions, continue to rely on the undocumented and private
._as_const()method, or use other workarounds.
._as_const()public (i.e. rename it to
- Add a new
Const.castmethod (this option).
Alternatives (2) and (3) both introduce a new language-level concept, the only difference is in the interface that is used to access it.
Alternative (3) fits the language better:
Value.cast takes something value-castable and returns a
Shape.cast takes something shape-castable and returns a
Const.cast is a logical addition in that it takes something const-castable and returns a
Rust and C++ provide functionality (
const fn and
constexpr respectively) for performing computation at compile time, restricted to a strict subset of the full language. In particular, it can be used to initialize constants, which makes it similar to the functionality proposed here.
One challenge these languages face is the question of how large the subset should be. Rust in particular started off heavily restricting
const fn, where it did not have any control flow. The functionality was gradually introduced later as needed.
Expanding the set of const-castable expressions to include arbitrary arithmetic operations. This RFC limits it to the most requested expression,
Cat. This simplifies implementation and reduces the likelihood of introducing bugs in the constant evaluation code, most of which would be almost never used.