#[instruction]Expand description
Processes #[instruction] attribute on both enum definitions and implementations.
§Enum definition
When applied to the enum definition, it will reorder variant fields and expose an enum with instructions as a dependency.
The fields are reordered as follows: rs1 > rs2 > others. This is helpful for
high-performance execution implementation. If rs1 or rs2 are not present, they will be added
automatically, and all implementations annotated with #[instruction] or
#[instruction_execution] will be automatically updated accordingly, but tests and other usages
will have to specify those fields explicitly.
More complex syntax is used when inheriting instructions:
#[instruction(
reorder = [C, Add],
ignore = [E],
inherit = [BaseInstruction],
reorder = [D, A],
if = [OptionalX]
)]
struct Extended<Reg> {
A(Reg),
B(Reg),
C(Reg),
#[instruction(
if = [OptionalY],
if = [OptionalZ1, OptionalZ2],
)]
D(Reg),
E(Reg),
}This will generate an enum with both BaseInstruction and Extended instructions, while also
reordering them according to the specified order. So the eventual enum will look like this:
struct Extended<Reg> {
C(Reg),
Add { rd: Reg, rs1: Reg, rs2: Reg },
// Any other instructions from `BaseInstruction` that were not mentioned explicitly
D(Reg),
A(Reg),
B(Reg),
}Note that all attribute parameters can be specified multiple times, and reordering can reference
any variant from both the BaseInstruction and Extended enums.
This, of course, only works when enums have compatible generics.
All instruction enums in the project must have unique names. Individual instructions can be repeated between inherited enums, but they must have the same exact variant definition and are assumed to be 100% compatible.
Here is how the attributes are processed:
- first, all own and inherited enum variants are collected into a set
- all reordered instructions are isolated from the rest to make sure they are not ignored
- then each attribute is processed in order of declaration
reorderindicated where the corresponding variant needs to be includedignoreremoved individual variants or the whole enum from a set mentioned earlier (but instructions that are “reordered” anywhere in the definition will remain). Ignored list may contain any known enum, including those that are not in the list of inherited enums.inheritincludes all remaining variants of the corresponding enum that were not explicitly reordered or ignored anywhere in the definition- own variants that were not explicitly reordered or ignored are placed at the end of the enum
reorder is a niche feature that physically moves the variants, allowing certain variants to be
next to each other for more efficient code generation (enum discriminant and execution code
locality).
inherit is used both for dependencies (Zve64x depends on Zicsr) and for direct inclusion
during composition (B contains Zba, Zbb and Zbs).
ignore can be used to create subsets of extensions (Zmmul is a multiply-only subset of M).
if on both enum and variant levels specifies soft optional dependencies on other instructions
or variants when this instruction is inherited further up the chain. Variants are always present
in the enum where they are defined, such that tests can be written against them without extra
effort. In this example above, all instructions require OptionalX enum or variant to be
included alongside Extended itself, while variant D specifically requires either OptionalY
or OptionalZ1 + OptionalZ2 to be present.
These if conditions allow modeling things like Zcf part of C extension only being
available when F extension is also available or Zcb’s c.sext.b only present when Zbb
extension is also available.
§Enum decoding implementation
For enum decoding implementation, the macro is applied to the implementation of Instruction
trait and affects its try_decode() method:
#[instruction]
impl<Reg> const Instruction for Rv64Instruction<Reg>
where
Reg: [const] Register<Type = u64>,
{
// ...
}try_decode() implementation will end up containing decoding logic for the full extended enum
as mentioned above. The two major restrictions are that return is not allowed in the
try_decode() method and enum variants must be constructed using Self::. The implementation
is quite fragile, so if you’re calling internal functions, they might have to be re-exported
since the macro will simply copy-paste the decoding logic as is. Similarly with missing imports,
etc. Compiler should be able to guide you through errors reasonably well.
§Enum display implementation
For enum display implementation, the macro is applied to the implementation of
core::fmt::Display trait and affects its fmt() method:
#[instruction]
impl<Reg> fmt::Display for Rv64Instruction<Reg>
where
Reg: fmt::Display + Copy,
{
// ...
}fmt() implementation will end up containing decoding logic for the full extended enum as
mentioned above. The three major restrictions are that an enum must be generic over Reg
register type, field types must have Copy bounds on them (like Reg in the example above),
and the method body must consist of a single match statement.
§process_instruction_macros()
What this macro “does” is impossible to do in Rust macros. So for completeness,
ab_riscv_macros::process_instruction_macros() must be called from build.rs in a
crate that uses #[instruction] macro to generate a bunch of special filed, which the macro
uses to replace the original code with. This is the only way to get the desired ergonomics
withing current constraints of what macros are allowed to do.
§[package.links]
package section of Cargo.toml must contain links = "crate-name" in order for metadata to
be successfully exported to dependent crates.