diff options
Diffstat (limited to 'rtic-macros/src/syntax')
| -rw-r--r-- | rtic-macros/src/syntax/.travis.yml | 31 | ||||
| -rw-r--r-- | rtic-macros/src/syntax/accessors.rs | 113 | ||||
| -rw-r--r-- | rtic-macros/src/syntax/analyze.rs | 414 | ||||
| -rw-r--r-- | rtic-macros/src/syntax/ast.rs | 335 | ||||
| -rw-r--r-- | rtic-macros/src/syntax/check.rs | 66 | ||||
| -rw-r--r-- | rtic-macros/src/syntax/optimize.rs | 36 | ||||
| -rw-r--r-- | rtic-macros/src/syntax/parse.rs | 319 | ||||
| -rw-r--r-- | rtic-macros/src/syntax/parse/app.rs | 480 | ||||
| -rw-r--r-- | rtic-macros/src/syntax/parse/hardware_task.rs | 76 | ||||
| -rw-r--r-- | rtic-macros/src/syntax/parse/idle.rs | 42 | ||||
| -rw-r--r-- | rtic-macros/src/syntax/parse/init.rs | 51 | ||||
| -rw-r--r-- | rtic-macros/src/syntax/parse/resource.rs | 55 | ||||
| -rw-r--r-- | rtic-macros/src/syntax/parse/software_task.rs | 76 | ||||
| -rw-r--r-- | rtic-macros/src/syntax/parse/util.rs | 338 |
14 files changed, 2432 insertions, 0 deletions
diff --git a/rtic-macros/src/syntax/.travis.yml b/rtic-macros/src/syntax/.travis.yml new file mode 100644 index 0000000..52d1ffd --- /dev/null +++ b/rtic-macros/src/syntax/.travis.yml @@ -0,0 +1,31 @@ +language: rust + +matrix: + include: + # MSRV + - env: TARGET=x86_64-unknown-linux-gnu + rust: 1.36.0 + + - env: TARGET=x86_64-unknown-linux-gnu + rust: stable + +before_install: set -e + +script: + - bash ci/script.sh + +after_script: set +e + +cache: cargo + +before_cache: + - chmod -R a+r $HOME/.cargo; + +branches: + only: + - staging + - trying + +notifications: + email: + on_success: never diff --git a/rtic-macros/src/syntax/accessors.rs b/rtic-macros/src/syntax/accessors.rs new file mode 100644 index 0000000..e75dde6 --- /dev/null +++ b/rtic-macros/src/syntax/accessors.rs @@ -0,0 +1,113 @@ +use syn::Ident; + +use crate::syntax::{ + analyze::Priority, + ast::{Access, App, Local, TaskLocal}, +}; + +impl App { + pub(crate) fn shared_resource_accesses( + &self, + ) -> impl Iterator<Item = (Option<Priority>, &Ident, Access)> { + self.idle + .iter() + .flat_map(|idle| { + idle.args + .shared_resources + .iter() + .map(move |(name, access)| (Some(0), name, *access)) + }) + .chain(self.hardware_tasks.values().flat_map(|task| { + task.args + .shared_resources + .iter() + .map(move |(name, access)| (Some(task.args.priority), name, *access)) + })) + .chain(self.software_tasks.values().flat_map(|task| { + task.args + .shared_resources + .iter() + .map(move |(name, access)| (Some(task.args.priority), name, *access)) + })) + } + + fn is_external(task_local: &TaskLocal) -> bool { + matches!(task_local, TaskLocal::External) + } + + pub(crate) fn local_resource_accesses(&self) -> impl Iterator<Item = &Ident> { + self.init + .args + .local_resources + .iter() + .filter(|(_, task_local)| Self::is_external(task_local)) // Only check the resources declared in `#[local]` + .map(move |(name, _)| name) + .chain(self.idle.iter().flat_map(|idle| { + idle.args + .local_resources + .iter() + .filter(|(_, task_local)| Self::is_external(task_local)) // Only check the resources declared in `#[local]` + .map(move |(name, _)| name) + })) + .chain(self.hardware_tasks.values().flat_map(|task| { + task.args + .local_resources + .iter() + .filter(|(_, task_local)| Self::is_external(task_local)) // Only check the resources declared in `#[local]` + .map(move |(name, _)| name) + })) + .chain(self.software_tasks.values().flat_map(|task| { + task.args + .local_resources + .iter() + .filter(|(_, task_local)| Self::is_external(task_local)) // Only check the resources declared in `#[local]` + .map(move |(name, _)| name) + })) + } + + fn get_declared_local(tl: &TaskLocal) -> Option<&Local> { + match tl { + TaskLocal::External => None, + TaskLocal::Declared(l) => Some(l), + } + } + + /// Get all declared local resources, i.e. `local = [NAME: TYPE = EXPR]`. + /// + /// Returns a vector of (task name, resource name, `Local` struct) + pub fn declared_local_resources(&self) -> Vec<(&Ident, &Ident, &Local)> { + self.init + .args + .local_resources + .iter() + .filter_map(move |(name, tl)| { + Self::get_declared_local(tl).map(|l| (&self.init.name, name, l)) + }) + .chain(self.idle.iter().flat_map(|idle| { + idle.args + .local_resources + .iter() + .filter_map(move |(name, tl)| { + Self::get_declared_local(tl) + .map(|l| (&self.idle.as_ref().unwrap().name, name, l)) + }) + })) + .chain(self.hardware_tasks.iter().flat_map(|(task_name, task)| { + task.args + .local_resources + .iter() + .filter_map(move |(name, tl)| { + Self::get_declared_local(tl).map(|l| (task_name, name, l)) + }) + })) + .chain(self.software_tasks.iter().flat_map(|(task_name, task)| { + task.args + .local_resources + .iter() + .filter_map(move |(name, tl)| { + Self::get_declared_local(tl).map(|l| (task_name, name, l)) + }) + })) + .collect() + } +} diff --git a/rtic-macros/src/syntax/analyze.rs b/rtic-macros/src/syntax/analyze.rs new file mode 100644 index 0000000..57f9f2c --- /dev/null +++ b/rtic-macros/src/syntax/analyze.rs @@ -0,0 +1,414 @@ +//! RTIC application analysis + +use core::cmp; +use std::collections::{BTreeMap, BTreeSet, HashMap}; + +use indexmap::{IndexMap, IndexSet}; +use syn::{Ident, Type}; + +use crate::syntax::{ + ast::{App, LocalResources, TaskLocal}, + Set, +}; + +pub(crate) fn app(app: &App) -> Result<Analysis, syn::Error> { + // Collect all tasks into a vector + type TaskName = Ident; + 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)> = + Some(&app.init) + .iter() + .map(|ht| (ht.name.clone(), Vec::new(), &ht.args.local_resources, 0)) + .chain(app.idle.iter().map(|ht| { + ( + ht.name.clone(), + ht.args + .shared_resources + .iter() + .map(|(v, _)| v) + .collect::<Vec<_>>(), + &ht.args.local_resources, + 0, + ) + })) + .chain(app.software_tasks.iter().map(|(name, ht)| { + ( + name.clone(), + ht.args + .shared_resources + .iter() + .map(|(v, _)| v) + .collect::<Vec<_>>(), + &ht.args.local_resources, + ht.args.priority, + ) + })) + .chain(app.hardware_tasks.iter().map(|(name, ht)| { + ( + name.clone(), + ht.args + .shared_resources + .iter() + .map(|(v, _)| v) + .collect::<Vec<_>>(), + &ht.args.local_resources, + ht.args.priority, + ) + })) + .collect(); + + let mut error = vec![]; + let mut lf_res_with_error = vec![]; + let mut lf_hash = HashMap::new(); + + // Collect lock free resources + let lock_free: Vec<&Ident> = app + .shared_resources + .iter() + .filter(|(_, r)| r.properties.lock_free) + .map(|(i, _)| i) + .collect(); + + // Check that lock_free resources are correct + for lf_res in lock_free.iter() { + for (task, tr, _, priority) in task_resources_list.iter() { + for r in tr { + // Get all uses of resources annotated lock_free + if lf_res == r { + // Check so async tasks do not use lock free resources + if app.software_tasks.get(task).is_some() { + error.push(syn::Error::new( + r.span(), + format!( + "Lock free shared resource {:?} is used by an async tasks, which is forbidden", + r.to_string(), + ), + )); + } + + // HashMap returns the previous existing object if old.key == new.key + if let Some(lf_res) = lf_hash.insert(r.to_string(), (task, r, priority)) { + // Check if priority differ, if it does, append to + // list of resources which will be annotated with errors + if priority != lf_res.2 { + lf_res_with_error.push(lf_res.1); + lf_res_with_error.push(r); + } + + // If the resource already violates lock free properties + if lf_res_with_error.contains(&r) { + lf_res_with_error.push(lf_res.1); + lf_res_with_error.push(r); + } + } + } + } + } + } + + // Add error message in the resource struct + for r in lock_free { + if lf_res_with_error.contains(&&r) { + error.push(syn::Error::new( + r.span(), + format!( + "Lock free shared resource {:?} is used by tasks at different priorities", + r.to_string(), + ), + )); + } + } + + // Add error message for each use of the shared resource + for resource in lf_res_with_error.clone() { + error.push(syn::Error::new( + resource.span(), + format!( + "Shared resource {:?} is declared lock free but used by tasks at different priorities", + resource.to_string(), + ), + )); + } + + // Collect local resources + let local: Vec<&Ident> = app.local_resources.iter().map(|(i, _)| i).collect(); + + let mut lr_with_error = vec![]; + let mut lr_hash = HashMap::new(); + + // Check that local resources are not shared + for lr in local { + for (task, _, local_resources, _) in task_resources_list.iter() { + for (name, res) in local_resources.iter() { + // Get all uses of resources annotated lock_free + if lr == name { + match res { + TaskLocal::External => { + // HashMap returns the previous existing object if old.key == new.key + if let Some(lr) = lr_hash.insert(name.to_string(), (task, name)) { + lr_with_error.push(lr.1); + lr_with_error.push(name); + } + } + // If a declared local has the same name as the `#[local]` struct, it's an + // direct error + TaskLocal::Declared(_) => { + lr_with_error.push(lr); + lr_with_error.push(name); + } + } + } + } + } + } + + // Add error message for each use of the local resource + for resource in lr_with_error.clone() { + error.push(syn::Error::new( + resource.span(), + format!( + "Local resource {:?} is used by multiple tasks or collides with multiple definitions", + resource.to_string(), + ), + )); + } + + // Check 0-priority async software tasks and idle dependency + for (name, task) in &app.software_tasks { + if task.args.priority == 0 { + // If there is a 0-priority task, there must be no idle + if app.idle.is_some() { + error.push(syn::Error::new( + name.span(), + format!( + "Async task {:?} has priority 0, but `#[idle]` is defined. 0-priority async tasks are only allowed if there is no `#[idle]`.", + name.to_string(), + ) + )); + } + } + } + + // Collect errors if any and return/halt + if !error.is_empty() { + let mut err = error.get(0).unwrap().clone(); + error.iter().for_each(|e| err.combine(e.clone())); + return Err(err); + } + + // e. Location of resources + let mut used_shared_resource = IndexSet::new(); + let mut ownerships = Ownerships::new(); + let mut sync_types = SyncTypes::new(); + for (prio, name, access) in app.shared_resource_accesses() { + let res = app.shared_resources.get(name).expect("UNREACHABLE"); + + // (e) + // This shared resource is used + used_shared_resource.insert(name.clone()); + + // (c) + if let Some(priority) = prio { + if let Some(ownership) = ownerships.get_mut(name) { + match *ownership { + Ownership::Owned { priority: ceiling } + | Ownership::CoOwned { priority: ceiling } + | Ownership::Contended { ceiling } + if priority != ceiling => + { + *ownership = Ownership::Contended { + ceiling: cmp::max(ceiling, priority), + }; + + if access.is_shared() { + sync_types.insert(res.ty.clone()); + } + } + + Ownership::Owned { priority: ceil } if ceil == priority => { + *ownership = Ownership::CoOwned { priority }; + } + + _ => {} + } + } else { + ownerships.insert(name.clone(), Ownership::Owned { priority }); + } + } + } + + // Create the list of used local resource Idents + let mut used_local_resource = IndexSet::new(); + + for (_, _, locals, _) in task_resources_list { + for (local, _) in locals { + used_local_resource.insert(local.clone()); + } + } + + // Most shared resources need to be `Send`, only 0 prio does not need it + let mut send_types = SendTypes::new(); + + for (name, res) in app.shared_resources.iter() { + if ownerships + .get(name) + .map(|ownership| match *ownership { + Ownership::Owned { priority: ceiling } + | Ownership::CoOwned { priority: ceiling } + | Ownership::Contended { ceiling } => ceiling != 0, + }) + .unwrap_or(false) + { + send_types.insert(res.ty.clone()); + } + } + + // Most local resources need to be `Send` as well, only 0 prio does not need it + for (name, res) in app.local_resources.iter() { + if ownerships + .get(name) + .map(|ownership| match *ownership { + Ownership::Owned { priority: ceiling } + | Ownership::CoOwned { priority: ceiling } + | Ownership::Contended { ceiling } => ceiling != 0, + }) + .unwrap_or(false) + { + send_types.insert(res.ty.clone()); + } + } + + let mut channels = Channels::new(); + + for (name, spawnee) in &app.software_tasks { + let spawnee_prio = spawnee.args.priority; + + let channel = channels.entry(spawnee_prio).or_default(); + channel.tasks.insert(name.clone()); + + // All inputs are send as we do not know from where they may be spawned. + spawnee.inputs.iter().for_each(|input| { + send_types.insert(input.ty.clone()); + }); + } + + // No channel should ever be empty + debug_assert!(channels.values().all(|channel| !channel.tasks.is_empty())); + + Ok(Analysis { + channels, + shared_resources: used_shared_resource, + local_resources: used_local_resource, + ownerships, + send_types, + sync_types, + }) +} + +// /// Priority ceiling +// pub type Ceiling = Option<u8>; + +/// Task priority +pub type Priority = u8; + +/// Resource name +pub type Resource = Ident; + +/// Task name +pub type Task = Ident; + +/// The result of analyzing an RTIC application +pub struct Analysis { + /// SPSC message channels + pub channels: Channels, + + /// Shared resources + /// + /// If a resource is not listed here it means that's a "dead" (never + /// accessed) resource and the backend should not generate code for it + pub shared_resources: UsedSharedResource, + + /// Local resources + /// + /// If a resource is not listed here it means that's a "dead" (never + /// accessed) resource and the backend should not generate code for it + pub local_resources: UsedLocalResource, + + /// Resource ownership + pub ownerships: Ownerships, + + /// These types must implement the `Send` trait + pub send_types: SendTypes, + + /// These types must implement the `Sync` trait + pub sync_types: SyncTypes, +} + +/// All channels, keyed by dispatch priority +pub type Channels = BTreeMap<Priority, Channel>; + +/// Location of all *used* shared resources +pub type UsedSharedResource = IndexSet<Resource>; + +/// Location of all *used* local resources +pub type UsedLocalResource = IndexSet<Resource>; + +/// Resource ownership +pub type Ownerships = IndexMap<Resource, Ownership>; + +/// These types must implement the `Send` trait +pub type SendTypes = Set<Box<Type>>; + +/// These types must implement the `Sync` trait +pub type SyncTypes = Set<Box<Type>>; + +/// A channel used to send messages +#[derive(Debug, Default)] +pub struct Channel { + /// Tasks that can be spawned on this channel + pub tasks: BTreeSet<Task>, +} + +/// Resource ownership +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum Ownership { + /// Owned by a single task + Owned { + /// Priority of the task that owns this resource + priority: u8, + }, + + /// "Co-owned" by more than one task; all of them have the same priority + CoOwned { + /// Priority of the tasks that co-own this resource + priority: u8, + }, + + /// Contended by more than one task; the tasks have different priorities + Contended { + /// Priority ceiling + ceiling: u8, + }, +} + +// impl Ownership { +// /// Whether this resource needs to a lock at this priority level +// pub fn needs_lock(&self, priority: u8) -> bool { +// match self { +// Ownership::Owned { .. } | Ownership::CoOwned { .. } => false, +// +// Ownership::Contended { ceiling } => { +// debug_assert!(*ceiling >= priority); +// +// priority < *ceiling +// } +// } +// } +// +// /// Whether this resource is exclusively owned +// pub fn is_owned(&self) -> bool { +// matches!(self, Ownership::Owned { .. }) +// } +// } diff --git a/rtic-macros/src/syntax/ast.rs b/rtic-macros/src/syntax/ast.rs new file mode 100644 index 0000000..27e6773 --- /dev/null +++ b/rtic-macros/src/syntax/ast.rs @@ -0,0 +1,335 @@ +//! Abstract Syntax Tree + +use syn::{Attribute, Expr, Ident, Item, ItemUse, Pat, PatType, Path, Stmt, Type}; + +use crate::syntax::Map; + +/// The `#[app]` attribute +#[derive(Debug)] +#[non_exhaustive] +pub struct App { + /// The arguments to the `#[app]` attribute + pub args: AppArgs, + + /// The name of the `const` item on which the `#[app]` attribute has been placed + pub name: Ident, + + /// The `#[init]` function + pub init: Init, + + /// The `#[idle]` function + pub idle: Option<Idle>, + + /// Resources shared between tasks defined in `#[shared]` + pub shared_resources: Map<SharedResource>, + + /// Task local resources defined in `#[local]` + pub local_resources: Map<LocalResource>, + + /// User imports + pub user_imports: Vec<ItemUse>, + + /// User code + pub user_code: Vec<Item>, + + /// Hardware tasks: `#[task(binds = ..)]`s + pub hardware_tasks: Map<HardwareTask>, + + /// Async software tasks: `#[task]` + pub software_tasks: Map<SoftwareTask>, +} + +/// Interrupts used to dispatch software tasks +pub type Dispatchers = Map<Dispatcher>; + +/// Interrupt that could be used to dispatch software tasks +#[derive(Debug, Clone)] +#[non_exhaustive] +pub struct Dispatcher { + /// Attributes that will apply to this interrupt handler + pub attrs: Vec<Attribute>, +} + +/// The arguments of the `#[app]` attribute +#[derive(Debug)] +pub struct AppArgs { + /// Device + pub device: Path, + + /// Peripherals + pub peripherals: bool, + + /// Interrupts used to dispatch software tasks + pub dispatchers: Dispatchers, +} + +/// The `init`-ialization function +#[derive(Debug)] +#[non_exhaustive] +pub struct Init { + /// `init` context metadata + pub args: InitArgs, + + /// Attributes that will apply to this `init` function + pub attrs: Vec<Attribute>, + + /// The name of the `#[init]` function + pub name: Ident, + + /// The context argument + pub context: Box<Pat>, + + /// The statements that make up this `init` function + pub stmts: Vec<Stmt>, + + /// The name of the user provided shared resources struct + pub user_shared_struct: Ident, + + /// The name of the user provided local resources struct + pub user_local_struct: Ident, +} + +/// `init` context metadata +#[derive(Debug)] +#[non_exhaustive] +pub struct InitArgs { + /// Local resources that can be accessed from this context + pub local_resources: LocalResources, +} + +impl Default for InitArgs { + fn default() -> Self { + Self { + local_resources: LocalResources::new(), + } + } +} + +/// The `idle` context +#[derive(Debug)] +#[non_exhaustive] +pub struct Idle { + /// `idle` context metadata + pub args: IdleArgs, + + /// Attributes that will apply to this `idle` function + pub attrs: Vec<Attribute>, + + /// The name of the `#[idle]` function + pub name: Ident, + + /// The context argument + pub context: Box<Pat>, + + /// The statements that make up this `idle` function + pub stmts: Vec<Stmt>, +} + +/// `idle` context metadata +#[derive(Debug)] +#[non_exhaustive] +pub struct IdleArgs { + /// Local resources that can be accessed from this context + pub local_resources: LocalResources, + + /// Shared resources that can be accessed from this context + pub shared_resources: SharedResources, +} + +impl Default for IdleArgs { + fn default() -> Self { + Self { + local_resources: LocalResources::new(), + shared_resources: SharedResources::new(), + } + } +} + +/// Shared resource properties +#[derive(Debug)] +pub struct SharedResourceProperties { + /// A lock free (exclusive resource) + pub lock_free: bool, +} + +/// A shared resource, defined in `#[shared]` +#[derive(Debug)] +#[non_exhaustive] +pub struct SharedResource { + /// `#[cfg]` attributes like `#[cfg(debug_assertions)]` + pub cfgs: Vec<Attribute>, + + /// `#[doc]` attributes like `/// this is a docstring` + pub docs: Vec<Attribute>, + + /// Attributes that will apply to this resource + pub attrs: Vec<Attribute>, + + /// The type of this resource + pub ty: Box<Type>, + + /// Shared resource properties + pub properties: SharedResourceProperties, +} + +/// A local resource, defined in `#[local]` +#[derive(Debug)] +#[non_exhaustive] +pub struct LocalResource { + /// `#[cfg]` attributes like `#[cfg(debug_assertions)]` + pub cfgs: Vec<Attribute>, + + /// `#[doc]` attributes like `/// this is a docstring` + pub docs: Vec<Attribute>, + + /// Attributes that will apply to this resource + pub attrs: Vec<Attribute>, + + /// The type of this resource + pub ty: Box<Type>, +} + +/// An async software task +#[derive(Debug)] +#[non_exhaustive] +pub struct SoftwareTask { + /// Software task metadata + pub args: SoftwareTaskArgs, + + /// `#[cfg]` attributes like `#[cfg(debug_assertions)]` + pub cfgs: Vec<Attribute>, + + /// Attributes that will apply to this interrupt handler + pub attrs: Vec<Attribute>, + + /// The context argument + pub context: Box<Pat>, + + /// The inputs of this software task + pub inputs: Vec<PatType>, + + /// The statements that make up the task handler + pub stmts: Vec<Stmt>, + + /// The task is declared externally + pub is_extern: bool, +} + +/// Software task metadata +#[derive(Debug)] +#[non_exhaustive] +pub struct SoftwareTaskArgs { + /// The priority of this task + pub priority: u8, + + /// Local resources that can be accessed from this context + pub local_resources: LocalResources, + + /// Shared resources that can be accessed from this context + pub shared_resources: SharedResources, +} + +impl Default for SoftwareTaskArgs { + fn default() -> Self { + Self { + priority: 1, + local_resources: LocalResources::new(), + shared_resources: SharedResources::new(), + } + } +} + +/// A hardware task +#[derive(Debug)] +#[non_exhaustive] +pub struct HardwareTask { + /// Hardware task metadata + pub args: HardwareTaskArgs, + + /// `#[cfg]` attributes like `#[cfg(debug_assertions)]` + pub cfgs: Vec<Attribute>, + + /// Attributes that will apply to this interrupt handler + pub attrs: Vec<Attribute>, + + /// The context argument + pub context: Box<Pat>, + + /// The statements that make up the task handler + pub stmts: Vec<Stmt>, + + /// The task is declared externally + pub is_extern: bool, +} + +/// Hardware task metadata +#[derive(Debug)] +#[non_exhaustive] +pub struct HardwareTaskArgs { + /// The interrupt or exception that this task is bound to + pub binds: Ident, + + /// The priority of this task + pub priority: u8, + + /// Local resources that can be accessed from this context + pub local_resources: LocalResources, + + /// Shared resources that can be accessed from this context + pub shared_resources: SharedResources, +} + +/// A `static mut` variable local to and owned by a context +#[derive(Debug)] +#[non_exhaustive] +pub struct Local { + /// Attributes like `#[link_section]` + pub attrs: Vec<Attribute>, + + /// `#[cfg]` attributes like `#[cfg(debug_assertions)]` + pub cfgs: Vec<Attribute>, + + /// Type + pub ty: Box<Type>, + + /// Initial value + pub expr: Box<Expr>, +} + +/// A wrapper of the 2 kinds of locals that tasks can have +#[derive(Debug)] +#[non_exhaustive] +pub enum TaskLocal { + /// The local is declared externally (i.e. `#[local]` struct) + External, + /// The local is declared in the task + Declared(Local), +} + +/// Resource access +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum Access { + /// `[x]`, a mutable resource + Exclusive, + + /// `[&x]`, a static non-mutable resource + Shared, +} + +impl Access { + /// Is this enum in the `Exclusive` variant? + pub fn is_exclusive(&self) -> bool { + *self == Access::Exclusive + } + + /// Is this enum in the `Shared` variant? + pub fn is_shared(&self) -> bool { + *self == Access::Shared + } +} + +/// Shared resource access list in task attribute +pub type SharedResources = Map<Access>; + +/// Local resource access/declaration list in task attribute +pub type LocalResources = Map<TaskLocal>; diff --git a/rtic-macros/src/syntax/check.rs b/rtic-macros/src/syntax/check.rs new file mode 100644 index 0000000..989d418 --- /dev/null +++ b/rtic-macros/src/syntax/check.rs @@ -0,0 +1,66 @@ +use std::collections::HashSet; + +use syn::parse; + +use crate::syntax::ast::App; + +pub fn app(app: &App) -> parse::Result<()> { + // Check that all referenced resources have been declared + // Check that resources are NOT `Exclusive`-ly shared + let mut owners = HashSet::new(); + for (_, name, access) in app.shared_resource_accesses() { + if app.shared_resources.get(name).is_none() { + return Err(parse::Error::new( + name.span(), + "this shared resource has NOT been declared", + )); + } + + if access.is_exclusive() { + owners.insert(name); + } + } + + for name in app.local_resource_accesses() { + if app.local_resources.get(name).is_none() { + return Err(parse::Error::new( + name.span(), + "this local resource has NOT been declared", + )); + } + } + + // Check that no resource has both types of access (`Exclusive` & `Shared`) + let exclusive_accesses = app + .shared_resource_accesses() + .filter_map(|(priority, name, access)| { + if priority.is_some() && access.is_exclusive() { + Some(name) + } else { + None + } + }) + .collect::<HashSet<_>>(); + for (_, name, access) in app.shared_resource_accesses() { + if access.is_shared() && exclusive_accesses.contains(name) { + return Err(parse::Error::new( + name.span(), + "this implementation doesn't support shared (`&-`) - exclusive (`&mut-`) locks; use `x` instead of `&x`", + )); + } + } + + // check that dispatchers are not used as hardware tasks + for task in app.hardware_tasks.values() { + let binds = &task.args.binds; + + if app.args.dispatchers.contains_key(binds) { + return Err(parse::Error::new( + binds.span(), + "dispatcher interrupts can't be used as hardware tasks", + )); + } + } + + Ok(()) +} diff --git a/rtic-macros/src/syntax/optimize.rs b/rtic-macros/src/syntax/optimize.rs new file mode 100644 index 0000000..e83ba31 --- /dev/null +++ b/rtic-macros/src/syntax/optimize.rs @@ -0,0 +1,36 @@ +use std::collections::{BTreeSet, HashMap}; + +use crate::syntax::ast::App; + +pub fn app(app: &mut App, settings: &Settings) { + // "compress" priorities + // If the user specified, for example, task priorities of "1, 3, 6", + // compress them into "1, 2, 3" as to leave no gaps + if settings.optimize_priorities { + // all task priorities ordered in ascending order + let priorities = app + .hardware_tasks + .values() + .map(|task| Some(task.args.priority)) + .chain( + app.software_tasks + .values() + .map(|task| Some(task.args.priority)), + ) + .collect::<BTreeSet<_>>(); + + let map = priorities + .iter() + .cloned() + .zip(1..) + .collect::<HashMap<_, _>>(); + + for task in app.hardware_tasks.values_mut() { + task.args.priority = map[&Some(task.args.priority)]; + } + + for task in app.software_tasks.values_mut() { + task.args.priority = map[&Some(task.args.priority)]; + } + } +} diff --git a/rtic-macros/src/syntax/parse.rs b/rtic-macros/src/syntax/parse.rs new file mode 100644 index 0000000..72eeeaf --- /dev/null +++ b/rtic-macros/src/syntax/parse.rs @@ -0,0 +1,319 @@ +mod app; +mod hardware_task; +mod idle; +mod init; +mod resource; +mod software_task; +mod util; + +use proc_macro2::TokenStream as TokenStream2; +use syn::{ + braced, parenthesized, + parse::{self, Parse, ParseStream, Parser}, + token::Brace, + Ident, Item, LitInt, Token, +}; + +use crate::syntax::{ + ast::{App, AppArgs, HardwareTaskArgs, IdleArgs, InitArgs, SoftwareTaskArgs, TaskLocal}, + Either, +}; + +// Parse the app, both app arguments and body (input) +pub fn app(args: TokenStream2, input: TokenStream2) -> parse::Result<App> { + let args = AppArgs::parse(args)?; + let input: Input = syn::parse2(input)?; + + App::parse(args, input) +} + +pub(crate) struct Input { + _mod_token: Token![mod], + pub ident: Ident, + _brace_token: Brace, + pub items: Vec<Item>, +} + +impl Parse for Input { + fn parse(input: ParseStream<'_>) -> parse::Result<Self> { + fn parse_items(input: ParseStream<'_>) -> parse::Result<Vec<Item>> { + let mut items = vec![]; + + while !input.is_empty() { + items.push(input.parse()?); + } + + Ok(items) + } + + let content; + + let _mod_token = input.parse()?; + let ident = input.parse()?; + let _brace_token = braced!(content in input); + let items = content.call(parse_items)?; + + Ok(Input { + _mod_token, + ident, + _brace_token, + items, + }) + } +} + +fn init_args(tokens: TokenStream2) -> parse::Result<InitArgs> { + (|input: ParseStream<'_>| -> parse::Result<InitArgs> { + if input.is_empty() { + return Ok(InitArgs::default()); + } + + let mut local_resources = None; + + let content; + parenthesized!(content in input); + + if !content.is_empty() { + loop { + // Parse identifier name + let ident: Ident = content.parse()?; + // Handle equal sign + let _: Token![=] = content.parse()?; + + match &*ident.to_string() { + "local" => { + if local_resources.is_some() { + return Err(parse::Error::new( + ident.span(), + "argument appears more than once", + )); + } + + local_resources = Some(util::parse_local_resources(&content)?); + } + _ => { + return Err(parse::Error::new(ident.span(), "unexpected argument")); + } + } + + if content.is_empty() { + break; + } + // Handle comma: , + let _: Token![,] = content.parse()?; + } + } + + if let Some(locals) = &local_resources { + for (ident, task_local) in locals { + if let TaskLocal::External = task_local { + return Err(parse::Error::new( + ident.span(), + "only declared local resources are allowed in init", + )); + } + } + } + + Ok(InitArgs { + local_resources: local_resources.unwrap_or_default(), + }) + }) + .parse2(tokens) +} + +fn idle_args(tokens: TokenStream2) -> parse::Result<IdleArgs> { + (|input: ParseStream<'_>| -> parse::Result<IdleArgs> { + if input.is_empty() { + return Ok(IdleArgs::default()); + } + + let mut shared_resources = None; + let mut local_resources = None; + + let content; + parenthesized!(content in input); + if !content.is_empty() { + loop { + // Parse identifier name + let ident: Ident = content.parse()?; + // Handle equal sign + let _: Token![=] = content.parse()?; + + match &*ident.to_string() { + "shared" => { + if shared_resources.is_some() { + return Err(parse::Error::new( + ident.span(), + "argument appears more than once", + )); + } + + shared_resources = Some(util::parse_shared_resources(&content)?); + } + + "local" => { + if local_resources.is_some() { + return Err(parse::Error::new( + ident.span(), + "argument appears more than once", + )); + } + + local_resources = Some(util::parse_local_resources(&content)?); + } + + _ => { + return Err(parse::Error::new(ident.span(), "unexpected argument")); + } + } + if content.is_empty() { + break; + } + + // Handle comma: , + let _: Token![,] = content.parse()?; + } + } + + Ok(IdleArgs { + shared_resources: shared_resources.unwrap_or_default(), + local_resources: local_resources.unwrap_or_default(), + }) + }) + .parse2(tokens) +} + +fn task_args(tokens: TokenStream2) -> parse::Result<Either<HardwareTaskArgs, SoftwareTaskArgs>> { + (|input: ParseStream<'_>| -> parse::Result<Either<HardwareTaskArgs, SoftwareTaskArgs>> { + if input.is_empty() { + return Ok(Either::Right(SoftwareTaskArgs::default())); + } + + let mut binds = None; + let mut priority = None; + let mut shared_resources = None; + let mut local_resources = None; + let mut prio_span = None; + + let content; + parenthesized!(content in input); + loop { + if content.is_empty() { + break; + } + + // Parse identifier name + let ident: Ident = content.parse()?; + let ident_s = ident.to_string(); + + // Handle equal sign + let _: Token![=] = content.parse()?; + + match &*ident_s { + "binds" => { + if binds.is_some() { + return Err(parse::Error::new( + ident.span(), + "argument appears more than once", + )); + } + + // Parse identifier name + let ident = content.parse()?; + + binds = Some(ident); + } + + "priority" => { + if priority.is_some() { + return Err(parse::Error::new( + ident.span(), + "argument appears more than once", + )); + } + + // #lit + let lit: LitInt = content.parse()?; + + if !lit.suffix().is_empty() { + return Err(parse::Error::new( + lit.span(), + "this literal must be unsuffixed", + )); + } + + let value = lit.base10_parse::<u8>().ok(); + if value.is_none() { + return Err(parse::Error::new( + lit.span(), + "this literal must be in the range 0...255", + )); + } + + prio_span = Some(lit.span()); + priority = Some(value.unwrap()); + } + + "shared" => { + if shared_resources.is_some() { + return Err(parse::Error::new( + ident.span(), + "argument appears more than once", + )); + } + + shared_resources = Some(util::parse_shared_resources(&content)?); + } + + "local" => { + if local_resources.is_some() { + return Err(parse::Error::new( + ident.span(), + "argument appears more than once", + )); + } + + local_resources = Some(util::parse_local_resources(&content)?); + } + + _ => { + return Err(parse::Error::new(ident.span(), "unexpected argument")); + } + } + + if content.is_empty() { + break; + } + + // Handle comma: , + let _: Token![,] = content.parse()?; + } + let priority = priority.unwrap_or(1); + let shared_resources = shared_resources.unwrap_or_default(); + let local_resources = local_resources.unwrap_or_default(); + + Ok(if let Some(binds) = binds { + if priority == 0 { + return Err(parse::Error::new( + prio_span.unwrap(), + "hardware tasks are not allowed to be at priority 0", + )); + } + + Either::Left(HardwareTaskArgs { + binds, + priority, + shared_resources, + local_resources, + }) + } else { + Either::Right(SoftwareTaskArgs { + priority, + shared_resources, + local_resources, + }) + }) + }) + .parse2(tokens) +} diff --git a/rtic-macros/src/syntax/parse/app.rs b/rtic-macros/src/syntax/parse/app.rs new file mode 100644 index 0000000..e797f75 --- /dev/null +++ b/rtic-macros/src/syntax/parse/app.rs @@ -0,0 +1,480 @@ +use std::collections::HashSet; + +// use indexmap::map::Entry; +use proc_macro2::TokenStream as TokenStream2; +use syn::{ + parse::{self, ParseStream, Parser}, + spanned::Spanned, + Expr, ExprArray, Fields, ForeignItem, Ident, Item, LitBool, Path, Token, Visibility, +}; + +use super::Input; +use crate::syntax::{ + ast::{ + App, AppArgs, Dispatcher, Dispatchers, HardwareTask, Idle, IdleArgs, Init, InitArgs, + LocalResource, SharedResource, SoftwareTask, + }, + parse::{self as syntax_parse, util}, + Either, Map, Set, +}; + +impl AppArgs { + pub(crate) fn parse(tokens: TokenStream2) -> parse::Result<Self> { + (|input: ParseStream<'_>| -> parse::Result<Self> { + let mut custom = Set::new(); + let mut device = None; + let mut peripherals = true; + let mut dispatchers = Dispatchers::new(); + + loop { + if input.is_empty() { + break; + } + + // #ident = .. + let ident: Ident = input.parse()?; + let _eq_token: Token![=] = input.parse()?; + + if custom.contains(&ident) { + return Err(parse::Error::new( + ident.span(), + "argument appears more than once", + )); + } + + custom.insert(ident.clone()); + + let ks = ident.to_string(); + + match &*ks { + "device" => { + if let Ok(p) = input.parse::<Path>() { + device = Some(p); + } else { + return Err(parse::Error::new( + ident.span(), + "unexpected argument value; this should be a path", + )); + } + } + + "peripherals" => { + if let Ok(p) = input.parse::<LitBool>() { + peripherals = p.value; + } else { + return Err(parse::Error::new( + ident.span(), + "unexpected argument value; this should be a boolean", + )); + } + } + + "dispatchers" => { + if let Ok(p) = input.parse::<ExprArray>() { + for e in p.elems { + match e { + Expr::Path(ep) => { + let path = ep.path; + let ident = if path.leading_colon.is_some() + || path.segments.len() != 1 + { + return Err(parse::Error::new( + path.span(), + "interrupt must be an identifier, not a path", + )); + } else { + path.segments[0].ident.clone() + }; + let span = ident.span(); + if dispatchers.contains_key(&ident) { + return Err(parse::Error::new( + span, + "this extern interrupt is listed more than once", + )); + } else { + dispatchers + .insert(ident, Dispatcher { attrs: ep.attrs }); + } + } + _ => { + return Err(parse::Error::new( + e.span(), + "interrupt must be an identifier", + )); + } + } + } + } else { + return Err(parse::Error::new( + ident.span(), + // increasing the length of the error message will break rustfmt + "unexpected argument value; expected an array", + )); + } + } + _ => { + return Err(parse::Error::new(ident.span(), "unexpected argument")); + } + } + + if input.is_empty() { + break; + } + + // , + let _: Token![,] = input.parse()?; + } + + let device = if let Some(device) = device { + device + } else { + return Err(parse::Error::new(input.span(), "missing `device = ...`")); + }; + + Ok(AppArgs { + device, + peripherals, + dispatchers, + }) + }) + .parse2(tokens) + } +} + +impl App { + pub(crate) fn parse(args: AppArgs, input: Input) -> parse::Result<Self> { + let mut init = None; + let mut idle = None; + + let mut shared_resources_ident = None; + let mut shared_resources = Map::new(); + let mut local_resources_ident = None; + let mut local_resources = Map::new(); + let mut hardware_tasks = Map::new(); + let mut software_tasks = Map::new(); + let mut user_imports = vec![]; + let mut user_code = vec![]; + + let mut seen_idents = HashSet::<Ident>::new(); + let mut bindings = HashSet::<Ident>::new(); + + let mut check_binding = |ident: &Ident| { + if bindings.contains(ident) { + return Err(parse::Error::new( + ident.span(), + "this interrupt is already bound", + )); + } else { + bindings.insert(ident.clone()); + } + + Ok(()) + }; + + let mut check_ident = |ident: &Ident| { + if seen_idents.contains(ident) { + return Err(parse::Error::new( + ident.span(), + "this identifier has already been used", + )); + } else { + seen_idents.insert(ident.clone()); + } + + Ok(()) + }; + + for mut item in input.items { + match item { + Item::Fn(mut item) => { + let span = item.sig.ident.span(); + if let Some(pos) = item + .attrs + .iter() + .position(|attr| util::attr_eq(attr, "init")) + { + let args = InitArgs::parse(item.attrs.remove(pos).tokens)?; + + // If an init function already exists, error + if init.is_some() { + return Err(parse::Error::new( + span, + "`#[init]` function must appear at most once", + )); + } + + check_ident(&item.sig.ident)?; + + init = Some(Init::parse(args, item)?); + } else if let Some(pos) = item + .attrs + .iter() + .position(|attr| util::attr_eq(attr, "idle")) + { + let args = IdleArgs::parse(item.attrs.remove(pos).tokens)?; + + // If an idle function already exists, error + if idle.is_some() { + return Err(parse::Error::new( + span, + "`#[idle]` function must appear at most once", + )); + } + + check_ident(&item.sig.ident)?; + + idle = Some(Idle::parse(args, item)?); + } else if let Some(pos) = item + .attrs + .iter() + .position(|attr| util::attr_eq(attr, "task")) + { + if hardware_tasks.contains_key(&item.sig.ident) + || software_tasks.contains_key(&item.sig.ident) + { + return Err(parse::Error::new( + span, + "this task is defined multiple times", + )); + } + + match syntax_parse::task_args(item.attrs.remove(pos).tokens)? { + Either::Left(args) => { + check_binding(&args.binds)?; + check_ident(&item.sig.ident)?; + + hardware_tasks.insert( + item.sig.ident.clone(), + HardwareTask::parse(args, item)?, + ); + } + + Either::Right(args) => { + check_ident(&item.sig.ident)?; + + software_tasks.insert( + item.sig.ident.clone(), + SoftwareTask::parse(args, item)?, + ); + } + } + } else { + // Forward normal functions + user_code.push(Item::Fn(item.clone())); + } + } + + Item::Struct(ref mut struct_item) => { + // Match structures with the attribute #[shared], name of structure is not + // important + if let Some(_pos) = struct_item + .attrs + .iter() + .position(|attr| util::attr_eq(attr, "shared")) + { + let span = struct_item.ident.span(); + + shared_resources_ident = Some(struct_item.ident.clone()); + + if !shared_resources.is_empty() { + return Err(parse::Error::new( + span, + "`#[shared]` 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 shared_resources.contains_key(ident) { + return Err(parse::Error::new( + ident.span(), + "this resource is listed more than once", + )); + } + + shared_resources.insert( + ident.clone(), + SharedResource::parse(field, ident.span())?, + ); + } + } else { + return Err(parse::Error::new( + struct_item.span(), + "this `struct` must have named fields", + )); + } + } else if let Some(_pos) = struct_item + .attrs + .iter() + .position(|attr| util::attr_eq(attr, "local")) + { + let span = struct_item.ident.span(); + + local_resources_ident = Some(struct_item.ident.clone()); + + if !local_resources.is_empty() { + return Err(parse::Error::new( + span, + "`#[local]` 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 local_resources.contains_key(ident) { + return Err(parse::Error::new( + ident.span(), + "this resource is listed more than once", + )); + } + + local_resources.insert( + ident.clone(), + LocalResource::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()); + } + } + + Item::ForeignMod(mod_) => { + if !util::abi_is_rust(&mod_.abi) { + return Err(parse::Error::new( + mod_.abi.extern_token.span(), + "this `extern` block must use the \"Rust\" ABI", + )); + } + + for item in mod_.items { + if let ForeignItem::Fn(mut item) = item { + let span = item.sig.ident.span(); + if let Some(pos) = item + .attrs + .iter() + .position(|attr| util::attr_eq(attr, "task")) + { + if hardware_tasks.contains_key(&item.sig.ident) + || software_tasks.contains_key(&item.sig.ident) + { + return Err(parse::Error::new( + span, + "this task is defined multiple times", + )); + } + + if item.attrs.len() != 1 { + return Err(parse::Error::new( + span, + "`extern` task required `#[task(..)]` attribute", + )); + } + + match syntax_parse::task_args(item.attrs.remove(pos).tokens)? { + Either::Left(args) => { + check_binding(&args.binds)?; + check_ident(&item.sig.ident)?; + + hardware_tasks.insert( + item.sig.ident.clone(), + HardwareTask::parse_foreign(args, item)?, + ); + } + + Either::Right(args) => { + check_ident(&item.sig.ident)?; + + software_tasks.insert( + item.sig.ident.clone(), + SoftwareTask::parse_foreign(args, item)?, + ); + } + } + } else { + return Err(parse::Error::new( + span, + "`extern` task required `#[task(..)]` attribute", + )); + } + } else { + return Err(parse::Error::new( + item.span(), + "this item must live outside the `#[app]` module", + )); + } + } + } + Item::Use(itemuse_) => { + // Store the user provided use-statements + user_imports.push(itemuse_.clone()); + } + _ => { + // Anything else within the module should not make any difference + user_code.push(item.clone()); + } + } + } + + let shared_resources_ident = + shared_resources_ident.expect("No `#[shared]` resource struct defined"); + let local_resources_ident = + local_resources_ident.expect("No `#[local]` resource struct defined"); + let init = init.expect("No `#[init]` function defined"); + + if shared_resources_ident != init.user_shared_struct { + return Err(parse::Error::new( + init.user_shared_struct.span(), + format!( + "This name and the one defined on `#[shared]` are not the same. Should this be `{shared_resources_ident}`?" + ), + )); + } + + if local_resources_ident != init.user_local_struct { + return Err(parse::Error::new( + init.user_local_struct.span(), + format!( + "This name and the one defined on `#[local]` are not the same. Should this be `{local_resources_ident}`?" + ), + )); + } + + Ok(App { + args, + name: input.ident, + init, + idle, + shared_resources, + local_resources, + user_imports, + user_code, + hardware_tasks, + software_tasks, + }) + } +} diff --git a/rtic-macros/src/syntax/parse/hardware_task.rs b/rtic-macros/src/syntax/parse/hardware_task.rs new file mode 100644 index 0000000..7f6dfbe --- /dev/null +++ b/rtic-macros/src/syntax/parse/hardware_task.rs @@ -0,0 +1,76 @@ +use syn::{parse, ForeignItemFn, ItemFn, Stmt}; + +use crate::syntax::parse::util::FilterAttrs; +use crate::syntax::{ + ast::{HardwareTask, HardwareTaskArgs}, + parse::util, +}; + +impl HardwareTask { + pub(crate) fn parse(args: HardwareTaskArgs, item: ItemFn) -> parse::Result<Self> { + let span = item.sig.ident.span(); + let valid_signature = util::check_fn_signature(&item, false) + && item.sig.inputs.len() == 1 + && util::type_is_unit(&item.sig.output); + + let name = item.sig.ident.to_string(); + + if valid_signature { + if let Some((context, Ok(rest))) = util::parse_inputs(item.sig.inputs, &name) { + if rest.is_empty() { + let FilterAttrs { cfgs, attrs, .. } = util::filter_attributes(item.attrs); + + return Ok(HardwareTask { + args, + cfgs, + attrs, + context, + stmts: item.block.stmts, + is_extern: false, + }); + } + } + } + + Err(parse::Error::new( + span, + format!("this task handler must have type signature `fn({name}::Context)`"), + )) + } +} + +impl HardwareTask { + pub(crate) fn parse_foreign( + args: HardwareTaskArgs, + item: ForeignItemFn, + ) -> parse::Result<Self> { + let span = item.sig.ident.span(); + let valid_signature = util::check_foreign_fn_signature(&item, false) + && item.sig.inputs.len() == 1 + && util::type_is_unit(&item.sig.output); + + let name = item.sig.ident.to_string(); + + if valid_signature { + if let Some((context, Ok(rest))) = util::parse_inputs(item.sig.inputs, &name) { + if rest.is_empty() { + let FilterAttrs { cfgs, attrs, .. } = util::filter_attributes(item.attrs); + + return Ok(HardwareTask { + args, + cfgs, + attrs, + context, + stmts: Vec::<Stmt>::new(), + is_extern: true, + }); + } + } + } + + Err(parse::Error::new( + span, + format!("this task handler must have type signature `fn({name}::Context)`"), + )) + } +} diff --git a/rtic-macros/src/syntax/parse/idle.rs b/rtic-macros/src/syntax/parse/idle.rs new file mode 100644 index 0000000..124c136 --- /dev/null +++ b/rtic-macros/src/syntax/parse/idle.rs @@ -0,0 +1,42 @@ +use proc_macro2::TokenStream as TokenStream2; +use syn::{parse, ItemFn}; + +use crate::syntax::{ + ast::{Idle, IdleArgs}, + parse::util, +}; + +impl IdleArgs { + pub(crate) fn parse(tokens: TokenStream2) -> parse::Result<Self> { + crate::syntax::parse::idle_args(tokens) + } +} + +impl Idle { + pub(crate) fn parse(args: IdleArgs, item: ItemFn) -> parse::Result<Self> { + let valid_signature = util::check_fn_signature(&item, false) + && item.sig.inputs.len() == 1 + && util::type_is_bottom(&item.sig.output); + + let name = item.sig.ident.to_string(); + + if valid_signature { + if let Some((context, Ok(rest))) = util::parse_inputs(item.sig.inputs, &name) { + if rest.is_empty() { + return Ok(Idle { + args, + attrs: item.attrs, + context, + name: item.sig.ident, + stmts: item.block.stmts, + }); + } + } + } + + Err(parse::Error::new( + item.sig.ident.span(), + format!("this `#[idle]` function must have signature `fn({name}::Context) -> !`"), + )) + } +} diff --git a/rtic-macros/src/syntax/parse/init.rs b/rtic-macros/src/syntax/parse/init.rs new file mode 100644 index 0000000..0aea20b --- /dev/null +++ b/rtic-macros/src/syntax/parse/init.rs @@ -0,0 +1,51 @@ +use proc_macro2::TokenStream as TokenStream2; + +use syn::{parse, ItemFn}; + +use crate::syntax::{ + ast::{Init, InitArgs}, + parse::{self as syntax_parse, util}, +}; + +impl InitArgs { + pub(crate) fn parse(tokens: TokenStream2) -> parse::Result<Self> { + syntax_parse::init_args(tokens) + } +} + +impl Init { + pub(crate) fn parse(args: InitArgs, item: ItemFn) -> parse::Result<Self> { + let valid_signature = util::check_fn_signature(&item, false) && item.sig.inputs.len() == 1; + + let span = item.sig.ident.span(); + + let name = item.sig.ident.to_string(); + + if valid_signature { + if let Ok((user_shared_struct, user_local_struct)) = + util::type_is_init_return(&item.sig.output) + { + if let Some((context, Ok(rest))) = util::parse_inputs(item.sig.inputs, &name) { + if rest.is_empty() { + return Ok(Init { + args, + attrs: item.attrs, + context, + name: item.sig.ident, + stmts: item.block.stmts, + user_shared_struct, + user_local_struct, + }); + } + } + } + } + + Err(parse::Error::new( + span, + format!( + "the `#[init]` function must have signature `fn({name}::Context) -> (Shared resources struct, Local resources struct)`" + ), + )) + } +} diff --git a/rtic-macros/src/syntax/parse/resource.rs b/rtic-macros/src/syntax/parse/resource.rs new file mode 100644 index 0000000..ff10057 --- /dev/null +++ b/rtic-macros/src/syntax/parse/resource.rs @@ -0,0 +1,55 @@ +use proc_macro2::Span; +use syn::{parse, Field, Visibility}; + +use crate::syntax::parse::util::FilterAttrs; +use crate::syntax::{ + ast::{LocalResource, SharedResource, SharedResourceProperties}, + parse::util, +}; + +impl SharedResource { + pub(crate) fn parse(item: &Field, span: Span) -> parse::Result<Self> { + if item.vis != Visibility::Inherited { + return Err(parse::Error::new( + span, + "this field must have inherited / private visibility", + )); + } + + let FilterAttrs { + cfgs, + mut attrs, + docs, + } = util::filter_attributes(item.attrs.clone()); + + let lock_free = util::extract_lock_free(&mut attrs)?; + + Ok(SharedResource { + cfgs, + attrs, + docs, + ty: Box::new(item.ty.clone()), + properties: SharedResourceProperties { lock_free }, + }) + } +} + +impl LocalResource { + pub(crate) fn parse(item: &Field, span: Span) -> parse::Result<Self> { + if item.vis != Visibility::Inherited { + return Err(parse::Error::new( + span, + "this field must have inherited / private visibility", + )); + } + + let FilterAttrs { cfgs, attrs, docs } = util::filter_attributes(item.attrs.clone()); + + Ok(LocalResource { + cfgs, + attrs, + docs, + ty: Box::new(item.ty.clone()), + }) + } +} diff --git a/rtic-macros/src/syntax/parse/software_task.rs b/rtic-macros/src/syntax/parse/software_task.rs new file mode 100644 index 0000000..769aa65 --- /dev/null +++ b/rtic-macros/src/syntax/parse/software_task.rs @@ -0,0 +1,76 @@ +use syn::{parse, ForeignItemFn, ItemFn, Stmt}; + +use crate::syntax::parse::util::FilterAttrs; +use crate::syntax::{ + ast::{SoftwareTask, SoftwareTaskArgs}, + parse::util, +}; + +impl SoftwareTask { + pub(crate) fn parse(args: SoftwareTaskArgs, item: ItemFn) -> parse::Result<Self> { + let valid_signature = util::check_fn_signature(&item, true) + && util::type_is_unit(&item.sig.output) + && item.sig.asyncness.is_some(); + + let span = item.sig.ident.span(); + + let name = item.sig.ident.to_string(); + + if valid_signature { + if let Some((context, Ok(inputs))) = util::parse_inputs(item.sig.inputs, &name) { + let FilterAttrs { cfgs, attrs, .. } = util::filter_attributes(item.attrs); + + return Ok(SoftwareTask { + args, + attrs, + cfgs, + context, + inputs, + stmts: item.block.stmts, + is_extern: false, + }); + } + } + + Err(parse::Error::new( + span, + format!("this task handler must have type signature `async fn({name}::Context, ..)`"), + )) + } +} + +impl SoftwareTask { + pub(crate) fn parse_foreign( + args: SoftwareTaskArgs, + item: ForeignItemFn, + ) -> parse::Result<Self> { + let valid_signature = util::check_foreign_fn_signature(&item, true) + && util::type_is_unit(&item.sig.output) + && item.sig.asyncness.is_some(); + + let span = item.sig.ident.span(); + + let name = item.sig.ident.to_string(); + + if valid_signature { + if let Some((context, Ok(inputs))) = util::parse_inputs(item.sig.inputs, &name) { + let FilterAttrs { cfgs, attrs, .. } = util::filter_attributes(item.attrs); + + return Ok(SoftwareTask { + args, + attrs, + cfgs, + context, + inputs, + stmts: Vec::<Stmt>::new(), + is_extern: true, + }); + } + } + + Err(parse::Error::new( + span, + format!("this task handler must have type signature `async fn({name}::Context, ..)`"), + )) + } +} diff --git a/rtic-macros/src/syntax/parse/util.rs b/rtic-macros/src/syntax/parse/util.rs new file mode 100644 index 0000000..5a5e0c0 --- /dev/null +++ b/rtic-macros/src/syntax/parse/util.rs @@ -0,0 +1,338 @@ +use syn::{ + bracketed, + parse::{self, ParseStream}, + punctuated::Punctuated, + spanned::Spanned, + Abi, AttrStyle, Attribute, Expr, FnArg, ForeignItemFn, Ident, ItemFn, Pat, PatType, Path, + PathArguments, ReturnType, Token, Type, Visibility, +}; + +use crate::syntax::{ + ast::{Access, Local, LocalResources, SharedResources, TaskLocal}, + Map, +}; + +pub fn abi_is_rust(abi: &Abi) -> bool { + match &abi.name { + None => true, + Some(s) => s.value() == "Rust", + } +} + +pub fn attr_eq(attr: &Attribute, name: &str) -> bool { + attr.style == AttrStyle::Outer && attr.path.segments.len() == 1 && { + let segment = attr.path.segments.first().unwrap(); + segment.arguments == PathArguments::None && *segment.ident.to_string() == *name + } +} + +/// checks that a function signature +/// +/// - has no bounds (like where clauses) +/// - is not `async` +/// - is not `const` +/// - is not `unsafe` +/// - is not generic (has no type parameters) +/// - is not variadic +/// - uses the Rust ABI (and not e.g. "C") +pub fn check_fn_signature(item: &ItemFn, allow_async: bool) -> bool { + item.vis == Visibility::Inherited + && item.sig.constness.is_none() + && (item.sig.asyncness.is_none() || allow_async) + && item.sig.abi.is_none() + && item.sig.unsafety.is_none() + && item.sig.generics.params.is_empty() + && item.sig.generics.where_clause.is_none() + && item.sig.variadic.is_none() +} + +#[allow(dead_code)] +pub fn check_foreign_fn_signature(item: &ForeignItemFn, allow_async: bool) -> bool { + item.vis == Visibility::Inherited + && item.sig.constness.is_none() + && (item.sig.asyncness.is_none() || allow_async) + && item.sig.abi.is_none() + && item.sig.unsafety.is_none() + && item.sig.generics.params.is_empty() + && item.sig.generics.where_clause.is_none() + && item.sig.variadic.is_none() +} + +pub struct FilterAttrs { + pub cfgs: Vec<Attribute>, + pub docs: Vec<Attribute>, + pub attrs: Vec<Attribute>, +} + +pub fn filter_attributes(input_attrs: Vec<Attribute>) -> FilterAttrs { + let mut cfgs = vec![]; + let mut docs = vec![]; + let mut attrs = vec![]; + + for attr in input_attrs { + if attr_eq(&attr, "cfg") { + cfgs.push(attr); + } else if attr_eq(&attr, "doc") { + docs.push(attr); + } else { + attrs.push(attr); + } + } + + FilterAttrs { cfgs, docs, attrs } +} + +pub fn extract_lock_free(attrs: &mut Vec<Attribute>) -> parse::Result<bool> { + if let Some(pos) = attrs.iter().position(|attr| attr_eq(attr, "lock_free")) { + attrs.remove(pos); + Ok(true) + } else { + Ok(false) + } +} + +pub fn parse_shared_resources(content: ParseStream<'_>) -> parse::Result<SharedResources> { + let inner; + bracketed!(inner in content); + + let mut resources = Map::new(); + for e in inner.call(Punctuated::<Expr, Token![,]>::parse_terminated)? { + let err = Err(parse::Error::new( + e.span(), + "identifier appears more than once in list", + )); + let (access, path) = match e { + Expr::Path(e) => (Access::Exclusive, e.path), + + Expr::Reference(ref r) if r.mutability.is_none() => match &*r.expr { + Expr::Path(e) => (Access::Shared, e.path.clone()), + + _ => return err, + }, + + _ => return err, + }; + + let ident = extract_resource_name_ident(path)?; + + if resources.contains_key(&ident) { + return Err(parse::Error::new( + ident.span(), + "resource appears more than once in list", + )); + } + + resources.insert(ident, access); + } + + Ok(resources) +} + +fn extract_resource_name_ident(path: Path) -> parse::Result<Ident> { + if path.leading_colon.is_some() + || path.segments.len() != 1 + || path.segments[0].arguments != PathArguments::None + { + Err(parse::Error::new( + path.span(), + "resource must be an identifier, not a path", + )) + } else { + Ok(path.segments[0].ident.clone()) + } +} + +pub fn parse_local_resources(content: ParseStream<'_>) -> parse::Result<LocalResources> { + let inner; + bracketed!(inner in content); + + let mut resources = Map::new(); + + for e in inner.call(Punctuated::<Expr, Token![,]>::parse_terminated)? { + let err = Err(parse::Error::new( + e.span(), + "identifier appears more than once in list", + )); + + let (name, local) = match e { + // local = [IDENT], + Expr::Path(path) => { + if !path.attrs.is_empty() { + return Err(parse::Error::new( + path.span(), + "attributes are not supported here", + )); + } + + let ident = extract_resource_name_ident(path.path)?; + // let (cfgs, attrs) = extract_cfgs(path.attrs); + + (ident, TaskLocal::External) + } + + // local = [IDENT: TYPE = EXPR] + Expr::Assign(e) => { + let (name, ty, cfgs, attrs) = match *e.left { + Expr::Type(t) => { + // Extract name and attributes + let (name, cfgs, attrs) = match *t.expr { + Expr::Path(path) => { + let name = extract_resource_name_ident(path.path)?; + let FilterAttrs { cfgs, attrs, .. } = filter_attributes(path.attrs); + + (name, cfgs, attrs) + } + _ => return err, + }; + + let ty = t.ty; + + // Error check + match &*ty { + Type::Array(_) => {} + Type::Path(_) => {} + Type::Ptr(_) => {} + Type::Tuple(_) => {} + _ => return Err(parse::Error::new( + ty.span(), + "unsupported type, must be an array, tuple, pointer or type path", + )), + }; + + (name, ty, cfgs, attrs) + } + e => return Err(parse::Error::new(e.span(), "malformed, expected a type")), + }; + + let expr = e.right; // Expr + + ( + name, + TaskLocal::Declared(Local { + attrs, + cfgs, + ty, + expr, + }), + ) + } + + expr => { + return Err(parse::Error::new( + expr.span(), + "malformed, expected 'IDENT: TYPE = EXPR'", + )) + } + }; + + resources.insert(name, local); + } + + Ok(resources) +} + +type ParseInputResult = Option<(Box<Pat>, Result<Vec<PatType>, FnArg>)>; + +pub fn parse_inputs(inputs: Punctuated<FnArg, Token![,]>, name: &str) -> ParseInputResult { + let mut inputs = inputs.into_iter(); + + match inputs.next() { + Some(FnArg::Typed(first)) => { + if type_is_path(&first.ty, &[name, "Context"]) { + let rest = inputs + .map(|arg| match arg { + FnArg::Typed(arg) => Ok(arg), + _ => Err(arg), + }) + .collect::<Result<Vec<_>, _>>(); + + Some((first.pat, rest)) + } else { + None + } + } + + _ => None, + } +} + +pub fn type_is_bottom(ty: &ReturnType) -> bool { + if let ReturnType::Type(_, ty) = ty { + matches!(**ty, Type::Never(_)) + } else { + false + } +} + +fn extract_init_resource_name_ident(ty: Type) -> Result<Ident, ()> { + match ty { + Type::Path(path) => { + let path = path.path; + + if path.leading_colon.is_some() + || path.segments.len() != 1 + || path.segments[0].arguments != PathArguments::None + { + Err(()) + } else { + Ok(path.segments[0].ident.clone()) + } + } + _ => Err(()), + } +} + +/// Checks Init's return type, return the user provided types for analysis +pub fn type_is_init_return(ty: &ReturnType) -> Result<(Ident, Ident), ()> { + 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) + // + // We check the length and the last one here, analysis checks that the user + // provided structs are correct. + if t.elems.len() == 2 { + return Ok(( + extract_init_resource_name_ident(t.elems[0].clone())?, + extract_init_resource_name_ident(t.elems[1].clone())?, + )); + } + + Err(()) + } + + _ => Err(()), + }, + } +} + +pub fn type_is_path(ty: &Type, segments: &[&str]) -> bool { + match ty { + Type::Path(tpath) if tpath.qself.is_none() => { + tpath.path.segments.len() == segments.len() + && tpath + .path + .segments + .iter() + .zip(segments) + .all(|(lhs, rhs)| lhs.ident == **rhs) + } + + _ => false, + } +} + +pub fn type_is_unit(ty: &ReturnType) -> bool { + if let ReturnType::Type(_, ty) = ty { + if let Type::Tuple(ref tuple) = **ty { + tuple.elems.is_empty() + } else { + false + } + } else { + true + } +} |
