Skip to content

Commit

Permalink
Macro to implement riscv-pac traits
Browse files Browse the repository at this point in the history
  • Loading branch information
romancardenas committed Feb 22, 2024
1 parent 3608f39 commit 1263126
Show file tree
Hide file tree
Showing 19 changed files with 389 additions and 253 deletions.
8 changes: 7 additions & 1 deletion riscv-pac/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,10 @@ targets = [
]

[dependencies]
riscv-pac-macros = { path = "macros", version = "0.1.0" }
riscv-pac-macros = { path = "macros", version = "0.1.0", optional = true }

[features]
default = ["riscv-pac-macros"]

[dev-dependencies]
trybuild = "1.0"
2 changes: 1 addition & 1 deletion riscv-pac/macros/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,4 @@ proc-macro = true
[dependencies]
proc-macro2 = "1.0"
quote = "1.0"
syn = { version = "2.0", default-features = false, features = ["derive", "parsing", "proc-macro"]}
syn = { version = "2.0" }
226 changes: 136 additions & 90 deletions riscv-pac/macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,66 @@ extern crate syn;
use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use quote::quote;
use std::{collections::HashMap, convert::TryFrom, ops::Range, str::FromStr};
use syn::{parse_macro_input, Data, DeriveInput, Error, Ident};
use std::{collections::HashMap, ops::Range, str::FromStr};
use syn::{parse_macro_input, Data, DeriveInput, Ident};

struct PacNumberEnum {
name: Ident,
valid_ranges: Vec<Range<usize>>,
}

