Skip to main content

ferroforge_contracts/
lib.rs

1//! Source contracts shared by the procedural macro and host discovery.
2//!
3//! This crate parses declarations, not task-body semantics. It has no target
4//! HAL dependencies. Parsing the standalone syntax is not a checking expansion.
5
6use std::collections::BTreeSet;
7
8use syn::{
9    Error, FnArg, Ident, LitStr, Pat, Path, ReturnType, Signature, Token, Type, TypeParamBound,
10    bracketed, parenthesized,
11    parse::{Parse, ParseStream},
12    punctuated::Punctuated,
13    spanned::Spanned,
14};
15
16/// Rust raw and ordinary identifiers name the same declaration.
17pub fn identifier_key(name: &Ident) -> String {
18    name.to_string().trim_start_matches("r#").to_owned()
19}
20
21/// An inline resource or configuration entry. A resource needs either a type or
22/// a matching bound; `None` is the bare-name form, which `#[task]` rejects for
23/// resources because it cannot infer a field type from a name alone.
24#[derive(Clone, Debug)]
25pub struct Resource {
26    pub name: Ident,
27    pub ty: Option<Type>,
28}
29
30impl Parse for Resource {
31    fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
32        let name = input.parse()?;
33        let ty = if input.peek(Token![:]) {
34            input.parse::<Token![:]>()?;
35            Some(input.parse()?)
36        } else {
37            None
38        };
39        Ok(Self { name, ty })
40    }
41}
42
43#[derive(Clone, Debug)]
44pub struct ResourceBound {
45    pub name: Ident,
46    pub traits: Punctuated<TypeParamBound, Token![+]>,
47}
48
49impl Parse for ResourceBound {
50    fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
51        let name = input.parse()?;
52        input.parse::<Token![:]>()?;
53        let traits = Punctuated::parse_separated_nonempty(input)?;
54        Ok(Self { name, traits })
55    }
56}
57
58#[derive(Clone, Debug)]
59pub struct Parameter {
60    pub name: Ident,
61    pub ty: Type,
62}
63
64impl Parse for Parameter {
65    fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
66        let name = input.parse()?;
67        input.parse::<Token![:]>()?;
68        Ok(Self {
69            name,
70            ty: input.parse()?,
71        })
72    }
73}
74
75#[derive(Clone, Debug)]
76pub struct Spawn {
77    pub name: Ident,
78    /// `Some([])` is an explicit zero-input signature, `report()`. `None` is the
79    /// bare name `report`, which carries no signature to bound the closure with.
80    pub inputs: Option<Vec<Parameter>>,
81}
82
83impl Parse for Spawn {
84    fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
85        let name = input.parse()?;
86        let inputs = if input.peek(syn::token::Paren) {
87            let body;
88            parenthesized!(body in input);
89            Some(
90                Punctuated::<Parameter, Token![,]>::parse_terminated(&body)?
91                    .into_iter()
92                    .collect(),
93            )
94        } else {
95            None
96        };
97        Ok(Self { name, inputs })
98    }
99}
100
101/// Registry requirements are retained only for the existing prototype adapter.
102#[derive(Clone, Debug)]
103pub struct TaskDependency {
104    pub id: Ident,
105    pub features: Vec<LitStr>,
106}
107
108impl Parse for TaskDependency {
109    fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
110        let id = input.parse()?;
111        let mut features = Vec::new();
112        if input.peek(syn::token::Paren) {
113            let body;
114            parenthesized!(body in input);
115            let key: Ident = body.parse()?;
116            if key != "features" {
117                return Err(Error::new(key.span(), "expected `features`"));
118            }
119            body.parse::<Token![=]>()?;
120            features = list(&body)?;
121            if !body.is_empty() {
122                return Err(body.error("unexpected dependency requirement argument"));
123            }
124        }
125        Ok(Self { id, features })
126    }
127}
128
129#[derive(Clone, Debug, Default)]
130pub struct TaskArguments {
131    pub bounds: Vec<ResourceBound>,
132    pub local: Vec<Resource>,
133    pub shared: Vec<Resource>,
134    pub config: Vec<Resource>,
135    pub spawn: Vec<Spawn>,
136    /// Source reference, not the system's actual tick rate or hardware clock.
137    pub monotonic: Option<Path>,
138    pub dependencies: Vec<TaskDependency>,
139}
140
141fn list<T: Parse>(input: ParseStream<'_>) -> syn::Result<Vec<T>> {
142    let body;
143    bracketed!(body in input);
144    Ok(Punctuated::<T, Token![,]>::parse_terminated(&body)?
145        .into_iter()
146        .collect())
147}
148
149impl Parse for TaskArguments {
150    fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
151        let mut args = Self::default();
152        let mut keys = BTreeSet::new();
153        while !input.is_empty() {
154            let key: Ident = input.parse()?;
155            if !keys.insert(key.to_string()) {
156                return Err(Error::new(
157                    key.span(),
158                    format!("duplicate task argument `{key}`"),
159                ));
160            }
161            input.parse::<Token![=]>()?;
162            match key.to_string().as_str() {
163                "bounds" => args.bounds = list(input)?,
164                "local" => args.local = list(input)?,
165                "shared" => args.shared = list(input)?,
166                "config" => args.config = list(input)?,
167                "spawn" => args.spawn = list(input)?,
168                "monotonic" => args.monotonic = Some(input.parse()?),
169                "dependencies" => args.dependencies = list(input)?,
170                _ => {
171                    return Err(Error::new(
172                        key.span(),
173                        "unknown task argument; scheduling belongs in composition",
174                    ));
175                }
176            }
177            if !input.is_empty() {
178                input.parse::<Token![,]>()?;
179            }
180        }
181        Ok(args)
182    }
183}
184
185fn unique<'a>(names: impl IntoIterator<Item = &'a Ident>, message: &str) -> syn::Result<()> {
186    let mut seen = BTreeSet::new();
187    for name in names {
188        if !seen.insert(identifier_key(name)) {
189            return Err(Error::new(name.span(), message));
190        }
191    }
192    Ok(())
193}
194
195impl TaskArguments {
196    /// Structural validation of the declaration itself, before any expansion
197    /// reads it: duplicate names, empty categories, missing types.
198    pub fn validate(&self) -> syn::Result<()> {
199        unique(
200            self.local.iter().map(|r| &r.name),
201            "duplicate local resource",
202        )?;
203        unique(
204            self.shared.iter().map(|r| &r.name),
205            "duplicate shared resource",
206        )?;
207        unique(
208            self.config.iter().map(|r| &r.name),
209            "duplicate task configuration",
210        )?;
211        unique(
212            self.bounds.iter().map(|r| &r.name),
213            "duplicate resource bound",
214        )?;
215        unique(self.spawn.iter().map(|s| &s.name), "duplicate spawn alias")?;
216        unique(
217            self.dependencies.iter().map(|d| &d.id),
218            "duplicate task dependency",
219        )?;
220        for local in &self.local {
221            if self
222                .shared
223                .iter()
224                .any(|r| identifier_key(&r.name) == identifier_key(&local.name))
225            {
226                return Err(Error::new(
227                    local.name.span(),
228                    "a task resource cannot be both local and shared",
229                ));
230            }
231        }
232        for dependency in &self.dependencies {
233            let mut seen = BTreeSet::new();
234            for feature in &dependency.features {
235                if !seen.insert(feature.value()) {
236                    return Err(Error::new(
237                        feature.span(),
238                        "duplicate task dependency feature",
239                    ));
240                }
241            }
242        }
243        for spawn in &self.spawn {
244            if let Some(inputs) = &spawn.inputs {
245                unique(inputs.iter().map(|p| &p.name), "duplicate spawn parameter")?;
246            }
247        }
248        Ok(())
249    }
250
251    /// Check the independently authored SW contract. Rust still checks types.
252    pub fn validate_standalone(&self) -> syn::Result<()> {
253        self.validate()?;
254        if let Some(dependency) = self.dependencies.first() {
255            return Err(Error::new(
256                dependency.id.span(),
257                "standalone task dependencies belong in Cargo.toml",
258            ));
259        }
260        for bound in &self.bounds {
261            if !self
262                .local
263                .iter()
264                .chain(&self.shared)
265                .any(|r| identifier_key(&r.name) == identifier_key(&bound.name))
266            {
267                return Err(Error::new(
268                    bound.name.span(),
269                    "bounded resource must be claimed as local or shared",
270                ));
271            }
272            if bound
273                .traits
274                .iter()
275                .any(|b| !matches!(b, TypeParamBound::Trait(_)))
276            {
277                return Err(Error::new(
278                    bound.name.span(),
279                    "resource bounds must be trait bounds",
280                ));
281            }
282        }
283        for resource in self.local.iter().chain(&self.shared) {
284            let bounded = self
285                .bounds
286                .iter()
287                .any(|b| identifier_key(&b.name) == identifier_key(&resource.name));
288            if bounded == resource.ty.is_some() {
289                return Err(Error::new(
290                    resource.name.span(),
291                    "resource needs either an inline type or a resource-keyed bound, not both",
292                ));
293            }
294            if let Some(ty) = &resource.ty {
295                explicit_type(ty)?;
296            }
297        }
298        for config in &self.config {
299            if config.ty.is_none() {
300                return Err(Error::new(
301                    config.name.span(),
302                    "standalone configuration needs an inline type",
303                ));
304            }
305            explicit_type(config.ty.as_ref().expect("configuration type was checked"))?;
306        }
307        for spawn in &self.spawn {
308            if spawn.inputs.is_none() {
309                return Err(Error::new(
310                    spawn.name.span(),
311                    "standalone spawn alias needs a signature, including `()` for no inputs",
312                ));
313            }
314        }
315        Ok(())
316    }
317}
318
319fn explicit_type(ty: &Type) -> syn::Result<()> {
320    if matches!(ty, Type::ImplTrait(_) | Type::Infer(_)) {
321        return Err(Error::new(
322            ty.span(),
323            "declare an explicit type; resource trait requirements belong in `bounds`",
324        ));
325    }
326    Ok(())
327}
328
329/// Which RTIC task shape a definition was authored as. RTIC itself makes the
330/// same distinction by signature, so the authored `fn` versus `async fn` is
331/// what decides it here.
332#[derive(Clone, Copy, Debug, PartialEq, Eq)]
333pub enum TaskKind {
334    /// `async fn` - dispatched in software, may take inputs and diverge.
335    Software,
336    /// `fn` - bound to an interrupt by composition, takes only its context.
337    Hardware,
338}
339
340#[derive(Clone, Debug)]
341pub struct TaskContract {
342    pub arguments: TaskArguments,
343    pub kind: TaskKind,
344    pub context: Ident,
345    pub inputs: Vec<Parameter>,
346    pub diverges: bool,
347}
348
349impl TaskContract {
350    pub fn new(arguments: TaskArguments, signature: &Signature) -> syn::Result<Self> {
351        arguments.validate_standalone()?;
352        if signature.unsafety.is_some()
353            || signature.abi.is_some()
354            || signature.constness.is_some()
355            || !signature.generics.params.is_empty()
356            || signature.generics.where_clause.is_some()
357            || signature.variadic.is_some()
358        {
359            return Err(Error::new(
360                signature.span(),
361                "standalone tasks must be safe functions without generics or an ABI",
362            ));
363        }
364        let kind = if signature.asyncness.is_some() {
365            TaskKind::Software
366        } else {
367            TaskKind::Hardware
368        };
369        let mut parameters = signature.inputs.iter();
370        let context = parameter(
371            parameters
372                .next()
373                .ok_or_else(|| Error::new(signature.span(), "task needs a context parameter"))?,
374        )?;
375        let Type::Path(context_type) = &context.ty else {
376            return Err(Error::new(context.ty.span(), "expected task_name::Context"));
377        };
378        let segments = &context_type.path.segments;
379        if context_type.qself.is_some()
380            || context_type.path.leading_colon.is_some()
381            || segments.len() != 2
382            || identifier_key(&segments[0].ident) != identifier_key(&signature.ident)
383            || segments[1].ident != "Context"
384            || segments.iter().any(|s| !s.arguments.is_empty())
385        {
386            return Err(Error::new(context.ty.span(), "expected task_name::Context"));
387        }
388        let inputs = parameters.map(parameter).collect::<syn::Result<Vec<_>>>()?;
389        unique(
390            std::iter::once(&context.name).chain(inputs.iter().map(|p| &p.name)),
391            "duplicate task parameter",
392        )?;
393        let diverges = match &signature.output {
394            ReturnType::Default => false,
395            ReturnType::Type(_, ty) => match ty.as_ref() {
396                Type::Never(_) => true,
397                Type::Tuple(tuple) if tuple.elems.is_empty() => false,
398                _ => {
399                    return Err(Error::new(
400                        ty.span(),
401                        "initial SW tasks must return `()` or `!`",
402                    ));
403                }
404            },
405        };
406        if kind == TaskKind::Hardware {
407            // An interrupt handler is entered by the hardware, so there is no
408            // caller to supply inputs and nowhere for a `!` return to go.
409            if let Some(extra) = inputs.first() {
410                return Err(Error::new(
411                    extra.name.span(),
412                    "hardware tasks take only their context; an interrupt has no caller to pass inputs",
413                ));
414            }
415            if diverges {
416                return Err(Error::new(
417                    signature.span(),
418                    "hardware tasks must return `()`; an interrupt handler has to return",
419                ));
420            }
421        }
422        Ok(Self {
423            arguments,
424            kind,
425            context: context.name,
426            inputs,
427            diverges,
428        })
429    }
430}
431
432fn parameter(argument: &FnArg) -> syn::Result<Parameter> {
433    let FnArg::Typed(argument) = argument else {
434        return Err(Error::new(
435            argument.span(),
436            "task parameters cannot be receivers",
437        ));
438    };
439    let Pat::Ident(pattern) = argument.pat.as_ref() else {
440        return Err(Error::new(
441            argument.pat.span(),
442            "initial task parameters must be named identifiers",
443        ));
444    };
445    if pattern.by_ref.is_some() || pattern.subpat.is_some() {
446        return Err(Error::new(
447            pattern.span(),
448            "initial task parameters cannot use `ref` or subpatterns",
449        ));
450    }
451    Ok(Parameter {
452        name: pattern.ident.clone(),
453        ty: (*argument.ty).clone(),
454    })
455}
456
457#[cfg(test)]
458mod tests {
459    use super::*;
460    use quote::ToTokens;
461
462    fn contract(args: &str, function: &str) -> syn::Result<TaskContract> {
463        let args = syn::parse_str(args)?;
464        let function: syn::ItemFn = syn::parse_str(function)?;
465        TaskContract::new(args, &function.sig)
466    }
467    #[test]
468    fn parses_complete_standalone_contract_without_system_types() {
469        let task = contract(
470            "bounds = [led: StatefulOutputPin + Send], local = [led, count: u32],
471             shared = [enabled: bool], config = [period_ms: u32],
472             spawn = [report(value: u32), wake()], monotonic = Mono,",
473            "pub async fn blink(mut cx: blink::Context, value: (u32, bool)) -> ! { loop {} }",
474        )
475        .unwrap();
476        assert_eq!(task.context, "cx");
477        assert_eq!(task.inputs.len(), 1);
478        assert_eq!(
479            task.inputs[0].ty.to_token_stream().to_string(),
480            "(u32 , bool)"
481        );
482        assert!(task.diverges);
483        assert_eq!(task.arguments.bounds[0].traits.len(), 2);
484        assert_eq!(task.arguments.local[1].name, "count");
485        assert_eq!(task.arguments.shared[0].name, "enabled");
486        assert_eq!(task.arguments.config[0].name, "period_ms");
487        assert_eq!(task.arguments.spawn[1].inputs.as_ref().unwrap().len(), 0);
488        assert!(task.arguments.monotonic.unwrap().is_ident("Mono"));
489    }
490
491    #[test]
492    fn distinguishes_an_explicit_zero_input_signature_from_a_bare_name() {
493        let args: TaskArguments = syn::parse_str("spawn = [wake(), report]").unwrap();
494        assert!(args.spawn[0].inputs.as_ref().unwrap().is_empty());
495        assert!(args.spawn[1].inputs.is_none());
496        assert!(
497            args.validate_standalone()
498                .unwrap_err()
499                .to_string()
500                .contains("signature")
501        );
502    }
503
504    #[test]
505    fn rejects_invalid_resource_and_configuration_contracts() {
506        for (args, message) in [
507            ("bounds = [led: Pin]", "must be claimed"),
508            ("local = [led]", "either an inline type"),
509            ("bounds = [led: Pin], local = [led: u32]", "not both"),
510            (
511                "local = [state: bool], shared = [state: bool]",
512                "both local and shared",
513            ),
514            (
515                "shared = [state: bool, state: bool]",
516                "duplicate shared resource",
517            ),
518            ("config = [period_ms]", "inline type"),
519            ("local = [led: impl Pin]", "explicit type"),
520            ("config = [period_ms: _]", "explicit type"),
521            ("config = [n: u32, n: u32]", "duplicate task configuration"),
522            (
523                "spawn = [report(x: u32, x: bool)]",
524                "duplicate spawn parameter",
525            ),
526            ("dependencies = [Fugit]", "Cargo.toml"),
527            ("bounds = [led: 'static], local = [led]", "trait bounds"),
528        ] {
529            let error = contract(args, "async fn run(cx: run::Context) {}").unwrap_err();
530            assert!(error.to_string().contains(message), "{args}: {error}");
531        }
532    }
533
534    #[test]
535    fn rejects_duplicate_keys_missing_commas_and_dependency_trailing_tokens() {
536        for source in [
537            "local = [a], local = [b]",
538            "local = [a] shared = [b]",
539            "dependencies = [D(features = [], unexpected)]",
540            "priority = 1",
541        ] {
542            assert!(syn::parse_str::<TaskArguments>(source).is_err(), "{source}");
543        }
544    }
545
546    #[test]
547    fn accepts_synchronous_hardware_handlers() {
548        let task = contract("", "fn on_tick(cx: on_tick::Context) {}").unwrap();
549        assert_eq!(task.kind, TaskKind::Hardware);
550        assert!(task.inputs.is_empty());
551        assert!(!task.diverges);
552
553        let task = contract("", "async fn run(cx: run::Context) {}").unwrap();
554        assert_eq!(task.kind, TaskKind::Software);
555    }
556
557    #[test]
558    fn rejects_hardware_handlers_that_cannot_be_entered_by_an_interrupt() {
559        for source in [
560            // an interrupt has no caller to supply inputs
561            "fn on_tick(cx: on_tick::Context, value: u32) {}",
562            // an interrupt handler has to return
563            "fn on_tick(cx: on_tick::Context) -> ! { loop {} }",
564        ] {
565            assert!(contract("", source).is_err(), "{source}");
566        }
567    }
568
569    #[test]
570    fn rejects_unsupported_task_signatures() {
571        for source in [
572            "async fn run() {}",
573            "async fn run(cx: other::Context) {}",
574            "async fn run(cx: run::Context<'static>) {}",
575            "async fn run<T>(cx: run::Context) {}",
576            "async fn run(cx: run::Context) -> u32 { 0 }",
577            "async fn run(cx: run::Context, (a,b): (u32,u32)) {}",
578            "async fn run(cx: run::Context, cx: u32) {}",
579        ] {
580            assert!(contract("", source).is_err(), "{source}");
581        }
582    }
583
584    #[test]
585    fn accepts_returning_tasks_and_raw_parameter_names() {
586        let task = contract("", "async fn run(ctx: run::Context, r#type: u32) -> () {}").unwrap();
587        assert!(!task.diverges);
588        assert_eq!(task.context, "ctx");
589        assert_eq!(task.inputs.len(), 1);
590    }
591}