diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b37439..d65aa64 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). ### Added - CI changelog entry enforcer +- actor API syntax. for details see [RFC #52](https://github.com/rtic-rs/rfcs/pull/52) ### Changed diff --git a/src/analyze.rs b/src/analyze.rs index 18938ac..5471dee 100644 --- a/src/analyze.rs +++ b/src/analyze.rs @@ -11,10 +11,10 @@ use crate::{ Set, }; +type TaskName = String; + pub(crate) fn app(app: &App) -> Result { // Collect all tasks into a vector - type TaskName = String; - type Priority = u8; // The task list is a Tuple (Name, Shared Resources, Local Resources, Priority) let task_resources_list: Vec<(TaskName, Vec<&Ident>, &LocalResources, Priority)> = @@ -252,7 +252,9 @@ pub(crate) fn app(app: &App) -> Result { let spawnee_prio = spawnee.args.priority; let channel = channels.entry(spawnee_prio).or_default(); - channel.tasks.insert(name.clone()); + channel + .spawnees + .insert(Spawnee::Task { name: name.clone() }); // All inputs are now send as we do not know from where they may be spawned. spawnee.inputs.iter().for_each(|input| { @@ -260,15 +262,41 @@ pub(crate) fn app(app: &App) -> Result { }); } + for (name, actor) in &app.actors { + let spawnee_prio = actor.priority; + + if !actor.subscriptions.is_empty() { + for (index, subscription) in actor.subscriptions.iter().enumerate() { + let channel = channels.entry(spawnee_prio).or_default(); + channel.spawnees.insert(Spawnee::Actor { + name: name.clone(), + subscription_index: index, + }); + + // All inputs are now send as we do not know from where they may be spawned. + send_types.insert(Box::new(subscription.ty.clone())); + } + } + } + // No channel should ever be empty - debug_assert!(channels.values().all(|channel| !channel.tasks.is_empty())); + debug_assert!(channels + .values() + .all(|channel| !channel.spawnees.is_empty())); // Compute channel capacities for channel in channels.values_mut() { channel.capacity = channel - .tasks + .spawnees .iter() - .map(|name| app.software_tasks[name].args.capacity) + .map(|spawnee| match spawnee { + Spawnee::Task { name } => app.software_tasks[name].args.capacity, + Spawnee::Actor { + name, + subscription_index, + .. + } => app.actors[name].subscriptions[*subscription_index].capacity, + }) .sum(); } @@ -345,8 +373,25 @@ pub struct Channel { /// The channel capacity pub capacity: u8, - /// Tasks that can be spawned on this channel - pub tasks: BTreeSet, + /// Tasks / AO that can be spawned on this channel + pub spawnees: BTreeSet, +} + +/// What can be spawned +#[derive(Debug, Eq, Ord, PartialEq, PartialOrd)] +pub enum Spawnee { + /// A software task + Task { + /// The name of the task + name: Task, + }, + /// An actor + Actor { + /// The name of the actor + name: Ident, + /// Index into the actor's `subscriptions` field + subscription_index: usize, + }, } /// Resource ownership diff --git a/src/ast.rs b/src/ast.rs index 03265d5..14a1715 100644 --- a/src/ast.rs +++ b/src/ast.rs @@ -29,6 +29,9 @@ pub struct App { /// Task local resources defined in `#[local]` pub local_resources: Map, + /// Actors defined in the `#[actors]` struct + pub actors: Map, + /// User imports pub user_imports: Vec, @@ -90,6 +93,9 @@ pub struct Init { /// The name of the user provided local resources struct pub user_local_struct: Ident, + + /// The name of the user provided actors struct + pub user_actors_struct: Option, } /// `init` context metadata @@ -192,6 +198,31 @@ pub struct LocalResource { pub ty: Box, } +/// An actor defined in the `#[actors]` struct. +#[derive(Debug)] +#[non_exhaustive] +pub struct Actor { + /// The priority of this actor + pub priority: u8, + /// The expression used to initialized this actor. If absent, uses late/runtime + /// initialization. + pub init: Option>, + /// Type of the actor. + pub ty: Box, + /// #[subscribe] attributes + pub subscriptions: Vec, +} + +/// The `#[subscribe]` attribute of an actor +#[derive(Debug)] +#[non_exhaustive] +pub struct Subscription { + /// Capacity of this channel + pub capacity: u8, + /// Message type + pub ty: Type, +} + /// Monotonic #[derive(Debug)] #[non_exhaustive] diff --git a/src/parse.rs b/src/parse.rs index fd15bae..37145b6 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -1,3 +1,4 @@ +mod actor; mod app; mod hardware_task; mod idle; diff --git a/src/parse/actor.rs b/src/parse/actor.rs new file mode 100644 index 0000000..9025ce0 --- /dev/null +++ b/src/parse/actor.rs @@ -0,0 +1,160 @@ +use proc_macro2::{Ident, Span}; +use syn::{ + parenthesized, + parse::{self, Parse}, + spanned::Spanned, + Field, LitInt, Token, Type, Visibility, +}; + +use crate::ast::{Actor, Subscription}; + +use super::util::{self, FilterAttrs}; + +impl Actor { + pub(crate) fn parse(item: &Field, span: Span) -> parse::Result { + if item.vis != Visibility::Inherited { + return Err(parse::Error::new( + span, + "this field must have inherited / private visibility", + )); + } + + let FilterAttrs { cfgs, attrs, .. } = util::filter_attributes(item.attrs.clone()); + + if !cfgs.is_empty() { + return Err(parse::Error::new(span, "`#[cfg]` is not allowed on actors")); + } + + let mut priority = None; + let mut init = None; + let mut subscriptions = Vec::new(); + + for attr in attrs { + match attr.path.get_ident() { + Some(name) => { + match &*name.to_string() { + "priority" => { + if priority.is_some() { + return Err(parse::Error::new( + attr.span(), + "only one `#[priority]` attribute is allowed on an actor", + )); + } + + let prio: EqPriority = syn::parse2(attr.tokens)?; + priority = Some(prio.priority); + } + "init" => { + if init.is_some() { + return Err(parse::Error::new( + attr.span(), + "only one `#[init]` attribute is allowed on an actor", + )); + } + + // `#[init(expr)]` can be parsed via `ExprParen` + let paren: syn::ExprParen = syn::parse2(attr.tokens)?; + + init = Some(paren.expr); + } + "subscribe" => { + let subscribe: Subscribe = syn::parse2(attr.tokens)?; + let capacity = subscribe + .capacity + .map(|lit| { + lit.base10_digits().parse::().map_err(|_| { + parse::Error::new(lit.span(), "not a `u8` value") + }) + }) + .transpose()?; + + subscriptions.push(Subscription { + ty: subscribe.ty, + capacity: capacity.unwrap_or(1), + }); + } + _ => { + return Err(parse::Error::new( + name.span(), + "this attribute is not supported on actor declarations", + )); + } + } + } + None => { + return Err(parse::Error::new( + attr.path.span(), + "this attribute is not supported on actor declarations", + )); + } + } + } + + Ok(Actor { + ty: Box::new(item.ty.clone()), + priority: priority.unwrap_or(1), + init, + subscriptions, + }) + } +} + +struct EqPriority { + priority: u8, +} + +impl parse::Parse for EqPriority { + fn parse(input: parse::ParseStream<'_>) -> syn::Result { + let _eq: Token![=] = input.parse()?; + let lit: syn::LitInt = input.parse()?; + + if !lit.suffix().is_empty() { + return Err(parse::Error::new( + lit.span(), + "this literal must be unsuffixed", + )); + } + + let value = lit.base10_parse::().ok(); + match value { + None | Some(0) => Err(parse::Error::new( + lit.span(), + "this literal must be in the range 1...255", + )), + Some(priority) => Ok(Self { priority }), + } + } +} + +struct Subscribe { + ty: Type, + capacity: Option, +} + +impl Parse for Subscribe { + fn parse(input: parse::ParseStream<'_>) -> syn::Result { + let content; + parenthesized!(content in input); + + let ty = content.parse()?; + + let capacity = if content.is_empty() { + None + } else { + let _: Token![,] = content.parse()?; + let ident: Ident = content.parse()?; + + if ident == "capacity" { + let _: Token![=] = content.parse()?; + Some(content.parse()?) + } else { + return Err(parse::Error::new( + ident.span(), + format!("expected `capacity`, found `{}`", ident), + )); + } + }; + + Ok(Self { ty, capacity }) + } +} diff --git a/src/parse/app.rs b/src/parse/app.rs index e4a5665..bcd2854 100644 --- a/src/parse/app.rs +++ b/src/parse/app.rs @@ -11,7 +11,7 @@ use syn::{ use super::Input; use crate::{ ast::{ - App, AppArgs, ExternInterrupt, ExternInterrupts, HardwareTask, Idle, IdleArgs, Init, + Actor, App, AppArgs, ExternInterrupt, ExternInterrupts, HardwareTask, Idle, IdleArgs, Init, InitArgs, LocalResource, Monotonic, MonotonicArgs, SharedResource, SoftwareTask, }, parse::util, @@ -143,7 +143,9 @@ impl App { let mut shared_resources_ident = None; let mut shared_resources = Map::new(); let mut local_resources_ident = None; + let mut actors_ident = None; let mut local_resources = Map::new(); + let mut actors = Map::new(); let mut monotonics = Map::new(); let mut hardware_tasks = Map::new(); let mut software_tasks = Map::new(); @@ -366,6 +368,48 @@ impl App { "this `struct` must have named fields", )); } + } else if let Some(_pos) = struct_item + .attrs + .iter() + .position(|attr| util::attr_eq(attr, "actors")) + { + let span = struct_item.ident.span(); + + actors_ident = Some(struct_item.ident.clone()); + + if !actors.is_empty() { + return Err(parse::Error::new( + span, + "`#[actors]` struct must appear at most once", + )); + } + + if struct_item.vis != Visibility::Inherited { + return Err(parse::Error::new( + struct_item.span(), + "this item must have inherited / private visibility", + )); + } + + if let Fields::Named(fields) = &mut struct_item.fields { + for field in &mut fields.named { + let ident = field.ident.as_ref().expect("UNREACHABLE"); + + if actors.contains_key(ident) { + return Err(parse::Error::new( + ident.span(), + "this resource is listed more than once", + )); + } + + actors.insert(ident.clone(), Actor::parse(field, ident.span())?); + } + } else { + return Err(parse::Error::new( + struct_item.span(), + "this `struct` must have named fields", + )); + } } else { // Structure without the #[resources] attribute should just be passed along user_code.push(item.clone()); @@ -516,6 +560,39 @@ impl App { )); } + match (&actors_ident, &init.user_actors_struct) { + (None, None) => {} + (None, Some(init_actors_struct)) => { + return Err(parse::Error::new( + init_actors_struct.span(), + format!( + "a `#[actors]` struct named `{}` needs to be defined", + init_actors_struct + ), + )); + } + (Some(actors_ident), None) => { + return Err(parse::Error::new( + actors_ident.span(), + format!( + "`#[init]` return type needs to include `{}` as the 4th tuple element ", + actors_ident + ), + )); + } + (Some(actors_ident), Some(init_actors_struct)) => { + if actors_ident != init_actors_struct { + return Err(parse::Error::new( + init_actors_struct.span(), + format!( + "This name and the one defined on `#[actors]` are not the same. Should this be `{}`?", + actors_ident + ), + )); + } + } + } + Ok(App { args, name: input.ident, @@ -524,6 +601,7 @@ impl App { monotonics, shared_resources, local_resources, + actors, user_imports, user_code, hardware_tasks, diff --git a/src/parse/init.rs b/src/parse/init.rs index c54ce7f..0c8dfe1 100644 --- a/src/parse/init.rs +++ b/src/parse/init.rs @@ -22,7 +22,7 @@ impl Init { let name = item.sig.ident.to_string(); if valid_signature { - if let Ok((user_shared_struct, user_local_struct)) = + if let Ok((user_shared_struct, user_local_struct, user_actors_struct)) = util::type_is_init_return(&item.sig.output, &name) { if let Some((context, Ok(rest))) = util::parse_inputs(item.sig.inputs, &name) { @@ -35,6 +35,7 @@ impl Init { stmts: item.block.stmts, user_shared_struct, user_local_struct, + user_actors_struct, }); } } diff --git a/src/parse/util.rs b/src/parse/util.rs index 645c47a..a328367 100644 --- a/src/parse/util.rs +++ b/src/parse/util.rs @@ -283,22 +283,36 @@ fn extract_init_resource_name_ident(ty: Type) -> Result { } /// Checks Init's return type, return the user provided types for analysis -pub fn type_is_init_return(ty: &ReturnType, name: &str) -> Result<(Ident, Ident), ()> { +pub fn type_is_init_return( + ty: &ReturnType, + name: &str, +) -> Result<(Ident, Ident, Option), ()> { match ty { ReturnType::Default => Err(()), ReturnType::Type(_, ty) => match &**ty { Type::Tuple(t) => { // return should be: - // fn -> (User's #[shared] struct, User's #[local] struct, {name}::Monotonics) + // fn -> (User's #[shared] struct, User's #[local] struct, {name}::Monotonics) OR + // fn -> (User's #[shared] struct, User's #[local] struct, {name}::Monotonics, User's #[actors] struct) // - // We check the length and the last one here, analysis checks that the user + // We check the length of the tuple and the Monotonics element here, analysis checks that the user // provided structs are correct. - if t.elems.len() == 3 && type_is_path(&t.elems[2], &[name, "Monotonics"]) { - return Ok(( - extract_init_resource_name_ident(t.elems[0].clone())?, - extract_init_resource_name_ident(t.elems[1].clone())?, - )); + let elems_count = t.elems.len(); + + if (3..=4).contains(&elems_count) { + let shared = extract_init_resource_name_ident(t.elems[0].clone())?; + let local = extract_init_resource_name_ident(t.elems[1].clone())?; + + if type_is_path(&t.elems[2], &[name, "Monotonics"]) { + let actors = if elems_count == 4 { + Some(extract_init_resource_name_ident(t.elems[3].clone())?) + } else { + None + }; + + return Ok((shared, local, actors)); + } } Err(()) diff --git a/src/tests/single.rs b/src/tests/single.rs index a57fad6..52f7e20 100644 --- a/src/tests/single.rs +++ b/src/tests/single.rs @@ -1,4 +1,7 @@ -use crate::{analyze::Ownership, Settings}; +use crate::{ + analyze::{Ownership, Spawnee}, + Settings, +}; use quote::quote; #[test] @@ -432,3 +435,105 @@ fn late_resources() { let late = &app.shared_resources; assert_eq!(late.len(), 1); } + +#[test] +fn actors() { + let (app, analysis) = crate::parse2( + quote!(), + quote!( + mod app { + #[actors] + struct Actors { + #[priority = 2] + #[subscribe(MsgA)] + #[subscribe(MsgB, capacity = 2)] + a: (), + + #[subscribe(MsgA)] + #[init(1)] + b: u32, + + #[priority = 1] + #[subscribe(MsgB, capacity = 3)] + c: (), + } + + #[shared] + struct Shared {} + + #[local] + struct Local {} + + #[init] + fn init(_: init::Context) -> (Shared, Local, init::Monotonics, Actors) { + .. + } + } + ), + Settings::default(), + ) + .unwrap(); + + let actors = &app.actors; + assert_eq!(actors.len(), 3); + + { + let actor = &app.actors[0]; + assert_eq!(2, actor.priority); + assert_eq!(1, actor.subscriptions[0].capacity); + assert_eq!(2, actor.subscriptions[1].capacity); + assert_eq!(None, actor.init); + } + + { + let actor = &app.actors[1]; + + assert_eq!(1, actor.priority); + assert_eq!(1, actor.subscriptions[0].capacity); + assert_eq!( + Some("1"), + actor + .init + .as_ref() + .map(|expr| quote!(#expr).to_string()) + .as_deref() + ); + } + + { + let actor = &app.actors[2]; + + assert_eq!(1, actor.priority); + assert_eq!(3, actor.subscriptions[0].capacity); + assert_eq!(None, actor.init); + } + + let expected = [ + ( + /* priority: */ 1, + /* capacity */ 4, + [("b", 0), ("c", 0)], + ), + ( + /* priority: */ 2, + /* capacity */ 3, + [("a", 0), ("a", 1)], + ), + ]; + for (actual, expected) in analysis.channels.iter().zip(expected) { + assert_eq!(*actual.0, expected.0); + assert_eq!(actual.1.capacity, expected.1); + for (spawnee, expected) in actual.1.spawnees.iter().zip(expected.2) { + match spawnee { + Spawnee::Task { .. } => panic!(), + Spawnee::Actor { + name, + subscription_index, + } => { + assert_eq!(expected.0, name.to_string()); + assert_eq!(expected.1, *subscription_index); + } + } + } + } +}