use proc_macro2::TokenStream;
use quote::{format_ident, quote};
use syn::{
Attribute, Expr, Ident, Item, LitBool, LitInt, Path, Signature, Token, Type,
parse::{Parse, ParseStream},
punctuated::Punctuated,
spanned::Spanned,
};
struct Rebind {
requirement: Ident,
target: Ident,
}
impl Parse for Rebind {
fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
let requirement = input.parse()?;
input.parse::<Token![=]>()?;
Ok(Self {
requirement,
target: input.parse()?,
})
}
}
struct ConfigValue {
name: Ident,
ty: Type,
value: Expr,
}
impl Parse for ConfigValue {
fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
let name = input.parse()?;
input.parse::<Token![:]>()?;
let ty = input.parse()?;
input.parse::<Token![=]>()?;
Ok(Self {
name,
ty,
value: input.parse()?,
})
}
}
#[derive(Default)]
struct TaskAttribute {
from: Option<Path>,
priority: Option<LitInt>,
binds: Option<Path>,
local: Vec<Rebind>,
shared: Vec<Rebind>,
config: Vec<ConfigValue>,
spawn: Vec<Rebind>,
}
fn bracketed_list<T: Parse>(input: ParseStream<'_>) -> syn::Result<Vec<T>> {
let content;
syn::bracketed!(content in input);
Ok(Punctuated::<T, Token![,]>::parse_terminated(&content)?
.into_iter()
.collect())
}
impl Parse for TaskAttribute {
fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
let mut parsed = Self::default();
while !input.is_empty() {
let key: Ident = input.parse()?;
input.parse::<Token![=]>()?;
match key.to_string().as_str() {
"from" => parsed.from = Some(input.parse()?),
"priority" => parsed.priority = Some(input.parse()?),
"binds" => parsed.binds = Some(input.parse()?),
"local" => parsed.local = bracketed_list(input)?,
"shared" => parsed.shared = bracketed_list(input)?,
"config" => parsed.config = bracketed_list(input)?,
"spawn" => parsed.spawn = bracketed_list(input)?,
other => {
return Err(syn::Error::new(
key.span(),
format!("unknown task option `{other}`"),
));
}
}
if input.peek(Token![,]) {
input.parse::<Token![,]>()?;
}
}
Ok(parsed)
}
}
struct Instance {
attribute: TaskAttribute,
signature: Signature,
}
enum Element {
Verbatim(Item),
Instance(Instance),
}
fn parse_instance(input: ParseStream<'_>, attribute: TaskAttribute) -> syn::Result<Instance> {
let signature: Signature = input.parse()?;
input.parse::<Token![;]>()?;
Ok(Instance {
attribute,
signature,
})
}
fn instance_attribute(attributes: &[Attribute]) -> Option<TaskAttribute> {
attributes
.iter()
.find(|attribute| attribute.path().is_ident("task"))
.and_then(|attribute| attribute.parse_args_with(TaskAttribute::parse).ok())
.filter(|parsed| parsed.from.is_some())
}
const MONOTONIC_SLOT: &str = "__FfMonotonic";
const NO_MONOTONIC: &str = "NoMonotonicDeclared";
pub struct App {
device: Path,
dispatchers: Vec<Ident>,
peripherals: Option<LitBool>,
monotonic: Option<Ident>,
elements: Vec<Element>,
}
impl Parse for App {
fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
let mut device: Option<Path> = None;
let mut dispatchers = Vec::new();
let mut peripherals = None;
let mut monotonic = None;
let mut seen: Vec<String> = Vec::new();
while input.peek(Ident) && input.peek2(Token![=]) {
let key: Ident = input.parse()?;
let name = key.to_string();
if seen.contains(&name) {
return Err(syn::Error::new(
key.span(),
format!("`{name}` appears more than once"),
));
}
seen.push(name.clone());
input.parse::<Token![=]>()?;
match name.as_str() {
"device" => device = Some(input.parse()?),
"dispatchers" => dispatchers = bracketed_list(input)?,
"peripherals" => peripherals = Some(input.parse()?),
"monotonic" => monotonic = Some(input.parse()?),
other => {
return Err(syn::Error::new(
key.span(),
format!(
"unknown argument `{other}`; expected `device`, \
`dispatchers`, `peripherals` or `monotonic`"
),
));
}
}
if input.peek(Token![,]) {
input.parse::<Token![,]>()?;
}
}
let device = device.ok_or_else(|| {
syn::Error::new(
proc_macro2::Span::call_site(),
"an application needs `device = <path to the PAC>`",
)
})?;
let mut elements = Vec::new();
while !input.is_empty() {
let attributes = input.call(Attribute::parse_outer)?;
if let Some(attribute) = instance_attribute(&attributes) {
elements.push(Element::Instance(parse_instance(input, attribute)?));
} else {
let mut item: Item = input.parse()?;
prepend_attributes(&mut item, attributes)?;
elements.push(Element::Verbatim(item));
}
}
Ok(Self {
device,
dispatchers,
peripherals,
monotonic,
elements,
})
}
}
fn prepend_attributes(item: &mut Item, mut attributes: Vec<Attribute>) -> syn::Result<()> {
let existing = match item {
Item::Use(item) => &mut item.attrs,
Item::Struct(item) => &mut item.attrs,
Item::Fn(item) => &mut item.attrs,
Item::Impl(item) => &mut item.attrs,
Item::Const(item) => &mut item.attrs,
Item::Type(item) => &mut item.attrs,
Item::Mod(item) => &mut item.attrs,
other => {
return Err(syn::Error::new(
other.span(),
"unsupported item in a composition",
));
}
};
attributes.append(existing);
*existing = attributes;
Ok(())
}
fn render_instance(instance: &Instance) -> syn::Result<TokenStream> {
let Instance {
attribute,
signature,
} = instance;
let name = &signature.ident;
let from = attribute
.from
.as_ref()
.expect("instances are recognised by their `from`");
let local_fields = attribute.local.iter().map(|rebind| {
let (requirement, resource) = (&rebind.requirement, &rebind.target);
quote!(#requirement: cx.local.#resource)
});
let shared = &attribute.shared;
let shared_fields = shared.iter().map(|rebind| {
let (requirement, resource) = (&rebind.requirement, &rebind.target);
quote!(#requirement: cx.shared.#resource)
});
let local_claims = attribute.local.iter().map(|rebind| &rebind.target);
let shared_claims = shared.iter().map(|rebind| &rebind.target);
let local_attribute =
(!attribute.local.is_empty()).then(|| quote!(, local = [#(#local_claims),*]));
let shared_attribute = (!shared.is_empty()).then(|| quote!(, shared = [#(#shared_claims),*]));
let spawn_fields = attribute.spawn.iter().map(|rebind| {
let (alias, target) = (&rebind.requirement, &rebind.target);
quote!(#alias: |value| #target::spawn(value))
});
let config_type = format_ident!("__FfConfig{}", name.to_string().to_uppercase());
let monotonic_slot = format_ident!("{MONOTONIC_SLOT}");
let config_consts = attribute.config.iter().map(|value| {
let (name, ty, value) = (&value.name, &value.ty, &value.value);
let name = format_ident!("{}", name.to_string().to_uppercase(), span = name.span());
quote!(const #name: #ty = #value;)
});
let priority = attribute
.priority
.clone()
.unwrap_or_else(|| LitInt::new("1", name.span()));
if let (Some(asyncness), Some(interrupt)) = (&signature.asyncness, &attribute.binds) {
let interrupt = interrupt
.segments
.last()
.map(|segment| segment.ident.to_string())
.unwrap_or_default();
return Err(syn::Error::new(
asyncness.span,
format!(
"an `async` task cannot bind an interrupt: `binds = {interrupt}` makes \
`{name}` a hardware task. Drop `async` and select a hardware definition, \
or remove `binds` to leave it a software task"
),
));
}
let binds = attribute
.binds
.as_ref()
.map(|interrupt| quote!(binds = #interrupt,));
let inputs = signature.inputs.iter().skip(1).collect::<Vec<_>>();
let forwarded = signature
.inputs
.iter()
.skip(1)
.map(|argument| match argument {
syn::FnArg::Typed(typed) => Ok(typed.pat.clone()),
other => Err(syn::Error::new(other.span(), "unsupported task input")),
})
.collect::<syn::Result<Vec<_>>>()?;
let context = signature
.inputs
.first()
.ok_or_else(|| syn::Error::new(signature.span(), "task needs a context parameter"))?;
let asyncness = &signature.asyncness;
let awaiting = signature.asyncness.map(|_| quote!(.await));
let output = &signature.output;
Ok(quote! {
struct #config_type;
impl #from::Config for #config_type {
#(#config_consts)*
}
#[task(#binds priority = #priority #local_attribute #shared_attribute)]
#asyncness fn #name(#context #(, #inputs)*) #output {
#from(
#from::Context {
local: #from::Local { #(#local_fields),* },
shared: #from::Shared { #(#shared_fields),* },
spawn: #from::Spawn { #(#spawn_fields),* },
config: ::core::marker::PhantomData::<#config_type>,
monotonic: ::core::marker::PhantomData::<#monotonic_slot>,
}
#(, #forwarded)*
) #awaiting
}
})
}
pub fn expand(application: App) -> syn::Result<TokenStream> {
let App {
device,
dispatchers,
peripherals,
monotonic,
elements,
} = application;
let mut body = Vec::new();
for element in &elements {
body.push(match element {
Element::Verbatim(item) => quote!(#item),
Element::Instance(instance) => render_instance(instance)?,
});
}
let peripherals = peripherals.map(|value| quote!(, peripherals = #value));
let dispatchers =
(!dispatchers.is_empty()).then(|| quote!(, dispatchers = [#(#dispatchers),*]));
let slot = format_ident!("{MONOTONIC_SLOT}");
let monotonic = match monotonic {
Some(name) => quote! {
use super::#name;
type #slot = #name;
},
None => {
let absent = format_ident!("{NO_MONOTONIC}");
quote! {
enum #absent {}
type #slot = #absent;
}
}
};
Ok(quote! {
#[rtic::app(device = #device #dispatchers #peripherals)]
mod app {
#monotonic
#(#body)*
}
})
}
#[cfg(test)]
mod tests {
use super::*;
fn expand_source(tasks: &str) -> syn::Result<TokenStream> {
let source =
format!("device = chip::pac, dispatchers = [SPARE], monotonic = Mono,\n{tasks}");
expand(syn::parse_str::<App>(&source)?)
}
#[test]
fn rejects_an_async_task_bound_to_an_interrupt() {
let error =
expand_source("#[task(from = blink, binds = TIM2)] async fn led(cx: led::Context);")
.expect_err("an async task binding an interrupt must be rejected")
.to_string();
assert!(error.contains("cannot bind an interrupt"), "{error}");
assert!(
error.contains("TIM2"),
"the interrupt must be named: {error}"
);
assert!(error.contains("led"), "the instance must be named: {error}");
}
#[test]
fn accepts_a_synchronous_task_bound_to_an_interrupt() {
expand_source("#[task(from = on_tick, binds = TIM2)] fn tick(cx: tick::Context);")
.expect("a synchronous bound task is a hardware task");
}
}
#[cfg(test)]
mod header {
use super::*;
fn parse(header: &str) -> syn::Result<App> {
syn::parse_str::<App>(header)
}
fn rendered(header: &str) -> String {
match parse(header) {
Ok(application) => expand(application).unwrap().to_string(),
Err(error) => panic!("`{header}` must parse: {error}"),
}
}
fn refused(header: &str) -> String {
match parse(header) {
Ok(_) => panic!("`{header}` must not parse"),
Err(error) => error.to_string(),
}
}
#[test]
fn arguments_may_appear_in_any_order() {
assert!(
parse("dispatchers = [A], monotonic = Mono, device = chip::pac,").is_ok(),
"order must not matter, as in RTIC"
);
}
#[test]
fn dispatchers_are_optional_and_omitted_when_empty() {
let output = rendered("device = chip::pac,");
assert!(!output.contains("dispatchers"), "{output}");
}
#[test]
fn peripherals_is_passed_through_only_when_stated() {
assert!(!rendered("device = chip::pac,").contains("peripherals"));
let stated = rendered("device = chip::pac, peripherals = false,");
assert!(stated.contains("peripherals = false"), "{stated}");
}
#[test]
fn device_is_required_and_says_so() {
let error = refused("dispatchers = [A],");
assert!(error.contains("device"), "{error}");
}
#[test]
fn a_repeated_argument_is_named() {
let error = refused("device = a::pac, device = b::pac,");
assert!(error.contains("more than once"), "{error}");
assert!(error.contains("device"), "{error}");
}
#[test]
fn an_unknown_argument_lists_the_real_ones() {
let error = refused("device = chip::pac, monotonic_hz = 1000,");
assert!(error.contains("monotonic_hz"), "{error}");
assert!(error.contains("dispatchers"), "{error}");
assert!(error.contains("monotonic"), "{error}");
}
#[test]
fn an_application_without_a_monotonic_still_expands() {
let output = rendered("device = chip::pac,");
assert!(output.contains(NO_MONOTONIC), "{output}");
}
#[test]
fn a_declared_monotonic_is_imported_under_its_own_name() {
let output = rendered("device = chip::pac, monotonic = Mono,");
assert!(output.contains("use super :: Mono"), "{output}");
assert!(!output.contains(NO_MONOTONIC), "{output}");
}
}