1use 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
16pub fn identifier_key(name: &Ident) -> String {
18 name.to_string().trim_start_matches("r#").to_owned()
19}
20
21#[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 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#[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 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 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 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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
333pub enum TaskKind {
334 Software,
336 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 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 "fn on_tick(cx: on_tick::Context, value: u32) {}",
562 "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}