Skip to main content

instruction

Attribute Macro instruction 

Source
#[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
    • reorder indicated where the corresponding variant needs to be included
    • ignore removed 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 satisfy if conditions of other instructions.
    • inherit includes 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 section of Cargo.toml must contain links = "crate-name" in order for metadata to be successfully exported to dependent crates.