#[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]
)]
enum Extended<Hart>
where
Hart: HartConfig,
{
A(Hart::Reg),
B(Hart::Reg),
C(Hart::Reg),
#[instruction(
if = [OptionalY],
if = [OptionalZ1, OptionalZ2],
)]
D(Hart::Reg),
E(Hart::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:
enum Extended<Hart>
where
Hart: HartConfig,
{
C(Hart::Reg),
Add { rd: Hart::Reg, rs1: Hart::Reg, rs2: Hart::Reg },
// Any other instructions from `BaseInstruction` that were not mentioned explicitly
D(Hart::Reg),
A(Hart::Reg),
B(Hart::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 are
generic over hart configuration Hart (see HartConfig), and implementations have an
additional Reg generic parameter for its register type (constrained with
Hart: HartConfig<Reg = Reg>).
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. Ignored instructions don’t satisfyifconditions of other instructions.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 exclude individual instructions or whole enums from a custom instruction
set (like ecall from an instruction set that doesn’t support system calls). An enum ignored as
a whole, as well as enums that inherit it, are not implemented extensions of the instruction set
(see Instruction::IMPLEMENTED_EXTENSIONS), while ignoring individual instructions doesn’t
change that, unless none of the enum’s own instructions are left. It should not be used for
modeling relationships between extensions though: an extension that is a subset of another
extension according to the specification is inherited by that extension (M inherits Zmmul),
while instructions shared by extensions that are not subsets of each other are extracted into a
separate enum, which both extensions inherit (Zbb and Zbkb both inherit
Rv64ZbbZbkbSharedInstruction).
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. An enum in if condition is satisfied when it is implemented, the
same way as in Instruction::IMPLEMENTED_EXTENSIONS.
§Enum decoding implementation
For enum decoding implementation, the macro is applied to the implementation of Instruction
trait and affects its try_decode() method:
#[instruction]
const impl<Reg, Hart> Instruction for Rv64Instruction<Hart>
where
Reg: [const] Register<Type = u64>,
Hart: [const] HartConfig<Reg = Reg>,
{
// ...
}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.
Construction of variants that are not present in the instruction set (like ignored ones) is
replaced with None?. This also allows decoding to construct variants of other enums when they
are present, like Zbkb decoding packw rd, rs1, x0 as Zbb’s zext.h.
The macro also generates IMPLEMENTED_EXTENSIONS constant (see
Instruction::IMPLEMENTED_EXTENSIONS), while OWN_ISA_EXTENSIONS with ISA extensions of the
enum itself must be specified explicitly, like Ok(&[IsaExtension::new("m", 2, 0)]) for M.
ImplementedExtension must be in scope for this to work.
§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, Hart> fmt::Display for Rv64Instruction<Hart>
where
Reg: fmt::Display + Copy,
Hart: HartConfig<Reg = Reg>,
{
// ...
}fmt() implementation will end up containing decoding logic for the full extended enum as
mentioned above. The two major restrictions are that 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.
process_instruction_macros() finds annotated items anywhere in the crate’s Rust files,
including inline modules, and attributes may be path-qualified like
#[ab_riscv_macros::instruction]. Attributes must be written in the source code at the start
of a line (indentation is fine), ones produced by other macros (like cfg_attr) are not seen.
§[package.links]
package section of Cargo.toml must contain links = "crate-name" in order for metadata to
be successfully exported to dependent crates.