impl PacNumberEnum {
fn new(input: &DeriveInput) -> Self {
let variants = match &input.data {
Data::Enum(data) => &data.variants,
_ => panic!("Input is not an enum"),
};

// Collect the variants and their associated number discriminants
let mut var_map = HashMap::new();
let mut numbers = Vec::new();
for variant in variants {
let ident = &variant.ident;
let value = match &variant.discriminant {
Some(d) => match &d.1 {
syn::Expr::Lit(expr_lit) => match &expr_lit.lit {
syn::Lit::Int(lit_int) => match lit_int.base10_parse::<usize>() {
Ok(num) => num,
Err(_) => panic!("All variant discriminants must be unsigned integers"),
},
_ => panic!("All variant discriminants must be unsigned integers"),
},
_ => panic!("All variant discriminants must be unsigned integers"),
},
_ => panic!("Variant must have a discriminant"),
};
// check for duplicate discriminant values
var_map.insert(value, ident);
numbers.push(value);
}

// sort the number discriminants and generate a list of valid ranges
numbers.sort_unstable();
let mut valid_ranges = Vec::new();
let mut start = numbers[0];
let mut end = start;
for &number in &numbers[1..] {
if number == end + 1 {
end = number;
} else {
valid_ranges.push(start..end + 1);
start = number;
end = start;
}
}
valid_ranges.push(start..end + 1);

Self {
name: input.ident.clone(),
valid_ranges,
}
}

fn valid_condition(&self) -> TokenStream2 {
let mut arms = Vec::new();
for range in &self.valid_ranges {
Expand Down Expand Up @@ -45,7 +96,7 @@ impl PacNumberEnum {
let const_name = TokenStream2::from_str(const_name).unwrap();

quote! {
unsafe impl #trait_name for #name {
unsafe impl riscv_pac::#trait_name for #name {
const #const_name: #num_type = #max_discriminant;

#[inline]
Expand All @@ -67,93 +118,88 @@ impl PacNumberEnum {
}
}

impl TryFrom<DeriveInput> for PacNumberEnum {
type Error = Error;

fn try_from(input: DeriveInput) -> Result<Self, Self::Error> {
let variants = match &input.data {
Data::Enum(data) => &data.variants,
_ => panic!("Input is not an enum"),
};

// Collect the variants and their associated number discriminants
let mut var_map = HashMap::new();
let mut numbers = Vec::new();
for variant in variants {
let ident = &variant.ident;
let value = match &variant.discriminant {
Some(d) => match &d.1 {
syn::Expr::Lit(expr_lit) => match &expr_lit.lit {
syn::Lit::Int(lit_int) => match lit_int.base10_parse::<usize>() {
Ok(num) => num,
Err(_) => panic!("All variant discriminants must be unsigned integers"),
},
_ => panic!("All variant discriminants must be unsigned integers"),
},
_ => panic!("All variant discriminants must be unsigned integers"),
},
_ => panic!("Variant must have a discriminant"),
};
// check for duplicate discriminant values
var_map.insert(value, ident);
numbers.push(value);
}

// sort the number discriminants and generate a list of valid ranges
numbers.sort_unstable();
let mut valid_ranges = Vec::new();
let mut start = numbers[0];
let mut end = start;
for &number in &numbers[1..] {
if number == end + 1 {
end = number;
} else {
valid_ranges.push(start..end + 1);
start = number;
end = start;
}
}
valid_ranges.push(start..end + 1);

Ok(PacNumberEnum {
name: input.ident.clone(),
valid_ranges,
})
/// Attribute-like macro that implements the traits of the `riscv-pac` crate for a given enum.
///
/// As these traits are unsafe, the macro must be called with the `unsafe` keyword followed by the trait name.
/// In this way, we warn callers that they must comply with the requirements of the trait.
///
/// The trait name must be one of `ExceptionNumber`, `InterruptNumber`, `PriorityNumber`, or `HartIdNumber`.
/// Marker traits `CoreInterruptNumber` and `ExternalInterruptNumber` cannot be implemented using this macro.
///
/// # Note
///
/// To implement number-to-enum operation, the macro works with ranges of valid discriminant numbers.
/// If the number is within any of the valid ranges, the number is transmuted to the enum variant.
/// In this way, the macro achieves better performance for enums with a large number of consecutive variants.
/// Thus, the enum must comply with the following requirements:
///
/// - All the enum variants must have a valid discriminant number (i.e., a number that is within the valid range of the enum).
/// - For the `ExceptionNumber`, `InterruptNumber`, and `HartIdNumber` traits, the enum must be annotated as `#[repr(u16)]`
/// - For the `PriorityNumber` trait, the enum must be annotated as `#[repr(u8)]`
///
/// If the enum does not meet these requirements, you will have to implement the traits manually (e.g., `riscv::mcause::Interrupt`).
/// For enums with a small number of consecutive variants, it might be better to implement the traits manually.
///
/// # Safety
///
/// The struct to be implemented must comply with the requirements of the specified trait.
///
/// # Example
///
/// ```rust
/// use riscv_pac::*;
///
/// #[repr(u16)]
/// #[pac_enum(unsafe ExceptionNumber)]
/// #[derive(Clone, Copy, Debug, Eq, PartialEq)]
/// enum Exception {
/// E1 = 1,
/// E3 = 3,
/// }
///
/// fn main() {
/// assert_eq!(Exception::E1.number(), 1);
/// assert_eq!(Exception::E3.number(), 3);
///
/// assert_eq!(Exception::from_number(1), Ok(Exception::E1));
/// assert_eq!(Exception::from_number(2), Err(2));
/// assert_eq!(Exception::from_number(3), Ok(Exception::E3));
///
/// assert_eq!(Exception::MAX_EXCEPTION_NUMBER, 3);
/// }
///```
#[proc_macro_attribute]
pub fn pac_enum(attr: TokenStream, item: TokenStream) -> TokenStream {
let input = parse_macro_input!(item as DeriveInput);
let pac_enum = PacNumberEnum::new(&input);

// attr should be unsafe ExceptionNumber, unsafe InterruptNumber, unsafe PriorityNumber, or unsafe HartIdNumber
// assert that attribute starts with the unsafe token. If not, raise a panic error
let attr = attr.to_string();
// split string into words and check if the first word is "unsafe"
let attrs = attr.split_whitespace().collect::<Vec<&str>>();
if attrs.is_empty() {
panic!("Attribute is empty. Expected: 'riscv_pac::pac_enum(unsafe <PacTraitToImplement>)'");
}
if attrs.len() > 2 {
panic!(
"Wrong attribute format. Expected: 'riscv_pac::pac_enum(unsafe <PacTraitToImplement>)'"
);
}
if attrs[0] != "unsafe" {
panic!("Attribute does not start with 'unsafe'. Expected: 'riscv_pac::pac_enum(unsafe <PacTraitToImplement>)'");
}
}

#[proc_macro_derive(ExceptionNumber)]
pub fn exception_number_derive(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let pac_enum = PacNumberEnum::try_from(input).unwrap();
pac_enum
.quote("ExceptionNumber", "u16", "MAX_EXCEPTION_NUMBER")
.into()
}

#[proc_macro_derive(InterruptNumber)]
pub fn interrupt_number_derive(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let pac_enum = PacNumberEnum::try_from(input).unwrap();
pac_enum
.quote("InterruptNumber", "u16", "MAX_INTERRUPT_NUMBER")
.into()
}

#[proc_macro_derive(PriorityNumber)]
pub fn priority_number_derive(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let pac_enum = PacNumberEnum::try_from(input).unwrap();
pac_enum
.quote("PriorityNumber", "u8", "MAX_PRIORITY_NUMBER")
.into()
}

#[proc_macro_derive(HartIdNumber)]
pub fn hart_id_number_derive(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let pac_enum = PacNumberEnum::try_from(input).unwrap();
pac_enum
.quote("HartIdNumber", "u16", "MAX_HART_ID_NUMBER")
.into()
let trait_impl = match attrs[1] {
"ExceptionNumber" => pac_enum.quote("ExceptionNumber", "u16", "MAX_EXCEPTION_NUMBER"),
"InterruptNumber" => pac_enum.quote("InterruptNumber", "u16", "MAX_INTERRUPT_NUMBER"),
"PriorityNumber" => pac_enum.quote("PriorityNumber", "u8", "MAX_PRIORITY_NUMBER"),
"HartIdNumber" => pac_enum.quote("HartIdNumber", "u16", "MAX_HART_ID_NUMBER"),
_ => panic!("Unknown trait '{}'. Expected: 'ExceptionNumber', 'InterruptNumber', 'PriorityNumber', or 'HartIdNumber'", attrs[1]),
};
quote! {
#input
#trait_impl
}
.into()
}
Loading

0 comments on commit 1263126

Please sign in to comment.