rtic_macros/syntax/
analyze.rs

1//! RTIC application analysis
2
3use core::cmp;
4use std::collections::{BTreeMap, BTreeSet, HashMap};
5
6use indexmap::{IndexMap, IndexSet};
7use syn::{Ident, Type};
8
9use crate::syntax::{
10    ast::{App, LocalResources, TaskLocal},
11    Set,
12};
13
14pub(crate) fn app(app: &App) -> Result<Analysis, syn::Error> {
15    // Collect all tasks into a vector
16    type TaskName = Ident;
17    type Priority = u8;
18
19    // The task list is a Tuple (Name, Shared Resources, Local Resources, Priority)
20    let task_resources_list: Vec<(TaskName, Vec<&Ident>, &LocalResources, Priority)> =
21        Some(&app.init)
22            .iter()
23            .map(|ht| (ht.name.clone(), Vec::new(), &ht.args.local_resources, 0))
24            .chain(app.idle.iter().map(|ht| {
25                (
26                    ht.name.clone(),
27                    ht.args
28                        .shared_resources
29                        .iter()
30                        .map(|(v, _)| v)
31                        .collect::<Vec<_>>(),
32                    &ht.args.local_resources,
33                    0,
34                )
35            }))
36            .chain(app.software_tasks.iter().map(|(name, ht)| {
37                (
38                    name.clone(),
39                    ht.args
40                        .shared_resources
41                        .iter()
42                        .map(|(v, _)| v)
43                        .collect::<Vec<_>>(),
44                    &ht.args.local_resources,
45                    ht.args.priority,
46                )
47            }))
48            .chain(app.hardware_tasks.iter().map(|(name, ht)| {
49                (
50                    name.clone(),
51                    ht.args
52                        .shared_resources
53                        .iter()
54                        .map(|(v, _)| v)
55                        .collect::<Vec<_>>(),
56                    &ht.args.local_resources,
57                    ht.args.priority,
58                )
59            }))
60            .collect();
61
62    let mut error = vec![];
63    let mut lf_res_with_error = vec![];
64    let mut lf_hash = HashMap::new();
65
66    // Collect lock free resources
67    let lock_free: Vec<&Ident> = app
68        .shared_resources
69        .iter()
70        .filter(|(_, r)| r.properties.lock_free)
71        .map(|(i, _)| i)
72        .collect();
73
74    // Check that lock_free resources are correct
75    for lf_res in lock_free.iter() {
76        for (task, tr, _, priority) in task_resources_list.iter() {
77            for r in tr {
78                // Get all uses of resources annotated lock_free
79                if lf_res == r {
80                    // Check so async tasks do not use lock free resources
81                    if app.software_tasks.get(task).is_some() {
82                        error.push(syn::Error::new(
83                            r.span(),
84                            format!(
85                                "Lock free shared resource {:?} is used by an async tasks, which is forbidden",
86                                r.to_string(),
87                            ),
88                        ));
89                    }
90
91                    // HashMap returns the previous existing object if old.key == new.key
92                    if let Some(lf_res) = lf_hash.insert(r.to_string(), (task, r, priority)) {
93                        // Check if priority differ, if it does, append to
94                        // list of resources which will be annotated with errors
95                        if priority != lf_res.2 {
96                            lf_res_with_error.push(lf_res.1);
97                            lf_res_with_error.push(r);
98                        }
99
100                        // If the resource already violates lock free properties
101                        if lf_res_with_error.contains(&r) {
102                            lf_res_with_error.push(lf_res.1);
103                            lf_res_with_error.push(r);
104                        }
105                    }
106                }
107            }
108        }
109    }
110
111    // Add error message in the resource struct
112    for r in lock_free {
113        if lf_res_with_error.contains(&&r) {
114            error.push(syn::Error::new(
115                r.span(),
116                format!(
117                    "Lock free shared resource {:?} is used by tasks at different priorities",
118                    r.to_string(),
119                ),
120            ));
121        }
122    }
123
124    // Add error message for each use of the shared resource
125    for resource in lf_res_with_error.clone() {
126        error.push(syn::Error::new(
127            resource.span(),
128            format!(
129                "Shared resource {:?} is declared lock free but used by tasks at different priorities",
130                resource.to_string(),
131            ),
132        ));
133    }
134
135    // Collect local resources
136    let local: Vec<&Ident> = app.local_resources.iter().map(|(i, _)| i).collect();
137
138    let mut lr_with_error = vec![];
139    let mut lr_hash = HashMap::new();
140
141    // Check that local resources are not shared
142    for lr in local {
143        for (task, _, local_resources, _) in task_resources_list.iter() {
144            for (name, res) in local_resources.iter() {
145                // Get all uses of resources annotated lock_free
146                if lr == name {
147                    match res {
148                        TaskLocal::External => {
149                            // HashMap returns the previous existing object if old.key == new.key
150                            if let Some(lr) = lr_hash.insert(name.to_string(), (task, name)) {
151                                lr_with_error.push(lr.1);
152                                lr_with_error.push(name);
153                            }
154                        }
155                        // If a declared local has the same name as the `#[local]` struct, it's an
156                        // direct error
157                        TaskLocal::Declared(_) => {
158                            lr_with_error.push(lr);
159                            lr_with_error.push(name);
160                        }
161                    }
162                }
163            }
164        }
165    }
166
167    // Add error message for each use of the local resource
168    for resource in lr_with_error.clone() {
169        error.push(syn::Error::new(
170            resource.span(),
171            format!(
172                "Local resource {:?} is used by multiple tasks or collides with multiple definitions",
173                resource.to_string(),
174            ),
175        ));
176    }
177
178    // Check 0-priority async software tasks and idle dependency
179    for (name, task) in &app.software_tasks {
180        if task.args.priority == 0 {
181            // If there is a 0-priority task, there must be no idle
182            if app.idle.is_some() {
183                error.push(syn::Error::new(
184                    name.span(),
185                    format!(
186                        "Async task {:?} has priority 0, but `#[idle]` is defined. 0-priority async tasks are only allowed if there is no `#[idle]`.",
187                        name.to_string(),
188                    )
189                ));
190            }
191        }
192    }
193
194    // Collect errors if any and return/halt
195    if !error.is_empty() {
196        let mut err = error.first().unwrap().clone();
197        error.iter().for_each(|e| err.combine(e.clone()));
198        return Err(err);
199    }
200
201    // e. Location of resources
202    let mut used_shared_resource = IndexSet::new();
203    let mut ownerships = Ownerships::new();
204    let mut sync_types = SyncTypes::new();
205    for (prio, name, access) in app.shared_resource_accesses() {
206        let res = app.shared_resources.get(name).expect("UNREACHABLE");
207
208        // (e)
209        // This shared resource is used
210        used_shared_resource.insert(name.clone());
211
212        // (c)
213        if let Some(priority) = prio {
214            if let Some(ownership) = ownerships.get_mut(name) {
215                match *ownership {
216                    Ownership::Owned { priority: ceiling }
217                    | Ownership::CoOwned { priority: ceiling }
218                    | Ownership::Contended { ceiling }
219                        if priority != ceiling =>
220                    {
221                        *ownership = Ownership::Contended {
222                            ceiling: cmp::max(ceiling, priority),
223                        };
224
225                        if access.is_shared() {
226                            sync_types.insert(res.ty.clone());
227                        }
228                    }
229
230                    Ownership::Owned { priority: ceil } if ceil == priority => {
231                        *ownership = Ownership::CoOwned { priority };
232                    }
233
234                    _ => {}
235                }
236            } else {
237                ownerships.insert(name.clone(), Ownership::Owned { priority });
238            }
239        }
240    }
241
242    // Create the list of used local resource Idents
243    let mut used_local_resource = IndexSet::new();
244
245    for (_, _, locals, _) in task_resources_list {
246        for (local, _) in locals {
247            used_local_resource.insert(local.clone());
248        }
249    }
250
251    // Most shared resources need to be `Send`, only 0 prio does not need it
252    let mut send_types = SendTypes::new();
253
254    for (name, res) in app.shared_resources.iter() {
255        if ownerships
256            .get(name)
257            .map(|ownership| match *ownership {
258                Ownership::Owned { priority: ceiling }
259                | Ownership::CoOwned { priority: ceiling }
260                | Ownership::Contended { ceiling } => ceiling != 0,
261            })
262            .unwrap_or(false)
263        {
264            send_types.insert(res.ty.clone());
265        }
266    }
267
268    // Most local resources need to be `Send` as well, only 0 prio does not need it
269    for (name, res) in app.local_resources.iter() {
270        if ownerships
271            .get(name)
272            .map(|ownership| match *ownership {
273                Ownership::Owned { priority: ceiling }
274                | Ownership::CoOwned { priority: ceiling }
275                | Ownership::Contended { ceiling } => ceiling != 0,
276            })
277            .unwrap_or(false)
278        {
279            send_types.insert(res.ty.clone());
280        }
281    }
282
283    let mut channels = Channels::new();
284
285    for (name, spawnee) in &app.software_tasks {
286        let spawnee_prio = spawnee.args.priority;
287
288        let channel = channels.entry(spawnee_prio).or_default();
289        channel.tasks.insert(name.clone());
290
291        // All inputs are send as we do not know from where they may be spawned.
292        spawnee.inputs.iter().for_each(|input| {
293            send_types.insert(input.ty.clone());
294        });
295    }
296
297    // No channel should ever be empty
298    debug_assert!(channels.values().all(|channel| !channel.tasks.is_empty()));
299
300    Ok(Analysis {
301        channels,
302        shared_resources: used_shared_resource,
303        local_resources: used_local_resource,
304        ownerships,
305        send_types,
306        sync_types,
307    })
308}
309
310// /// Priority ceiling
311// pub type Ceiling = Option<u8>;
312
313/// Task priority
314pub type Priority = u8;
315
316/// Resource name
317pub type Resource = Ident;
318
319/// Task name
320pub type Task = Ident;
321
322/// The result of analyzing an RTIC application
323pub struct Analysis {
324    /// SPSC message channels
325    pub channels: Channels,
326
327    /// Shared resources
328    ///
329    /// If a resource is not listed here it means that's a "dead" (never
330    /// accessed) resource and the backend should not generate code for it
331    pub shared_resources: UsedSharedResource,
332
333    /// Local resources
334    ///
335    /// If a resource is not listed here it means that's a "dead" (never
336    /// accessed) resource and the backend should not generate code for it
337    pub local_resources: UsedLocalResource,
338
339    /// Resource ownership
340    pub ownerships: Ownerships,
341
342    /// These types must implement the `Send` trait
343    pub send_types: SendTypes,
344
345    /// These types must implement the `Sync` trait
346    pub sync_types: SyncTypes,
347}
348
349/// All channels, keyed by dispatch priority
350pub type Channels = BTreeMap<Priority, Channel>;
351
352/// Location of all *used* shared resources
353pub type UsedSharedResource = IndexSet<Resource>;
354
355/// Location of all *used* local resources
356pub type UsedLocalResource = IndexSet<Resource>;
357
358/// Resource ownership
359pub type Ownerships = IndexMap<Resource, Ownership>;
360
361/// These types must implement the `Send` trait
362pub type SendTypes = Set<Box<Type>>;
363
364/// These types must implement the `Sync` trait
365pub type SyncTypes = Set<Box<Type>>;
366
367/// A channel used to send messages
368#[derive(Debug, Default)]
369pub struct Channel {
370    /// Tasks that can be spawned on this channel
371    pub tasks: BTreeSet<Task>,
372}
373
374/// Resource ownership
375#[derive(Clone, Copy, Debug, PartialEq)]
376pub enum Ownership {
377    /// Owned by a single task
378    Owned {
379        /// Priority of the task that owns this resource
380        priority: u8,
381    },
382
383    /// "Co-owned" by more than one task; all of them have the same priority
384    CoOwned {
385        /// Priority of the tasks that co-own this resource
386        priority: u8,
387    },
388
389    /// Contended by more than one task; the tasks have different priorities
390    Contended {
391        /// Priority ceiling
392        ceiling: u8,
393    },
394}
395
396// impl Ownership {
397//     /// Whether this resource needs to a lock at this priority level
398//     pub fn needs_lock(&self, priority: u8) -> bool {
399//         match self {
400//             Ownership::Owned { .. } | Ownership::CoOwned { .. } => false,
401//
402//             Ownership::Contended { ceiling } => {
403//                 debug_assert!(*ceiling >= priority);
404//
405//                 priority < *ceiling
406//             }
407//         }
408//     }
409//
410//     /// Whether this resource is exclusively owned
411//     pub fn is_owned(&self) -> bool {
412//         matches!(self, Ownership::Owned { .. })
413//     }
414// }