1mod gate;
2
3use embassy_supervisor_syntax::{
4 AdoptedFn, Dep, GraphSpec, Item, NodeItem, PoolItem, ResourceDecl, ResourceKind, SignalDecl,
5 StateInit, TaskSource, VerbTable, item_executor, item_ident_cfg, item_resources, kw,
6 name_string, node_param, normalize_fragment_crate, rewrite_verb_calls, substitute_dollar_crate,
7};
8use proc_macro::TokenStream;
9use proc_macro2::TokenStream as TokenStream2;
10use quote::{format_ident, quote};
11use std::collections::{HashMap, HashSet};
12use syn::parse::{Parse, ParseStream};
13use syn::punctuated::Punctuated;
14use syn::spanned::Spanned;
15use syn::{Attribute, Expr, Ident, LitInt, Meta, Path, Result as SynResult, Token};
16
17const LOCAL_SLOT_TYPE: &str = "__SvLocalResourceSlot";
18
19struct HelperIdents {
20 local_slot: Ident,
21 try_box: Ident,
22 try_box_zeroed: Ident,
23 alloc_alias: Ident,
24 nodes: Ident,
25 graph_ref: Ident,
26}
27
28impl HelperIdents {
29 fn new(graph_name: Option<&Ident>) -> Self {
30 match graph_name {
31 None => Self {
32 local_slot: format_ident!("{LOCAL_SLOT_TYPE}"),
33 try_box: format_ident!("__sv_try_box"),
34 try_box_zeroed: format_ident!("__sv_try_box_zeroed"),
35 alloc_alias: format_ident!("__sv_alloc"),
36 nodes: format_ident!("NODES"),
37 graph_ref: format_ident!("GRAPH_REF"),
38 },
39 Some(n) => {
40 let lower = n.to_string().to_lowercase();
41 Self {
42 local_slot: format_ident!("{LOCAL_SLOT_TYPE}{}", n),
43 try_box: format_ident!("__sv_try_box_{lower}"),
44 try_box_zeroed: format_ident!("__sv_try_box_zeroed_{lower}"),
45 alloc_alias: format_ident!("__sv_alloc_{lower}"),
46 nodes: format_ident!("__SV_NODES_{}", n),
47 graph_ref: format_ident!("__SV_GRAPH_REF_{}", n),
48 }
49 }
50 }
51 }
52}
53
54fn substitute_it(expr: &Expr, target: &TokenStream2) -> TokenStream2 {
55 fn walk(tokens: TokenStream2, target: &TokenStream2) -> TokenStream2 {
56 tokens
57 .into_iter()
58 .map(|tt| match tt {
59 proc_macro2::TokenTree::Ident(ref id) if id == "it" => {
60 let mut group = proc_macro2::Group::new(
61 proc_macro2::Delimiter::Parenthesis,
62 quote!(&#target),
63 );
64 group.set_span(id.span());
65 proc_macro2::TokenTree::Group(group)
66 }
67 proc_macro2::TokenTree::Group(g) => {
68 let mut new = proc_macro2::Group::new(g.delimiter(), walk(g.stream(), target));
69 new.set_span(g.span());
70 proc_macro2::TokenTree::Group(new)
71 }
72 other => other,
73 })
74 .collect()
75 }
76 walk(quote!(#expr), target)
77}
78
79fn inject_call_with(task: &Expr, lead: &[TokenStream2]) -> SynResult<TokenStream2> {
80 match task {
81 Expr::Path(_) => Ok(quote!(#task(#(#lead),*))),
82 Expr::Call(c) => {
83 let f = &c.func;
84 let mut args: Vec<TokenStream2> = lead.to_vec();
85 args.extend(c.args.iter().map(|a| quote!(#a)));
86 Ok(quote!(#f(#(#args),*)))
87 }
88 other => Err(syn::Error::new_spanned(
89 other,
90 "expected a task-fn path or a partial call like `f(extra_args)`",
91 )),
92 }
93}
94
95fn cfg_aware_len<'a>(cfgs: impl Iterator<Item = &'a Vec<Attribute>> + Clone) -> TokenStream2 {
96 if !cfgs.clone().any(|c| cfg_predicate(c).is_some()) {
97 let n = cfgs.count();
98 return quote!(#n);
99 }
100 let terms: Vec<TokenStream2> = cfgs
101 .map(|c| match cfg_predicate(c) {
102 None => quote!(1usize),
103 Some(pred) => quote!({
104 #[cfg(#pred)]
105 {
106 1usize
107 }
108 #[cfg(not(#pred))]
109 {
110 0usize
111 }
112 }),
113 })
114 .collect();
115 quote!(0usize #(+ #terms)*)
116}
117
118struct EmitCtx<'a> {
119 cfg: &'a [Attribute],
120 cr: &'a TokenStream2,
121 owner: &'a Ident,
122}
123
124#[derive(Default)]
125struct ObserveDefaults {
126 writes: Option<Expr>,
127 reads: Option<Expr>,
128}
129
130fn marker_array_tokens(
131 ctx: &EmitCtx<'_>,
132 deps: &[Dep],
133 pool_names: &std::collections::HashSet<String>,
134 select: impl Fn(&Dep) -> bool,
135 prefix: &str,
136 builder: &str,
137) -> (TokenStream2, TokenStream2) {
138 let EmitCtx { cfg, cr, owner } = ctx;
139 let Some((len, refs)) = marked_dep_tokens(deps, pool_names, select) else {
140 return (quote!(), quote!());
141 };
142 let table = format_ident!("__SV_{}_{}", prefix, owner);
143 let builder = format_ident!("{}", builder);
144 (
145 quote! {
146 #(#cfg)*
147 static #table: [&'static #cr::TaskNode; #len] = [#(#refs),*];
148 },
149 quote!( .#builder(&#table) ),
150 )
151}
152
153#[derive(Clone, Copy, Default)]
157struct VetoSlots<'a> {
158 bases: &'a [Option<u8>],
159 offset: usize,
160}
161
162fn coupling_binding_tokens(
163 ctx: &EmitCtx<'_>,
164 decls: &[SignalDecl],
165 prefix: &str,
166 builder: &str,
167 default_observe: Option<&Expr>,
168 foreign: &[AdoptedFn],
169 veto: VetoSlots<'_>,
170) -> (TokenStream2, TokenStream2) {
171 let EmitCtx { cfg, cr, owner } = ctx;
172 let mut defs = quote!();
173 let mut defs_refs: Vec<TokenStream2> = Vec::new();
174 let mut ref_cfgs: Vec<Vec<Attribute>> = Vec::new();
175 if !decls.is_empty() {
176 let table = format_ident!("__SV_{}_{}", prefix, owner);
177 let len = cfg_aware_len(decls.iter().map(|d| &d.cfg));
180 let entries: Vec<TokenStream2> = decls
181 .iter()
182 .enumerate()
183 .map(|(i, d)| {
184 let (entry_cfg, name, target) = (&d.cfg, d.display(), d.target());
185 let veto = veto.bases.get(i).copied().flatten().map(|base| {
186 let slot = u8::try_from(usize::from(base) + veto.offset)
187 .expect("veto slot count checked");
188 quote!( .veto(#slot) )
189 });
190 let observed = d.observed.as_ref().map(|_| {
191 let accessor = d
196 .via
197 .as_ref()
198 .or(default_observe)
199 .map(|e| substitute_it(e, &target))
200 .unwrap_or_else(|| quote!( #cr::Observable::change_token(&#target) ));
201 quote!( .observed(#cr::Observer::new(|| #accessor)) )
202 });
203 let beat = d.beat.as_ref().map(|_| quote!( .beat() ));
204 quote!( #(#entry_cfg)* #cr::Coupling::new(#name, &#target) #observed #beat #veto )
205 })
206 .collect();
207 defs.extend(quote! {
208 #(#cfg)*
209 static #table: [#cr::Coupling; #len] = [#(#entries),*];
210 });
211 defs_refs.push(quote!(&#table));
212 ref_cfgs.push(Vec::new());
213 }
214 for f in foreign {
215 let table = dataflow_static_path(&f.path, prefix, "");
216 let entry_cfg = &f.cfg;
217 defs_refs.push(quote!(#(#entry_cfg)* &#table));
218 ref_cfgs.push(f.cfg.clone());
219 }
220 let refs = defs_refs;
221 if refs.is_empty() {
222 return (defs, quote!());
223 }
224 let k = cfg_aware_len(ref_cfgs.iter());
227 let tbls = format_ident!("__SV_{}_TBLS_{}", prefix, owner);
228 let builder = format_ident!("{}", builder);
229 defs.extend(quote! {
230 #(#cfg)*
231 static #tbls: [&'static [#cr::Coupling]; #k] = [#(#refs),*];
232 });
233 (defs, quote!( .#builder(&#tbls) ))
234}
235
236fn marker_assert_tokens(
237 ctx: &EmitCtx<'_>,
238 owner: &str,
239 discover: Option<&[Attribute]>,
240 foreign: &[AdoptedFn],
241 decls: &[SignalDecl],
242 prefix: &str,
243) -> TokenStream2 {
244 let EmitCtx { cfg, cr, .. } = ctx;
245 let (Some(dcfg), false) = (discover, foreign.is_empty()) else {
246 return quote!();
247 };
248 let clause = if prefix == "READS" { "reads" } else { "writes" };
249 let mut out = quote!();
250 for d in decls {
251 let name = d.display();
252 let entry_cfg = &d.cfg;
253 let tables: Vec<TokenStream2> = foreign
255 .iter()
256 .map(|f| {
257 let t = dataflow_static_path(&f.path, prefix, "");
258 let fcfg = &f.cfg;
259 quote! {
260 #(#fcfg)*
261 {
262 __sv_found = __sv_found || #cr::__sv_tail_declared(&#t, #name);
263 }
264 }
265 })
266 .collect();
267 let msg = format!(
268 "node `{owner}`: `{clause}: [{name} ..]` marks a signal no bound \
269 `#[dataflow]` table carries. Beside `discover` an entry may only \
270 add a marker to a coupling the scan already found, so either the \
271 task fn does not access this signal, or it reaches it under a \
272 different final path segment"
273 );
274 out.extend(quote! {
275 #(#cfg)*
276 #(#dcfg)*
277 #(#entry_cfg)*
278 const _: () = {
279 let mut __sv_found = false;
280 #(#tables)*
281 assert!(__sv_found, #msg);
282 };
283 });
284 }
285 out
286}
287
288fn dataflow_static_path(f: &syn::Path, prefix: &str, infix: &str) -> syn::Path {
289 let mut p = f.clone();
290 let last = p.segments.last_mut().expect("a path has a segment");
291 last.ident = format_ident!("__SV_DATAFLOW_{}_{}{}", prefix, infix, last.ident);
292 last.arguments = syn::PathArguments::None;
293 p
294}
295
296fn discover_fn_path(
297 discover: Option<&kw::discover>,
298 source: Option<&TaskSource>,
299) -> SynResult<Option<syn::Path>> {
300 let Some(k) = discover else {
301 return Ok(None);
302 };
303 let (TaskSource::Shell(expr) | TaskSource::Spawn(expr)) =
304 source.expect("shape-checked: discover has a source");
305 fn callee(e: &syn::Expr) -> Option<&syn::ExprPath> {
306 match e {
307 syn::Expr::Path(p) => Some(p),
308 syn::Expr::Call(c) => callee(&c.func),
309 _ => None,
310 }
311 }
312 let Some(path) = callee(expr) else {
313 return Err(syn::Error::new_spanned(
314 k,
315 "`discover` derives its tables from the `task:`/`spawn:` fn — \
316 name it by path, not a closure",
317 ));
318 };
319 Ok(Some(path.path.clone()))
320}
321
322struct GatedBuilder {
323 cfg: Vec<Attribute>,
324 tokens: TokenStream2,
325 available: bool,
326 err: &'static str,
327}
328
329fn builder_clause_tokens(
330 item_cfg: &[Attribute],
331 clauses: impl IntoIterator<Item = GatedBuilder>,
332) -> (TokenStream2, TokenStream2, TokenStream2) {
333 let (mut inline, mut stmts, mut errors) = (quote!(), quote!(), quote!());
334 for c in clauses {
335 let (clause_cfg, tokens) = (&c.cfg, &c.tokens);
336 if !c.available && !clause_cfg.is_empty() {
337 let err = c.err;
338 errors.extend(quote!( #(#item_cfg)* #(#clause_cfg)* ::core::compile_error!(#err); ));
339 } else if clause_cfg.is_empty() {
340 inline.extend(tokens.clone());
341 } else {
342 stmts.extend(quote!( #(#clause_cfg)* let __sv_cfg = __sv_cfg #tokens; ));
343 }
344 }
345 (inline, stmts, errors)
346}
347
348fn discover_error_tokens(
349 item_cfg: &[Attribute],
350 discover: Option<&embassy_supervisor_syntax::Gated<kw::discover>>,
351) -> TokenStream2 {
352 match discover {
353 Some(g) if !g.cfg.is_empty() && !cfg!(feature = "dataflow") => {
354 let c = &g.cfg;
355 quote!( #(#item_cfg)* #(#c)* ::core::compile_error!(
356 "`discover` requires the `dataflow` feature (embassy-supervisor \
357 feature `dataflow`) — it binds the coupling tables the task \
358 fn's `#[dataflow]` attribute derives"
359 ); )
360 }
361 _ => quote!(),
362 }
363}
364
365fn disabled_tokens(
366 disabled: Option<&embassy_supervisor_syntax::Gated<kw::disabled>>,
367) -> TokenStream2 {
368 match disabled {
369 None => quote!(false),
370 Some(g) if g.cfg.is_empty() => quote!(true),
371 Some(g) => {
372 let pred = cfg_predicate(&g.cfg).expect("parse validated cfg attrs");
373 quote!({
374 #[cfg(#pred)]
375 {
376 true
377 }
378 #[cfg(not(#pred))]
379 {
380 false
381 }
382 })
383 }
384 }
385}
386
387fn cfg_predicate(attrs: &[Attribute]) -> Option<TokenStream2> {
388 let preds: Vec<TokenStream2> = attrs
389 .iter()
390 .filter_map(|a| match &a.meta {
391 Meta::List(ml) if ml.path.is_ident("cfg") => Some(ml.tokens.clone()),
392 _ => None,
393 })
394 .collect();
395 match preds.len() {
396 0 => None,
397 1 => Some(preds[0].clone()),
398 _ => Some(quote!(all(#(#preds),*))),
399 }
400}
401
402fn gate_tokens(resources: &[ResourceDecl]) -> (TokenStream2, Vec<TokenStream2>) {
403 let gate_refs: Vec<TokenStream2> = resources
404 .iter()
405 .map(|r| {
406 let cfg = &r.cfg;
407 let res = &r.ident;
408 quote!(#(#cfg)* &#res)
409 })
410 .collect();
411 (cfg_aware_len(resources.iter().map(|r| &r.cfg)), gate_refs)
412}
413
414struct ResourcePlan {
417 cfgs: HashMap<String, (Vec<Attribute>, bool)>,
421 claim_bases: HashMap<(String, String), u8>,
423 veto_bases: HashMap<(String, String), u8>,
426}
427
428impl ResourcePlan {
429 fn veto_slots(&self, owner: &Ident, writes: &[SignalDecl]) -> Vec<Option<u8>> {
432 writes
433 .iter()
434 .map(|d| {
435 self.veto_bases
436 .get(&(owner.to_string(), d.display()))
437 .copied()
438 })
439 .collect()
440 }
441
442 fn claims(&self, owner: &Ident, resources: &[ResourceDecl]) -> Vec<Option<u8>> {
445 resources
446 .iter()
447 .map(|r| {
448 self.claim_bases
449 .get(&(owner.to_string(), r.ident.to_string()))
450 .copied()
451 })
452 .collect()
453 }
454}
455
456fn claims_tokens(
460 arr: &Ident,
461 item_cfg: &[Attribute],
462 resources: &[ResourceDecl],
463 claims: &[Option<u8>],
464 offset: usize,
465 cr: &TokenStream2,
466) -> (TokenStream2, TokenStream2) {
467 let entries: Vec<(&Vec<Attribute>, TokenStream2)> = resources
468 .iter()
469 .zip(claims)
470 .filter_map(|(r, base)| {
471 let base = (*base)?;
472 let cfg = &r.cfg;
473 let res = &r.ident;
474 let slot = u8::try_from(usize::from(base) + offset).expect("slot count checked");
475 Some((cfg, quote!(#(#cfg)* (&#res, #slot))))
476 })
477 .collect();
478 if entries.is_empty() {
479 return (quote!(), quote!());
480 }
481 let len = cfg_aware_len(entries.iter().map(|(c, _)| *c));
482 let refs = entries.iter().map(|(_, t)| t);
483 (
484 quote! {
485 #(#item_cfg)*
486 static #arr: [(&'static dyn #cr::Divisible, u8); #len] = [#(#refs),*];
487 },
488 quote!( .with_claims(&#arr) ),
489 )
490}
491
492fn provides_tokens(
493 n: &NodeItem,
494 cr: &TokenStream2,
495 resource_cfgs: &std::collections::HashMap<String, (Vec<Attribute>, bool)>,
496) -> SynResult<(TokenStream2, TokenStream2)> {
497 if n.provides.is_empty() {
498 return Ok((quote!(), quote!()));
499 }
500 let mut cfgs: Vec<Vec<Attribute>> = Vec::new();
501 let refs = n
502 .provides
503 .iter()
504 .map(|p| {
505 let slot = &p.ident;
506 let Some((cfg, from_pool)) = resource_cfgs.get(&slot.to_string()) else {
507 return Err(syn::Error::new_spanned(
508 slot,
509 format!(
510 "`provides:` names `{slot}`, but no `resources:` entry in \
511 this graph declares a slot by that name — the clause \
512 clears the macro-emitted slot statics on the provider's \
513 shutdown ack, so it can only name one of them"
514 ),
515 ));
516 };
517 if *from_pool {
518 return Err(syn::Error::new_spanned(
519 slot,
520 format!(
521 "`provides:` cannot name `{slot}` — it is a pool's \
522 per-member slot array, filled and cleared by the pool's \
523 own scaling; the clause clears a single node's slot \
524 statics on its shutdown ack"
525 ),
526 ));
527 }
528 let entry_cfg = &p.cfg;
529 cfgs.push(cfg.iter().chain(entry_cfg.iter()).cloned().collect());
530 Ok(quote!(#(#cfg)* #(#entry_cfg)* &#slot))
531 })
532 .collect::<SynResult<Vec<_>>>()?;
533 let len = cfg_aware_len(cfgs.iter());
534 let node_cfg = &n.cfg;
535 let arr = format_ident!("__SV_PROVIDES_{}", n.ident);
536 Ok((
537 quote! {
538 #(#node_cfg)*
539 static #arr: [&'static dyn #cr::ResourceGate; #len] = [#(#refs),*];
540 },
541 quote!( .with_provides(&#arr) ),
542 ))
543}
544
545fn fragment_suffix(fragment: &Option<String>) -> String {
548 match fragment {
549 Some(f) => format!(" (from fragment `{f}`)"),
550 None => String::new(),
551 }
552}
553
554fn marked_dep_tokens(
559 deps: &[Dep],
560 pool_names: &std::collections::HashSet<String>,
561 select: impl Fn(&Dep) -> bool,
562) -> Option<(TokenStream2, Vec<TokenStream2>)> {
563 let marked: Vec<&Dep> = deps.iter().filter(|d| select(d)).collect();
564 if marked.is_empty() {
565 return None;
566 }
567 let refs: Vec<TokenStream2> = marked
568 .iter()
569 .map(|d| {
570 let cfg = &d.cfg;
571 let ident = &d.ident;
572 if pool_names.contains(&ident.to_string()) {
573 quote!(#(#cfg)* &#ident[0])
574 } else {
575 quote!(#(#cfg)* &#ident)
576 }
577 })
578 .collect();
579 Some((cfg_aware_len(marked.iter().map(|d| &d.cfg)), refs))
580}
581
582fn policy_type(expr: &Expr) -> SynResult<Path> {
586 if let Expr::Call(call) = expr
587 && let Expr::Path(p) = &*call.func
588 {
589 let n = p.path.segments.len();
590 if n >= 2 {
591 let segs: Punctuated<_, Token![::]> =
592 p.path.segments.iter().take(n - 1).cloned().collect();
593 return Ok(Path {
594 leading_colon: p.path.leading_colon,
595 segments: segs,
596 });
597 }
598 }
599 Err(syn::Error::new_spanned(
600 expr,
601 "pool `policy:` must be a `Type::new(..)` constructor (e.g. `DeferredShrink::new(..)`), \
602 or give the type explicitly: `policy: <Type> = <expr>`",
603 ))
604}
605
606struct Slot {
608 cfg_pred: Option<TokenStream2>,
610 reference: TokenStream2,
612 deps: Vec<Dep>,
614 fragment: Option<String>,
617}
618
619#[allow(clippy::too_many_arguments)]
626fn node_spawn(
627 ident: &Ident,
628 spawn: &Option<Expr>,
629 executor: &Option<Ident>,
630 resources: &[ResourceDecl],
631 state: Option<(&syn::Type, &StateInit)>,
634 spawn_fn: &TokenStream2,
635 cr: &TokenStream2,
636 helpers: &HelperIdents,
637) -> SynResult<TokenStream2> {
638 let take_prelude: Vec<TokenStream2> = resources
648 .iter()
649 .map(|r| {
650 let cfg = &r.cfg;
651 let res = &r.ident;
652 quote! {
653 #(#cfg)*
654 if !#cr::ResourceGate::is_filled(&#res) {
655 return ::core::result::Result::Err(::embassy_executor::SpawnError::Busy);
656 }
657 }
658 })
659 .collect();
660 let (state_prelude, state_arg) = match state {
661 Some((ty, init)) => (state_box_stmt(ty, init, helpers), vec![quote!(__state)]),
662 None => (quote!(), vec![]),
663 };
664 Ok(match (spawn, executor) {
665 (None, None) => quote!(::core::option::Option::None),
666 (None, Some(ex)) => {
669 return Err(syn::Error::new_spanned(
670 ex,
671 "`executor:` requires a `spawn:` (a parked node is spawned by the \
672 application, which picks its own spawner)",
673 ));
674 }
675 (Some(e @ (Expr::Path(_) | Expr::Call(_))), executor) => {
681 let mut lead: Vec<TokenStream2> = vec![quote!(&#ident)];
682 lead.extend(state_arg.iter().cloned());
683 let call = inject_call_with(e, &lead)?;
684 match executor {
685 None => {
686 let stmts = spawn_stmts(&call, "e!(&#ident), "e!(s));
687 quote!(::core::option::Option::Some(
688 (|s| {
689 #state_prelude
690 #(#take_prelude)*
691 #stmts
692 ::core::result::Result::Ok(())
693 }) as #spawn_fn
694 ))
695 }
696 Some(ex) => {
697 let stmts = spawn_stmts(&call, "e!(&#ident), "e!(__sp));
698 quote!(::core::option::Option::Some(
699 (|_s| {
700 let __sp = #ex
707 .get()
708 .ok_or(::embassy_executor::SpawnError::Busy)?;
709 #state_prelude
710 #(#take_prelude)*
711 #stmts
712 ::core::result::Result::Ok(())
713 }) as #spawn_fn
714 ))
715 }
716 }
717 }
718 (Some(_), Some(ex)) => {
719 return Err(syn::Error::new_spanned(
720 ex,
721 "`executor:` cannot be combined with a verbatim spawn closure (the \
722 closure owns the spawn; use the named SpawnerSlot inside it instead)",
723 ));
724 }
725 (Some(e), None) => quote!(::core::option::Option::Some((#e) as #spawn_fn)),
729 })
730}
731
732fn state_box_stmt(ty: &syn::Type, init: &StateInit, helpers: &HelperIdents) -> TokenStream2 {
735 match init {
736 StateInit::Expr(init) => {
737 let try_box = &helpers.try_box;
738 quote! {
739 let __state = #try_box(#init)
740 .ok_or(::embassy_executor::SpawnError::Busy)?;
741 }
742 }
743 StateInit::Zeroed(z) => {
744 let try_box_zeroed = &helpers.try_box_zeroed;
745 let call = quote::quote_spanned!(z.span=> #try_box_zeroed::<#ty>());
748 quote! {
749 let __state = #call.ok_or(::embassy_executor::SpawnError::Busy)?;
750 }
751 }
752 }
753}
754
755fn spawn_stmts(call: &TokenStream2, node_ref: &TokenStream2, sp: &TokenStream2) -> TokenStream2 {
770 if cfg!(feature = "trace") {
771 quote! {
773 let __token = #call?;
774 (#node_ref).adopt(&__token);
775 #sp.spawn(__token);
776 }
777 } else if cfg!(feature = "metadata-names") {
778 quote! {
780 let __token = #call?;
781 (#node_ref).stamp_name(&__token);
782 #sp.spawn(__token);
783 }
784 } else {
785 quote!(#sp.spawn(#call?);)
786 }
787}
788
789#[allow(clippy::too_many_arguments)]
803fn emit_shell(
804 owner: &Ident,
805 cfg: &[Attribute],
806 worker: &Expr,
807 pool_size: usize,
808 resources: &[ResourceDecl],
809 claims: &[Option<u8>],
812 exit: Option<&syn::Type>,
813 state: Option<(&syn::Type, &StateInit)>,
817 pool_member: bool,
821 cancel: bool,
825 cr: &TokenStream2,
826 helpers: &HelperIdents,
827) -> SynResult<(TokenStream2, Expr)> {
828 if !matches!(worker, Expr::Path(_) | Expr::Call(_)) {
829 return Err(syn::Error::new_spanned(
830 worker,
831 "`task:` names an async worker fn — a path or a partial call like \
832 `worker(args)`; for a closure or a ready spawn fn use `spawn:`",
833 ));
834 }
835 let shell = format_ident!("__sv_task_{}", owner.to_string().to_lowercase());
836 let typed = |r: &ResourceDecl| r.ty.clone().expect("a typed resource kind");
859 let res_params: Vec<TokenStream2> = resources
860 .iter()
861 .enumerate()
862 .filter_map(|(i, r)| {
863 let cfg = &r.cfg;
864 match r.kind() {
865 ResourceKind::Lend | ResourceKind::Consume if pool_member => {
869 let ty = typed(r);
870 let slot_param = format_ident!("__r{}_slot", i);
871 Some(quote!(#(#cfg)* #slot_param: &'static #cr::ResourceSlot<#ty>))
872 }
873 ResourceKind::Divisible if pool_member => {
876 let var = format_ident!("__r{}", i);
877 Some(quote!(#(#cfg)* #var: #cr::Claimant))
878 }
879 _ => None, }
881 })
882 .collect();
883 let res_takes: Vec<TokenStream2> = resources
884 .iter()
885 .enumerate()
886 .filter_map(|(i, r)| {
887 let cfg = &r.cfg;
888 let var = format_ident!("__r{}", i);
889 let res = &r.ident;
890 match r.kind() {
891 ResourceKind::Shared => Some(quote! {
892 #(#cfg)*
893 let ::core::option::Option::Some(#var) = #res.get() else {
894 __node.mark_lost_resource();
895 return;
896 };
897 }),
898 ResourceKind::Divisible if pool_member => None,
899 ResourceKind::Divisible => {
900 let slot = claims[i].expect("a divisible entry has a slot");
901 Some(quote!(#(#cfg)* let #var = #res.claimant(#slot);))
902 }
903 kind => {
904 let mutability = if kind == ResourceKind::Consume {
905 quote!()
906 } else {
907 quote!(mut)
908 };
909 let slot = if pool_member {
910 let slot_param = format_ident!("__r{}_slot", i);
911 quote!(#slot_param)
912 } else {
913 quote!(#res)
914 };
915 Some(quote! {
916 #(#cfg)*
917 let ::core::option::Option::Some(#mutability #var) = #slot.take() else {
918 __node.mark_lost_resource();
919 return;
920 };
921 })
922 }
923 }
924 })
925 .collect();
926 let res_leases: Vec<TokenStream2> = resources
927 .iter()
928 .enumerate()
929 .map(|(i, r)| {
930 let cfg = &r.cfg;
931 let var = format_ident!("__r{}", i);
932 if r.kind() == ResourceKind::Lend {
933 quote!(#(#cfg)* &mut #var)
934 } else {
935 quote!(#(#cfg)* #var)
936 }
937 })
938 .collect();
939 let restores: Vec<TokenStream2> = resources
940 .iter()
941 .enumerate()
942 .filter(|(_, r)| r.kind() == ResourceKind::Lend)
943 .map(|(i, r)| {
944 let cfg = &r.cfg;
945 let var = format_ident!("__r{}", i);
946 if pool_member {
947 let slot_param = format_ident!("__r{}_slot", i);
948 quote!(#(#cfg)* #slot_param.restore(#var);)
949 } else {
950 let res = &r.ident;
951 quote!(#(#cfg)* #res.restore(#var);)
952 }
953 })
954 .collect();
955 let alloc_alias = &helpers.alloc_alias;
956 let (state_param, state_lease, state_drop) = match state {
957 Some((ty, _)) => (
958 quote!(, mut __state: #alloc_alias::boxed::Box<#ty>),
959 vec![quote!(&mut *__state)],
960 quote!(::core::mem::drop(__state);),
963 ),
964 None => (quote!(), vec![], quote!()),
965 };
966 let mut lead: Vec<TokenStream2> = if cancel {
971 Vec::new()
972 } else {
973 vec![quote!(__node)]
974 };
975 lead.extend(res_leases);
976 lead.extend(state_lease);
977 let call = inject_call_with(worker, &lead)?;
978 let ps = LitInt::new(&pool_size.to_string(), proc_macro2::Span::call_site());
980 let allow_unreachable = quote!(#[allow(unreachable_code)]);
986 let exit_ident = format_ident!("{}_EXIT", owner);
1005 let out_ident = |exit: &syn::Type| Ident::new("__out", exit.span());
1017 let provide = |exit: &syn::Type| {
1018 let out_ident = out_ident(exit);
1019 let slot = Ident::new(&exit_ident.to_string(), exit.span());
1022 quote::quote_spanned!(exit.span()=>
1023 #[deny(unreachable_code)]
1024 #slot.provide(#out_ident);
1025 )
1026 };
1027 let (drive, provide_exit) = if cfg!(feature = "fault-inject") {
1034 match (cancel, exit) {
1035 (false, Some(ty)) => {
1036 let provide = provide(ty);
1037 let out_ident = out_ident(ty);
1038 (
1039 quote!(let __res = { let __fut = ::core::pin::pin!(#call); #cr::Injected::new(__node, __fut).await };),
1040 quote!(if let ::core::option::Option::Some(#out_ident) = __res {
1041 #provide
1042 }),
1043 )
1044 }
1045 (false, None) => (
1046 quote!({
1047 let __fut = ::core::pin::pin!(#call);
1048 let _ = #cr::Injected::new(__node, __fut).await;
1049 }),
1050 quote!(),
1051 ),
1052 (true, Some(ty)) => {
1053 let provide = provide(ty);
1054 let out_ident = out_ident(ty);
1055 (
1056 quote!(let __res = { let __fut = ::core::pin::pin!(#call); __node.run_cancellable(#cr::Injected::new(__node, __fut)).await };),
1057 quote!(if let ::core::result::Result::Ok(::core::option::Option::Some(#out_ident)) = __res {
1058 #provide
1059 }),
1060 )
1061 }
1062 (true, None) => (
1063 quote!({
1064 let __fut = ::core::pin::pin!(#call);
1065 let _ = __node.run_cancellable(#cr::Injected::new(__node, __fut)).await;
1066 }),
1067 quote!(),
1068 ),
1069 }
1070 } else {
1071 match (cancel, exit) {
1072 (false, Some(ty)) => {
1073 let provide = provide(ty);
1074 let out_ident = out_ident(ty);
1075 (quote!(let #out_ident = #call.await;), provide)
1076 }
1077 (false, None) => (quote!(#call.await;), quote!()),
1078 (true, Some(ty)) => {
1079 let provide = provide(ty);
1080 let out_ident = out_ident(ty);
1081 (
1082 quote!(let __res = { let __fut = ::core::pin::pin!(#call); __node.run_cancellable(__fut).await };),
1083 quote!(if let ::core::result::Result::Ok(#out_ident) = __res {
1084 #provide
1085 }),
1086 )
1087 }
1088 (true, None) => (
1089 quote!({
1090 let __fut = ::core::pin::pin!(#call);
1091 let _ = __node.run_cancellable(__fut).await;
1092 }),
1093 quote!(),
1094 ),
1095 }
1096 };
1097 let mark_exited = if pool_member {
1104 quote!(__node.mark_exited();)
1105 } else {
1106 quote!(#owner.mark_exited();)
1107 };
1108 let def = quote! {
1109 #(#cfg)*
1110 #[::embassy_executor::task(pool_size = #ps)]
1111 #allow_unreachable
1112 async fn #shell(__node: &'static #cr::TaskNode #(, #res_params)* #state_param) {
1113 #(#res_takes)*
1114 #drive
1115 #state_drop
1116 #(#restores)*
1117 #provide_exit
1118 #mark_exited
1119 }
1120 };
1121 let path: Expr = syn::parse_quote!(#shell);
1122 Ok((def, path))
1123}
1124
1125fn emit_node(
1129 n: &NodeItem,
1130 cr: &TokenStream2,
1131 spawn_fn: &TokenStream2,
1132 pool_names: &std::collections::HashSet<String>,
1134 helpers: &HelperIdents,
1135 observe: &ObserveDefaults,
1136 plan: &ResourcePlan,
1139) -> SynResult<(TokenStream2, Slot)> {
1140 let ident = &n.ident;
1141 let claims = plan.claims(&n.ident, &n.resources);
1142 let cfg = &n.cfg;
1143 let mode = &n.mode;
1144 let name = name_string(&n.ident);
1145 let disabled = disabled_tokens(n.disabled.as_ref());
1146 let (shell_def, spawn_expr) = match &n.source {
1147 Some(TaskSource::Shell(worker)) => {
1148 let ps = match &n.pool_size {
1149 Some(l) => l.base10_parse::<usize>()?,
1150 None => 1,
1151 };
1152 if ps > 1
1158 && n.resources
1159 .iter()
1160 .any(|r| !matches!(r.kind(), ResourceKind::Shared))
1161 {
1162 return Err(syn::Error::new_spanned(
1163 n.pool_size.as_ref().unwrap(),
1164 "`pool_size > 1` cannot combine with lend/consume/divisible \
1165 `resources:` (the slot holds one value, or one claimant; use \
1166 `shared`, or an `ElasticPool` with per-member slots)",
1167 ));
1168 }
1169 let (def, path) = emit_shell(
1170 ident,
1171 cfg,
1172 worker,
1173 ps,
1174 &n.resources,
1175 &claims,
1176 n.exit.as_ref(),
1177 n.state.as_ref().map(|(_, ty, init)| (ty, init)),
1178 false,
1179 n.cancel,
1180 cr,
1181 helpers,
1182 )?;
1183 (def, Some(path))
1184 }
1185 Some(TaskSource::Spawn(e)) => (quote!(), Some(e.clone())),
1186 None => (quote!(), None),
1187 };
1188 let spawn = node_spawn(
1189 ident,
1190 &spawn_expr,
1191 &n.executor,
1192 &n.resources,
1193 n.state.as_ref().map(|(_, ty, init)| (ty, init)),
1194 spawn_fn,
1195 cr,
1196 helpers,
1197 )?;
1198 let mut with_exec = match &n.executor {
1201 Some(ex) => quote!( .with_executor(&#ex) ),
1202 None => quote!(),
1203 };
1204 if cfg!(feature = "fault-inject") && matches!(n.source, Some(TaskSource::Shell(_))) {
1207 with_exec.extend(quote!( .with_shell() ));
1208 }
1209 let (res_defs, with_res) = if n.resources.is_empty() {
1216 (quote!(), quote!())
1217 } else {
1218 let gates_ident = format_ident!("__SV_GATES_{}", ident);
1219 let slot_defs = n
1223 .resources
1224 .iter()
1225 .filter(|r| matches!(r.kind(), ResourceKind::Lend | ResourceKind::Consume))
1226 .map(|r| {
1227 let ecfg = &r.cfg;
1228 let res = &r.ident;
1229 let ty = r.ty.as_ref().expect("a typed resource kind");
1230 let slot_ty = if r.local.is_some() {
1236 let local = &helpers.local_slot;
1237 quote!(#local<#ty>)
1238 } else {
1239 quote!(#cr::ResourceSlot<#ty>)
1240 };
1241 let doc = if r.consume.is_some() {
1242 format!(
1243 "Resource slot for node `{ident}` (generated by `supervisor_graph!`). \
1244 Move the resource in with `.provide(..)` before `Supervisor::start`. \
1245 `consume`: the worker owns (and may drop) the value, so the slot is \
1246 empty after the task exits — re-`provide()` before any respawn."
1247 )
1248 } else {
1249 format!(
1250 "Resource slot for node `{ident}` (generated by `supervisor_graph!`). \
1251 Move the resource in with `.provide(..)` before `Supervisor::start`."
1252 )
1253 };
1254 quote! {
1255 #(#cfg)*
1256 #(#ecfg)*
1257 #[doc = #doc]
1258 pub static #res: #slot_ty = <#slot_ty>::new();
1259 }
1260 });
1261 let (gates_len, gate_refs) = gate_tokens(&n.resources);
1262 (
1263 quote! {
1264 #(#slot_defs)*
1265 #(#cfg)*
1266 static #gates_ident: [&'static dyn #cr::ResourceGate; #gates_len] =
1267 [#(#gate_refs),*];
1268 },
1269 quote!( .with_resources(&#gates_ident) ),
1270 )
1271 };
1272 let (with_clauses, clause_stmts, clause_errors) = builder_clause_tokens(
1273 cfg,
1274 [
1275 n.slot_timeout.as_ref().map(|g| {
1276 let (c, ms) = (&g.cfg, &g.value);
1277 GatedBuilder {
1278 cfg: c.clone(),
1279 tokens: quote!( .with_slot_timeout(#cr::_export::Duration::from_millis(#ms)) ),
1280 available: true,
1281 err: "",
1282 }
1283 }),
1284 n.ack_timeout.as_ref().map(|g| {
1285 let (c, ms) = (&g.cfg, &g.value);
1286 GatedBuilder {
1287 cfg: c.clone(),
1288 tokens: quote!( .with_ack_timeout(#cr::_export::Duration::from_millis(#ms)) ),
1289 available: true,
1290 err: "",
1291 }
1292 }),
1293 n.beat_timeout.as_ref().map(|g| {
1294 let (c, ms) = (&g.cfg, &g.value);
1295 GatedBuilder {
1296 cfg: c.clone(),
1297 tokens: quote!( .with_beat_timeout(#cr::_export::Duration::from_millis(#ms)) ),
1298 available: cfg!(feature = "liveness-monitor"),
1299 err: "`beat_timeout:` requires the `liveness-monitor` feature \
1300 (embassy-supervisor feature `liveness-monitor`) — the \
1301 supervisor then reports this node once it has been running \
1302 that long without a beat()",
1303 }
1304 }),
1305 n.beat_window.as_ref().map(|g| {
1307 let (c, w) = (&g.cfg, &g.value);
1308 GatedBuilder {
1309 cfg: c.clone(),
1310 tokens: quote!( .with_beat_window(#w) ),
1311 available: cfg!(feature = "liveness-monitor"),
1312 err: "`beat_window:` requires the `liveness-monitor` feature \
1313 (embassy-supervisor feature `liveness-monitor`) — it sets \
1314 how many consecutive stale sweeps are reported on",
1315 }
1316 }),
1317 n.ready_on_write.as_ref().map(|g| GatedBuilder {
1318 cfg: g.cfg.clone(),
1319 tokens: quote!( .with_ready_on_write() ),
1320 available: cfg!(all(feature = "coupling-observe", feature = "readiness")),
1321 err: "`ready_on_write` requires the `coupling-observe` and \
1322 `readiness` features (embassy-supervisor features of the \
1323 same names) — readiness is asserted by the monitor sweep \
1324 seeing an `observed beat` write advance",
1325 }),
1326 ]
1327 .into_iter()
1328 .flatten(),
1329 );
1330 let graph_ref = &helpers.graph_ref;
1334 let with_graph = cfg!(feature = "data-deps").then(|| quote!( .with_graph(&#graph_ref) ));
1335 let ctx = EmitCtx {
1339 cfg,
1340 cr,
1341 owner: ident,
1342 };
1343 let marker = |select: fn(&Dep) -> bool, prefix, builder| {
1344 marker_array_tokens(&ctx, &n.deps, pool_names, select, prefix, builder)
1345 };
1346 let (ready_def, with_ready) = marker(|d| d.ready.is_some(), "READY", "with_ready_deps");
1347 let (bound_def, with_bound) = marker(|d| d.bound.is_some(), "BOUND", "with_bound_deps");
1348 let mut foreign: Vec<AdoptedFn> = Vec::new();
1349 foreign.extend(
1350 discover_fn_path(n.discover.as_ref().map(|g| &g.kw), n.source.as_ref())?
1351 .filter(|_| cfg!(feature = "dataflow"))
1352 .map(|path| AdoptedFn {
1353 cfg: n
1354 .discover
1355 .as_ref()
1356 .map(|g| g.cfg.clone())
1357 .unwrap_or_default(),
1358 path,
1359 }),
1360 );
1361 foreign.extend(n.dataflow.iter().cloned());
1362 let (reads_def, with_reads) = coupling_binding_tokens(
1363 &ctx,
1364 &n.reads,
1365 "READS",
1366 "with_reads",
1367 observe.reads.as_ref(),
1368 &foreign,
1369 VetoSlots::default(),
1370 );
1371 let marker_asserts = {
1372 let d = n.discover.as_ref().map(|g| g.cfg.as_slice());
1373 let r = marker_assert_tokens(&ctx, &name_string(ident), d, &foreign, &n.reads, "READS");
1374 let w = marker_assert_tokens(&ctx, &name_string(ident), d, &foreign, &n.writes, "WRITES");
1375 quote!( #r #w )
1376 };
1377 let veto_bases = plan.veto_slots(&n.ident, &n.writes);
1378 let (writes_def, with_writes) = coupling_binding_tokens(
1379 &ctx,
1380 &n.writes,
1381 "WRITES",
1382 "with_writes",
1383 observe.writes.as_ref(),
1384 &foreign,
1385 VetoSlots {
1386 bases: &veto_bases,
1387 offset: 0,
1388 },
1389 );
1390 let exit_def = match &n.exit {
1391 Some(ty) => {
1392 let exit_ident = format_ident!("{}_EXIT", ident);
1393 let doc = format!(
1394 "Exit-value slot for node `{ident}` (generated by `supervisor_graph!`). \
1395 The generated shell `provide()`s the worker's return value here just \
1396 before recording the exit; read it with `.wait_take()` (or `.take()` \
1397 after `has_exited()`). Overwritten by the next completion."
1398 );
1399 quote! {
1400 #(#cfg)*
1401 #[doc = #doc]
1402 pub static #exit_ident: #cr::ResourceSlot<#ty> =
1403 #cr::ResourceSlot::new();
1404 }
1405 }
1406 None => quote!(),
1407 };
1408 let node_doc = format!(
1409 "Supervised node `{ident}` (`{mode}`), generated by `supervisor_graph!`. \
1410 Pass it to the supervisor's per-node verbs (`start_node`, `stop_node`, \
1411 `resume_node`, `activate`/`deactivate`); the worker gets the same \
1412 `&'static TaskNode` for the task-side protocol."
1413 );
1414 let cfg_ident = format_ident!("__SV_CFG_{}", ident);
1415 let (prov_def, with_provides) = provides_tokens(n, cr, &plan.cfgs)?;
1416 let (claims_def, with_claims) = claims_tokens(
1417 &format_ident!("__SV_CLAIMS_{}", ident),
1418 cfg,
1419 &n.resources,
1420 &claims,
1421 0,
1422 cr,
1423 );
1424 let cfg_chain = quote! {
1425 #cr::NodeCfg::new(#name, #cr::Mode::#mode, #spawn)
1426 #with_exec #with_res #with_provides #with_claims #with_clauses #with_ready
1427 #with_bound #with_reads #with_writes
1428 #with_graph
1429 };
1430 let cfg_init = if clause_stmts.is_empty() {
1431 cfg_chain
1432 } else {
1433 quote! {{
1434 let __sv_cfg = #cfg_chain;
1435 #clause_stmts
1436 __sv_cfg
1437 }}
1438 };
1439 let discover_errors = discover_error_tokens(cfg, n.discover.as_ref());
1440 let def = quote! {
1441 #clause_errors
1442 #discover_errors
1443 #res_defs
1444 #prov_def
1445 #claims_def
1446 #exit_def
1447 #ready_def
1448 #bound_def
1449 #reads_def
1450 #writes_def
1451 #marker_asserts
1452 #shell_def
1453 #(#cfg)*
1454 #[doc(hidden)]
1455 static #cfg_ident: #cr::NodeCfg = #cfg_init;
1456 #(#cfg)*
1457 #[doc = #node_doc]
1458 pub static #ident: #cr::TaskNode = #cr::TaskNode::new(&#cfg_ident, #disabled);
1459 };
1460 let slot = Slot {
1461 cfg_pred: cfg_predicate(cfg),
1462 reference: quote!(&#ident),
1463 deps: n.deps.clone(),
1464 fragment: n.fragment.clone(),
1465 };
1466 Ok((def, slot))
1467}
1468
1469fn emit_pool(
1470 p: &PoolItem,
1471 cr: &TokenStream2,
1472 spawn_fn: &TokenStream2,
1473 pool_names: &std::collections::HashSet<String>,
1474 helpers: &HelperIdents,
1475 observe: &ObserveDefaults,
1476 plan: &ResourcePlan,
1479) -> SynResult<(Vec<TokenStream2>, TokenStream2, Vec<Slot>)> {
1480 let ident = &p.ident;
1481 let claims = plan.claims(&p.ident, &p.resources);
1482 let cfg = &p.cfg;
1483 let lname = name_string(&p.ident);
1484 let pool_static = format_ident!("{}_POOL", ident);
1485 let k = p.modes.len();
1486
1487 let lit_bounds = match (&p.min, &p.max) {
1488 (Expr::Lit(lmin), Expr::Lit(lmax)) => match (&lmin.lit, &lmax.lit) {
1489 (syn::Lit::Int(imin), syn::Lit::Int(imax)) => {
1490 Some((imin.base10_parse::<u8>()?, imax.base10_parse::<u8>()?))
1491 }
1492 _ => None,
1493 },
1494 _ => None,
1495 };
1496 if let Some((min_v, max_v)) = lit_bounds {
1497 if min_v > max_v {
1498 return Err(syn::Error::new_spanned(
1499 &p.min,
1500 format!("pool `min:` ({min_v}) must not exceed `max:` ({max_v})"),
1501 ));
1502 }
1503 if usize::from(max_v) > k {
1504 return Err(syn::Error::new_spanned(
1505 &p.max,
1506 format!("pool `max:` ({max_v}) exceeds the declared member count ({k})"),
1507 ));
1508 }
1509 }
1510
1511 if !p.resources.is_empty() && matches!(p.source, TaskSource::Spawn(_)) {
1512 return Err(syn::Error::new_spanned(
1513 &p.resources[0].ident,
1514 "pool `resources:` requires `task:` — the values are handed to the \
1515 generated shell as arguments (and lend entries restored by it); a \
1516 `spawn:` task fn manages its own arguments",
1517 ));
1518 }
1519 if let Some((_, ty, _)) = &p.state
1520 && matches!(p.source, TaskSource::Spawn(_))
1521 {
1522 return Err(syn::Error::new_spanned(
1523 ty,
1524 "pool `state:` requires `task:` — the generated shell owns the boxed \
1525 state across the worker call; a `spawn:` task fn can Box its own",
1526 ));
1527 }
1528
1529 let (shell_def, member_expr) = match &p.source {
1530 TaskSource::Spawn(e) => (quote!(), e.clone()),
1531 TaskSource::Shell(worker) => emit_shell(
1532 ident,
1533 cfg,
1534 worker,
1535 k,
1536 &p.resources,
1537 &claims,
1538 None,
1539 p.state.as_ref().map(|(_, ty, init)| (ty, init)),
1540 true,
1541 p.cancel,
1542 cr,
1543 helpers,
1544 )?,
1545 };
1546 let res_args: Vec<TokenStream2> = p
1547 .resources
1548 .iter()
1549 .zip(&claims)
1550 .filter_map(|(r, base)| {
1551 let ecfg = &r.cfg;
1552 let res = &r.ident;
1553 match r.kind() {
1554 ResourceKind::Lend | ResourceKind::Consume => Some(quote!(#(#ecfg)* &#res[I])),
1555 ResourceKind::Divisible => {
1556 let base = base.expect("a divisible entry has a slot");
1557 Some(quote!(#(#ecfg)* #res.claimant((#base as usize + I) as u8)))
1558 }
1559 ResourceKind::Shared => None,
1560 }
1561 })
1562 .collect();
1563 let (state_prelude, state_arg) = match &p.state {
1564 Some((_, ty, init)) => (state_box_stmt(ty, init, helpers), vec![quote!(__state)]),
1565 None => (quote!(), vec![]),
1566 };
1567 let mut lead: Vec<TokenStream2> = vec![quote!(&#ident[I])];
1568 lead.extend(res_args);
1569 lead.extend(state_arg);
1570 let call = inject_call_with(&member_expr, &lead)?;
1571 let (param, prelude, sp_tokens) = match &p.executor {
1572 None => (quote!(s), quote!(), quote!(s)),
1573 Some(ex) => (
1574 quote!(_s),
1575 quote! {
1576 let __sp = #ex
1577 .get()
1578 .ok_or(::embassy_executor::SpawnError::Busy)?;
1579 },
1580 quote!(__sp),
1581 ),
1582 };
1583 let get_prelude: Vec<TokenStream2> = p
1584 .resources
1585 .iter()
1586 .map(|r| {
1587 let ecfg = &r.cfg;
1588 let res = &r.ident;
1589 let slot = match r.kind() {
1590 ResourceKind::Lend | ResourceKind::Consume => quote!(#res[I]),
1591 ResourceKind::Shared | ResourceKind::Divisible => quote!(#res),
1592 };
1593 quote! {
1594 #(#ecfg)*
1595 if !#cr::ResourceGate::is_filled(&#slot) {
1596 return ::core::result::Result::Err(::embassy_executor::SpawnError::Busy);
1597 }
1598 }
1599 })
1600 .collect();
1601 let pool_spawn_stmts = spawn_stmts(&call, "e!(&#ident[I]), &sp_tokens);
1602 let wrapper = format_ident!("spawn_{}", ident.to_string().to_lowercase());
1603 let mut defs: Vec<TokenStream2> = Vec::new();
1604 defs.push(shell_def);
1605 defs.push(quote! {
1606 #(#cfg)*
1607 fn #wrapper<const I: usize>(
1608 #param: ::embassy_executor::Spawner,
1609 ) -> ::core::result::Result<(), ::embassy_executor::SpawnError> {
1610 #prelude
1611 #state_prelude
1612 #(#get_prelude)*
1613 #pool_spawn_stmts
1614 ::core::result::Result::Ok(())
1615 }
1616 });
1617 let member_spawn: Vec<TokenStream2> = (0..k).map(|j| quote!(#wrapper::<#j>)).collect();
1618
1619 let mut member_with_exec = match &p.executor {
1620 Some(ex) => quote!( .with_executor(&#ex) ),
1621 None => quote!(),
1622 };
1623 if cfg!(feature = "fault-inject") && matches!(p.source, TaskSource::Shell(_)) {
1624 member_with_exec.extend(quote!( .with_shell() ));
1625 }
1626 for r in p
1627 .resources
1628 .iter()
1629 .filter(|r| matches!(r.kind(), ResourceKind::Lend | ResourceKind::Consume))
1630 {
1631 let ecfg = &r.cfg;
1632 let res = &r.ident;
1633 let ty = r.ty.as_ref().expect("a typed resource kind");
1634 let doc = format!(
1635 "Per-member resource slots for pool `{ident}` (generated by \
1636 `supervisor_graph!`): member `I` takes/restores element `I`. \
1637 Provide at least the floor members' elements before \
1638 `Supervisor::start`; a member whose element is empty fail-closes \
1639 its (re)spawn with `SpawnError::Busy`."
1640 );
1641 defs.push(quote! {
1642 #(#cfg)*
1643 #(#ecfg)*
1644 #[doc = #doc]
1645 pub static #res: [#cr::ResourceSlot<#ty>; #k] =
1646 [const { #cr::ResourceSlot::new() }; #k];
1647 });
1648 }
1649 let member_with_res: Vec<TokenStream2> = if p.resources.is_empty() {
1650 (0..k).map(|_| quote!()).collect()
1651 } else {
1652 let gates_len = cfg_aware_len(p.resources.iter().map(|r| &r.cfg));
1653 (0..k)
1654 .map(|j| {
1655 let gates_ident = format_ident!("__SV_GATES_{}_{}", ident, j);
1656 let gate_refs: Vec<TokenStream2> = p
1657 .resources
1658 .iter()
1659 .map(|r| {
1660 let ecfg = &r.cfg;
1661 let res = &r.ident;
1662 match r.kind() {
1663 ResourceKind::Lend | ResourceKind::Consume => {
1664 quote!(#(#ecfg)* &#res[#j])
1665 }
1666 ResourceKind::Shared | ResourceKind::Divisible => {
1667 quote!(#(#ecfg)* &#res)
1668 }
1669 }
1670 })
1671 .collect();
1672 defs.push(quote! {
1673 #(#cfg)*
1674 static #gates_ident: [&'static dyn #cr::ResourceGate; #gates_len] =
1675 [#(#gate_refs),*];
1676 });
1677 quote!( .with_resources(&#gates_ident) )
1678 })
1679 .collect()
1680 };
1681 let member_with_claims: Vec<TokenStream2> = (0..k)
1682 .map(|j| {
1683 let (def, with) = claims_tokens(
1684 &format_ident!("__SV_CLAIMS_{}_{}", ident, j),
1685 cfg,
1686 &p.resources,
1687 &claims,
1688 j,
1689 cr,
1690 );
1691 defs.push(def);
1692 with
1693 })
1694 .collect();
1695 let ctx = EmitCtx {
1703 cfg,
1704 cr,
1705 owner: ident,
1706 };
1707 let marker = |select: fn(&Dep) -> bool, prefix, builder| {
1708 marker_array_tokens(&ctx, &p.deps, pool_names, select, prefix, builder)
1709 };
1710 let (ready_def, member_with_ready) = marker(|d| d.ready.is_some(), "READY", "with_ready_deps");
1711 let (bound_def, member_with_bound) = marker(|d| d.bound.is_some(), "BOUND", "with_bound_deps");
1712 let mut foreign: Vec<AdoptedFn> = Vec::new();
1713 foreign.extend(
1714 discover_fn_path(p.discover.as_ref().map(|g| &g.kw), Some(&p.source))?
1715 .filter(|_| cfg!(feature = "dataflow"))
1716 .map(|path| AdoptedFn {
1717 cfg: p
1718 .discover
1719 .as_ref()
1720 .map(|g| g.cfg.clone())
1721 .unwrap_or_default(),
1722 path,
1723 }),
1724 );
1725 foreign.extend(p.dataflow.iter().cloned());
1726 let (reads_def, member_with_reads) = coupling_binding_tokens(
1727 &ctx,
1728 &p.reads,
1729 "READS",
1730 "with_reads",
1731 observe.reads.as_ref(),
1732 &foreign,
1733 VetoSlots::default(),
1734 );
1735 let veto_bases = plan.veto_slots(&p.ident, &p.writes);
1739 let (writes_def, member_with_writes): (TokenStream2, Vec<TokenStream2>) =
1740 if veto_bases.iter().any(Option::is_some) {
1741 let mut def = quote!();
1742 let withs = (0..k)
1743 .map(|j| {
1744 let member_ident = format_ident!("{}_{}", ident, j);
1745 let member_ctx = EmitCtx {
1746 cfg,
1747 cr,
1748 owner: &member_ident,
1749 };
1750 let (d, w) = coupling_binding_tokens(
1751 &member_ctx,
1752 &p.writes,
1753 "WRITES",
1754 "with_writes",
1755 observe.writes.as_ref(),
1756 &foreign,
1757 VetoSlots {
1758 bases: &veto_bases,
1759 offset: j,
1760 },
1761 );
1762 def.extend(d);
1763 w
1764 })
1765 .collect();
1766 (def, withs)
1767 } else {
1768 let (d, w) = coupling_binding_tokens(
1769 &ctx,
1770 &p.writes,
1771 "WRITES",
1772 "with_writes",
1773 observe.writes.as_ref(),
1774 &foreign,
1775 VetoSlots::default(),
1776 );
1777 (d, (0..k).map(|_| w.clone()).collect())
1778 };
1779 {
1782 let d = p.discover.as_ref().map(|g| g.cfg.as_slice());
1783 let owner = name_string(&p.ident);
1784 defs.push(marker_assert_tokens(
1785 &ctx, &owner, d, &foreign, &p.reads, "READS",
1786 ));
1787 defs.push(marker_assert_tokens(
1788 &ctx, &owner, d, &foreign, &p.writes, "WRITES",
1789 ));
1790 }
1791 defs.extend([ready_def, bound_def, reads_def, writes_def]);
1792
1793 let (member_with_clauses, member_clause_stmts, member_clause_errors) = builder_clause_tokens(
1794 cfg,
1795 [
1796 p.slot_timeout.as_ref().map(|g| {
1797 let (c, ms) = (&g.cfg, &g.value);
1798 GatedBuilder {
1799 cfg: c.clone(),
1800 tokens: quote!( .with_slot_timeout(#cr::_export::Duration::from_millis(#ms)) ),
1801 available: true,
1802 err: "",
1803 }
1804 }),
1805 p.ack_timeout.as_ref().map(|g| {
1806 let (c, ms) = (&g.cfg, &g.value);
1807 GatedBuilder {
1808 cfg: c.clone(),
1809 tokens: quote!( .with_ack_timeout(#cr::_export::Duration::from_millis(#ms)) ),
1810 available: true,
1811 err: "",
1812 }
1813 }),
1814 ]
1815 .into_iter()
1816 .flatten(),
1817 );
1818 defs.push(member_clause_errors);
1819 defs.push(discover_error_tokens(cfg, p.discover.as_ref()));
1820 let member_graph_ref = &helpers.graph_ref;
1821 let member_with_graph =
1822 cfg!(feature = "data-deps").then(|| quote!( .with_graph(&#member_graph_ref) ));
1823 let member_cfg_ident = format_ident!("__SV_CFG_{}", ident);
1824 let member_cfgs = p
1825 .modes
1826 .iter()
1827 .zip(&member_spawn)
1828 .enumerate()
1829 .map(|(j, (mode, sp))| {
1830 let nm = format!("{lname}{j}");
1831 let with_res = &member_with_res[j];
1832 let with_claims = &member_with_claims[j];
1833 let member_with_writes = &member_with_writes[j];
1834 let chain = quote! {
1835 #cr::NodeCfg::new(
1836 #nm, #cr::Mode::#mode,
1837 ::core::option::Option::Some((#sp) as #spawn_fn),
1838 ) #member_with_exec #with_res #with_claims #member_with_clauses
1839 #member_with_ready
1840 #member_with_bound
1841 #member_with_reads #member_with_writes #member_with_graph
1842 };
1843 if member_clause_stmts.is_empty() {
1844 chain
1845 } else {
1846 quote! {{
1847 let __sv_cfg = #chain;
1848 #member_clause_stmts
1849 __sv_cfg
1850 }}
1851 }
1852 });
1853 let members =
1854 (0..p.modes.len()).map(|j| quote!( #cr::TaskNode::new(&#member_cfg_ident[#j], false) ));
1855 defs.push(quote! {
1856 #(#cfg)*
1857 #[doc(hidden)]
1858 static #member_cfg_ident: [#cr::NodeCfg; #k] = [ #(#member_cfgs),* ];
1859 #(#cfg)*
1860 #[doc = concat!("Pool `", stringify!(#ident), "`'s members, one `TaskNode` per slot \
1861 (index = member index). Index it for the per-node verbs; the pool itself is \
1862 `", stringify!(#ident), "_POOL`.")]
1863 pub static #ident: [#cr::TaskNode; #k] = [ #(#members),* ];
1864 });
1865
1866 let min_const = format_ident!("{}_MIN", ident);
1867 let max_const = format_ident!("{}_MAX", ident);
1868 let members_const = format_ident!("{}_MEMBERS", ident);
1869 let (min_tokens, max_tokens, bound_asserts) = match lit_bounds {
1870 Some((min_v, max_v)) => {
1871 let (min_u, max_u) = (usize::from(min_v), usize::from(max_v));
1872 (quote!(#min_u), quote!(#max_u), quote!())
1873 }
1874 None => {
1875 let (min_e, max_e) = (&p.min, &p.max);
1876 (
1877 quote!({ #min_e }),
1878 quote!({ #max_e }),
1879 quote! {
1880 #(#cfg)*
1881 const _: () = ::core::assert!(
1882 #min_const <= #max_const,
1883 "pool `min:` must not exceed `max:`",
1884 );
1885 #(#cfg)*
1886 const _: () = ::core::assert!(
1887 #max_const <= #members_const,
1888 "pool `max:` exceeds the declared member count",
1889 );
1890 #(#cfg)*
1891 const _: () = ::core::assert!(
1892 #max_const <= 255,
1893 "pool `max:` exceeds 255 (ElasticPool bounds are u8)",
1894 );
1895 },
1896 )
1897 }
1898 };
1899 defs.push(quote! {
1900 #(#cfg)*
1901 #[doc = concat!("Pool `", stringify!(#ident), "`'s `min:` floor (validated at expansion or by const assert).")]
1902 pub const #min_const: usize = #min_tokens;
1903 #(#cfg)*
1904 #[doc = concat!("Pool `", stringify!(#ident), "`'s `max:` scaling ceiling — the most members ever running concurrently.")]
1905 pub const #max_const: usize = #max_tokens;
1906 #(#cfg)*
1907 #[doc = concat!("Pool `", stringify!(#ident), "`'s declared member count (the `[TaskNode; K]` array length).")]
1908 pub const #members_const: usize = #k;
1909 #bound_asserts
1910 });
1911
1912 let member_refs = (0..k).map(|j| quote!(&#ident[#j]));
1913 let policy = &p.policy;
1914 let policy_ty = match &p.policy_ty {
1915 Some(ty) => quote!(#ty),
1916 None => {
1917 let path = policy_type(policy)?;
1918 quote!(#path)
1919 }
1920 };
1921 defs.push(quote! {
1922 #(#cfg)*
1923 #[doc = concat!("The `ElasticPool` over the `", stringify!(#ident), "` members: \
1924 the `min:`/`max:` bounds and the scaling policy `Supervisor::run_pools` \
1925 drives. Also reachable through `GRAPH.pools`.")]
1926 pub static #pool_static: #cr::ElasticPool<#policy_ty> = #cr::ElasticPool {
1927 nodes: &[ #(#member_refs),* ],
1928 min: #min_const as u8,
1929 max: #max_const as u8,
1930 policy: #policy,
1931 };
1932 });
1933
1934 let pool_entry = quote!( #(#cfg)* &#pool_static );
1935
1936 let pred = cfg_predicate(cfg);
1937 let slots = (0..k)
1938 .map(|j| Slot {
1939 cfg_pred: pred.clone(),
1940 reference: quote!(&#ident[#j]),
1941 deps: p.deps.clone(),
1942 fragment: p.fragment.clone(),
1943 })
1944 .collect();
1945
1946 Ok((defs, pool_entry, slots))
1947}
1948
1949fn slot_tables(
1950 slots: &[Slot],
1951 names: &HashMap<String, usize>,
1952) -> SynResult<(Vec<TokenStream2>, Vec<TokenStream2>)> {
1953 let mut all_entries: Vec<TokenStream2> = Vec::new();
1954 let mut deps_entries: Vec<TokenStream2> = Vec::new();
1955 for slot in slots {
1956 let reference = &slot.reference;
1957 all_entries.push(match &slot.cfg_pred {
1958 None => quote!(::core::option::Option::Some(#reference)),
1959 Some(pred) => quote!({
1960 #[cfg(#pred)]
1961 { ::core::option::Option::Some(#reference) }
1962 #[cfg(not(#pred))]
1963 { ::core::option::Option::None }
1964 }),
1965 });
1966
1967 let mut dep_toks: Vec<TokenStream2> = Vec::new();
1968 let mut seen: Vec<(u8, String)> = Vec::new();
1969 for d in &slot.deps {
1970 let idx = match names.get(&d.ident.to_string()) {
1971 Some(&i) => i as u8,
1972 None => {
1973 return Err(syn::Error::new_spanned(
1974 &d.ident,
1975 format!(
1976 "unknown dependency `{}` — not a declared node or pool{}",
1977 d.ident,
1978 fragment_suffix(&slot.fragment),
1979 ),
1980 ));
1981 }
1982 };
1983 let cfg = &d.cfg;
1984 let cfg_key = quote!( #(#cfg)* ).to_string();
1985 if seen.iter().any(|(i, k)| *i == idx && *k == cfg_key) {
1986 return Err(syn::Error::new_spanned(
1987 &d.ident,
1988 format!("duplicate dependency `{}`", d.ident),
1989 ));
1990 }
1991 seen.push((idx, cfg_key));
1992 dep_toks.push(quote!( #(#cfg)* #idx ));
1993 }
1994 deps_entries.push(quote!( &[ #(#dep_toks),* ] ));
1995 }
1996 Ok((all_entries, deps_entries))
1997}
1998
1999fn expand(graph: GraphSpec) -> SynResult<TokenStream2> {
2000 gate::gate(&graph)?;
2001 let cr = quote!(::embassy_supervisor);
2002 let helpers = HelperIdents::new(graph.name.as_ref());
2003 let observe = ObserveDefaults {
2004 writes: graph.observe_writes.as_ref().map(|(_, e)| e.clone()),
2005 reads: graph.observe_reads.as_ref().map(|(_, e)| e.clone()),
2006 };
2007 let spawn_fn = quote!(
2008 fn(
2009 ::embassy_executor::Spawner,
2010 ) -> ::core::result::Result<(), ::embassy_executor::SpawnError>
2011 );
2012
2013 let mut defs: Vec<TokenStream2> = Vec::new();
2014 let mut pool_entries: Vec<TokenStream2> = Vec::new();
2015 let mut slots: Vec<Slot> = Vec::new();
2016 let mut names: HashMap<String, usize> = HashMap::new();
2017
2018 let has_state = |want: fn(&StateInit) -> bool| {
2019 graph.items.iter().any(|item| match item {
2020 Item::Node(n) => n.state.as_ref().is_some_and(|(_, _, i)| want(i)),
2021 Item::Pool(p) => p.state.as_ref().is_some_and(|(_, _, i)| want(i)),
2022 Item::Executor(_) => false,
2023 })
2024 };
2025 let any_init_state = has_state(|i| matches!(i, StateInit::Expr(_)));
2026 let any_zeroed_state = has_state(|i| matches!(i, StateInit::Zeroed(_)));
2027 let alloc_alias = &helpers.alloc_alias;
2028 if any_init_state || any_zeroed_state {
2029 defs.push(quote! {
2030 extern crate alloc as #alloc_alias;
2031 });
2032 }
2033 if any_init_state {
2034 let try_box = &helpers.try_box;
2035 defs.push(quote! {
2036 #[doc(hidden)]
2037 fn #try_box<T>(init: T) -> ::core::option::Option<#alloc_alias::boxed::Box<T>> {
2038 let layout = ::core::alloc::Layout::new::<T>();
2039 if layout.size() == 0 {
2040 ::core::mem::forget(init);
2041 return ::core::option::Option::Some(unsafe {
2042 #alloc_alias::boxed::Box::from_raw(
2043 ::core::ptr::NonNull::<T>::dangling().as_ptr(),
2044 )
2045 });
2046 }
2047 unsafe {
2048 let p = #alloc_alias::alloc::alloc(layout) as *mut T;
2049 if p.is_null() {
2050 return ::core::option::Option::None;
2051 }
2052 ::core::ptr::write(p, init);
2053 ::core::option::Option::Some(#alloc_alias::boxed::Box::from_raw(p))
2054 }
2055 }
2056 });
2057 }
2058 if any_zeroed_state {
2059 let try_box_zeroed = &helpers.try_box_zeroed;
2060 defs.push(quote! {
2061 #[doc(hidden)]
2062 fn #try_box_zeroed<T: #cr::Zeroable>() -> ::core::option::Option<#alloc_alias::boxed::Box<T>> {
2063 let layout = ::core::alloc::Layout::new::<T>();
2064 if layout.size() == 0 {
2065 return ::core::option::Option::Some(unsafe {
2066 #alloc_alias::boxed::Box::from_raw(
2067 ::core::ptr::NonNull::<T>::dangling().as_ptr(),
2068 )
2069 });
2070 }
2071 unsafe {
2072 let p = #alloc_alias::alloc::alloc_zeroed(layout) as *mut T;
2073 if p.is_null() {
2074 return ::core::option::Option::None;
2075 }
2076 ::core::option::Option::Some(#alloc_alias::boxed::Box::from_raw(p))
2077 }
2078 }
2079 });
2080 }
2081
2082 let any_local = graph
2083 .items
2084 .iter()
2085 .any(|item| item_resources(item).iter().any(|r| r.local.is_some()));
2086 if any_local {
2087 let local = helpers.local_slot.clone();
2088 let cell = quote!(::core::cell::Cell<::core::option::Option<T>>);
2089 let raw = quote!(#cr::_export::CriticalSectionRawMutex);
2090 let signal = quote!(#cr::_export::Signal<#raw, ()>);
2091 defs.push(quote! {
2092 #[allow(dead_code)]
2093 pub struct #local<T> {
2094 slot: #cr::_export::BlockingMutex<#raw, #cell>,
2095 filled: #signal,
2096 }
2097 unsafe impl<T> ::core::marker::Sync for #local<T> {}
2098 #[allow(dead_code)]
2099 impl<T> #local<T> {
2100 pub const fn new() -> Self {
2101 Self {
2102 slot: #cr::_export::BlockingMutex::new(
2103 ::core::cell::Cell::new(::core::option::Option::None),
2104 ),
2105 filled: #cr::_export::Signal::new(),
2106 }
2107 }
2108 pub fn provide(&self, value: T) {
2109 self.slot.lock(|c| c.set(::core::option::Option::Some(value)));
2110 self.filled.signal(());
2111 #cr::__sv_gate_event();
2112 }
2113 pub fn take(&self) -> ::core::option::Option<T> {
2114 self.slot.lock(::core::cell::Cell::take)
2115 }
2116 pub fn restore(&self, value: T) {
2117 self.provide(value);
2118 }
2119 }
2120 #[allow(dead_code)]
2121 impl<T: ::core::marker::Copy> #local<T> {
2122 pub fn get(&self) -> ::core::option::Option<T> {
2123 self.slot.lock(|c| {
2124 let v = c.take();
2125 c.set(v);
2126 v
2127 })
2128 }
2129 }
2130 impl<T> ::core::default::Default for #local<T> {
2131 fn default() -> Self {
2132 Self::new()
2133 }
2134 }
2135 impl<T> #cr::ResourceGate for #local<T> {
2136 fn is_filled(&self) -> bool {
2139 self.slot.lock(|c| {
2140 unsafe { (*c.as_ptr()).is_some() }
2142 })
2143 }
2144 fn filled_signal(&self) -> &#signal {
2145 &self.filled
2146 }
2147 fn clear(&self) {
2148 let stale = self.slot.lock(::core::cell::Cell::take);
2149 drop(stale);
2150 self.filled.reset();
2151 }
2152 }
2153 });
2154 }
2155
2156 let helpers = HelperIdents::new(graph.name.as_ref());
2157 let executor_names: Vec<String> = graph
2158 .items
2159 .iter()
2160 .filter_map(|i| match i {
2161 Item::Executor(x) => Some(x.ident.to_string()),
2162 _ => None,
2163 })
2164 .collect();
2165 let pool_names: std::collections::HashSet<String> = graph
2166 .items
2167 .iter()
2168 .filter_map(|i| match i {
2169 Item::Pool(p) => Some(p.ident.to_string()),
2170 _ => None,
2171 })
2172 .collect();
2173
2174 struct GraphSlotPlan<'a> {
2177 decl: &'a ResourceDecl,
2179 sig: String,
2181 preds: Vec<Option<TokenStream2>>,
2184 owners: Vec<String>,
2185 executor: Option<String>,
2188 slots: usize,
2191 }
2192 let mut shared_plans: Vec<(String, GraphSlotPlan)> = Vec::new();
2193 let mut claim_bases: HashMap<(String, String), u8> = HashMap::new();
2195 {
2196 let mut taken: HashSet<String> = HashSet::new();
2197 for item in &graph.items {
2198 let Some((owner, item_cfg)) = item_ident_cfg(item) else {
2199 continue;
2200 };
2201 let item_pred = cfg_predicate(item_cfg);
2202 let member_count = match item {
2203 Item::Pool(p) => p.modes.len(),
2204 _ => 1,
2205 };
2206 let executor_text = item_executor(item).map(ToString::to_string);
2207 for r in item_resources(item) {
2208 let key = r.ident.to_string();
2209 if executor_names.contains(&key) {
2210 return Err(syn::Error::new_spanned(
2211 &r.ident,
2212 format!(
2213 "resource name `{}` shadows an `executor {};` slot — \
2214 both are statics at the declaration site",
2215 r.ident, r.ident
2216 ),
2217 ));
2218 }
2219 let pred = match (item_pred.clone(), cfg_predicate(&r.cfg)) {
2220 (None, None) => None,
2221 (Some(p), None) | (None, Some(p)) => Some(p),
2222 (Some(a), Some(b)) => Some(quote!(all(#a, #b))),
2223 };
2224 if matches!(r.kind(), ResourceKind::Shared | ResourceKind::Divisible) {
2225 if taken.contains(&key) {
2226 return Err(syn::Error::new_spanned(
2227 &r.ident,
2228 format!(
2229 "`{}` is already a take-kind resource elsewhere in \
2230 the graph — a name is either one exclusive slot or \
2231 one `shared`/`divisible` slot, not both",
2232 r.ident
2233 ),
2234 ));
2235 }
2236 let sig = if r.kind() == ResourceKind::Divisible {
2237 "divisible".to_string()
2238 } else {
2239 r.shared_signature()
2240 };
2241 let plan = match shared_plans.iter_mut().find(|(k, _)| *k == key) {
2242 Some((_, plan)) => {
2243 if plan.sig != sig {
2244 return Err(syn::Error::new_spanned(
2245 &r.ident,
2246 format!(
2247 "shared resource `{}` re-declared with a \
2248 different shape: `{}` here vs `{}` on \
2249 `{}` — every declaration of a shared slot \
2250 must repeat the same kind markers and type",
2251 r.ident, sig, plan.sig, plan.owners[0]
2252 ),
2253 ));
2254 }
2255 if let Some(marker) = &r.serialized
2261 && executor_text != plan.executor
2262 {
2263 let tier = |e: &Option<String>| match e {
2264 Some(x) => format!("`{x}`"),
2265 None => "the supervisor's executor".to_string(),
2266 };
2267 return Err(syn::Error::new_spanned(
2268 marker,
2269 format!(
2270 "`{}` is `serialized`: every holder must run on one \
2271 executor so no higher-tier waiter can be starved by \
2272 a lower-tier holder (priority ceiling by \
2273 construction), but `{}` runs on {} and `{}` on {}",
2274 r.ident,
2275 plan.owners[0],
2276 tier(&plan.executor),
2277 owner,
2278 tier(&executor_text),
2279 ),
2280 ));
2281 }
2282 plan.preds.push(pred);
2283 plan.owners.push(owner.to_string());
2284 plan
2285 }
2286 None => {
2287 shared_plans.push((
2288 key.clone(),
2289 GraphSlotPlan {
2290 decl: r,
2291 sig,
2292 preds: vec![pred],
2293 owners: vec![owner.to_string()],
2294 executor: executor_text.clone(),
2295 slots: 0,
2296 },
2297 ));
2298 &mut shared_plans.last_mut().expect("just pushed").1
2299 }
2300 };
2301 if r.kind() == ResourceKind::Divisible {
2302 let base = u8::try_from(plan.slots)
2305 .ok()
2306 .filter(|_| plan.slots + member_count <= usize::from(u8::MAX) + 1);
2307 let Some(base) = base else {
2308 return Err(syn::Error::new_spanned(
2309 &r.ident,
2310 format!(
2311 "divisible resource `{}` has more than 256 claimant \
2312 slots across the graph — slots are `u8`",
2313 r.ident
2314 ),
2315 ));
2316 };
2317 claim_bases.insert((owner.to_string(), key), base);
2318 plan.slots += member_count;
2319 }
2320 } else {
2321 if !taken.insert(key.clone()) || shared_plans.iter().any(|(k, _)| *k == key) {
2322 return Err(syn::Error::new_spanned(
2323 &r.ident,
2324 format!(
2325 "duplicate resource name `{}` — resource slots are \
2326 statics and must be unique across the graph (only \
2327 `shared` entries may repeat a name)",
2328 r.ident
2329 ),
2330 ));
2331 }
2332 }
2333 }
2334 }
2335 }
2336 {
2339 let tier = |e: &Option<String>| match e {
2340 Some(x) => format!("`{x}`"),
2341 None => "the supervisor's executor".to_string(),
2342 };
2343 let mut homes: HashMap<String, (String, Option<String>)> = HashMap::new();
2345 for item in &graph.items {
2346 let Some((owner, _)) = item_ident_cfg(item) else {
2347 continue;
2348 };
2349 let executor_text = item_executor(item).map(ToString::to_string);
2350 for r in item_resources(item) {
2351 let Some(marker) = &r.local else {
2352 continue;
2353 };
2354 match homes.get(&r.ident.to_string()) {
2355 Some((first, home)) if *home != executor_text => {
2356 return Err(syn::Error::new_spanned(
2357 marker,
2358 format!(
2359 "`{}` is `local`: every declaration must run on one \
2360 executor (its value is only ever touched from that \
2361 executor), but `{}` runs on {} and `{}` on {}",
2362 r.ident,
2363 first,
2364 tier(home),
2365 owner,
2366 tier(&executor_text),
2367 ),
2368 ));
2369 }
2370 Some(_) => {}
2371 None => {
2372 homes.insert(
2373 r.ident.to_string(),
2374 (owner.to_string(), executor_text.clone()),
2375 );
2376 }
2377 }
2378 }
2379 }
2380 for item in &graph.items {
2381 let Item::Node(n) = item else {
2382 continue;
2383 };
2384 let executor_text = n.executor.as_ref().map(ToString::to_string);
2385 for p in &n.provides {
2386 if let Some((first, home)) = homes.get(&p.ident.to_string())
2387 && *home != executor_text
2388 {
2389 return Err(syn::Error::new_spanned(
2390 &p.ident,
2391 format!(
2392 "`{}` is `local`: its provider must run on the executor \
2393 of its declarations (the value is only ever touched \
2394 from that executor), but `{}` declares it on {} and \
2395 `{}` provides it on {}",
2396 p.ident,
2397 first,
2398 tier(home),
2399 n.ident,
2400 tier(&executor_text),
2401 ),
2402 ));
2403 }
2404 }
2405 }
2406 }
2407 for (_, plan) in &shared_plans {
2408 let res = &plan.decl.ident;
2409 let cfg_attr = if plan.preds.iter().any(|p| p.is_none()) {
2410 quote!()
2411 } else {
2412 let preds = plan.preds.iter().flatten();
2413 quote!(#[cfg(any(#(#preds),*))])
2414 };
2415 if plan.decl.kind() == ResourceKind::Divisible {
2416 let k = plan.slots;
2417 let doc = format!(
2418 "Divisible budget declared by `{}` (generated by `supervisor_graph!`): \
2419 {k} claimant slot(s), one per declaring node or pool member, in \
2420 declaration order. `provide()` the capacity before the holders \
2421 start (or from an allocator node that names it in `provides:`), \
2422 and `rebalance()` it with a `BudgetPolicy` when the wants move.",
2423 plan.owners.join("`, `"),
2424 );
2425 defs.push(quote! {
2426 #cfg_attr
2427 #[doc = #doc]
2428 pub static #res: #cr::Budget<#k> = #cr::Budget::new();
2429 });
2430 continue;
2431 }
2432 let ty = plan.decl.ty.as_ref().expect("a typed resource kind");
2433 let slot_ty = if plan.decl.local.is_some() {
2434 let local = &helpers.local_slot;
2435 quote!(#local<#ty>)
2436 } else {
2437 quote!(#cr::ResourceSlot<#ty>)
2438 };
2439 let doc = format!(
2440 "Shared (fan-out) resource slot declared by `{}` (generated by \
2441 `supervisor_graph!`). `provide()` the `Copy` handle before \
2442 `Supervisor::start`; every consumer's glue copies it out with \
2443 `get()`, so the slot STAYS FILLED — re-`provide()` only to replace \
2444 the handle (e.g. after rebuilding the underlying driver).",
2445 plan.owners.join("`, `"),
2446 );
2447 defs.push(quote! {
2448 #cfg_attr
2449 #[doc = #doc]
2450 pub static #res: #slot_ty = <#slot_ty>::new();
2451 });
2452 }
2453
2454 let mut resource_cfgs: HashMap<String, (Vec<Attribute>, bool)> = HashMap::new();
2455 for item in &graph.items {
2456 let from_pool = matches!(item, Item::Pool(_));
2457 for r in item_resources(item) {
2458 let per_member = matches!(r.kind(), ResourceKind::Lend | ResourceKind::Consume);
2459 resource_cfgs
2460 .entry(r.ident.to_string())
2461 .or_insert_with(|| (r.cfg.clone(), from_pool && per_member));
2462 }
2463 }
2464 struct VetoPlan {
2472 key: String,
2473 target: TokenStream2,
2474 total: usize,
2475 preds: Vec<Option<TokenStream2>>,
2479 }
2480 let mut veto_bases: HashMap<(String, String), u8> = HashMap::new();
2481 let mut veto_plans: Vec<VetoPlan> = Vec::new();
2482 let mut veto_stems: HashMap<String, String> = HashMap::new();
2484 for item in &graph.items {
2485 let Some((owner, item_cfg)) = item_ident_cfg(item) else {
2486 continue;
2487 };
2488 let item_pred = cfg_predicate(item_cfg);
2489 let member_count = match item {
2490 Item::Pool(p) => p.modes.len(),
2491 _ => 1,
2492 };
2493 let writes = match item {
2494 Item::Node(n) => &n.writes[..],
2495 Item::Pool(p) => &p.writes[..],
2496 Item::Executor(_) => &[][..],
2497 };
2498 for d in writes.iter().filter(|d| d.veto.is_some()) {
2499 let key = d.display();
2500 let stem = match (d.path.segments.last(), key.find('[')) {
2501 (Some(last), Some(at)) => format!("{}{}", last.ident, &key[at..]),
2502 (Some(last), None) => last.ident.to_string(),
2503 (None, _) => key.clone(),
2504 };
2505 match veto_stems.get(&stem) {
2506 Some(first) if *first != key => {
2507 return Err(syn::Error::new_spanned(
2508 &d.path,
2509 format!(
2510 "`{first}` and `{key}` both name a `veto` gate ending in \
2511 `{stem}`: contributor slots are numbered per spelling, so \
2512 one static named two ways would hand two writers the same \
2513 bit — spell the gate one way across the graph, or alias one \
2514 of two distinct statics with `use .. as`"
2515 ),
2516 ));
2517 }
2518 Some(_) => {}
2519 None => {
2520 veto_stems.insert(stem, key.clone());
2521 }
2522 }
2523 let pred = match (item_pred.clone(), cfg_predicate(&d.cfg)) {
2524 (None, None) => None,
2525 (Some(p), None) | (None, Some(p)) => Some(p),
2526 (Some(a), Some(b)) => Some(quote!(all(#a, #b))),
2527 };
2528 let plan = match veto_plans.iter_mut().find(|p| p.key == key) {
2529 Some(plan) => plan,
2530 None => {
2531 veto_plans.push(VetoPlan {
2532 key: key.clone(),
2533 target: d.target(),
2534 total: 0,
2535 preds: Vec::new(),
2536 });
2537 veto_plans.last_mut().expect("just pushed")
2538 }
2539 };
2540 if plan.total + member_count > 32 {
2541 return Err(syn::Error::new_spanned(
2542 &d.path,
2543 format!(
2544 "`{key}` has more than 32 `veto` writers across the graph — a \
2545 `VetoGate` holds at most 32 contributors"
2546 ),
2547 ));
2548 }
2549 veto_bases.insert((owner.to_string(), key), plan.total as u8);
2550 plan.total += member_count;
2551 plan.preds.push(pred);
2552 }
2553 }
2554 for plan in &veto_plans {
2555 let (target, total) = (&plan.target, plan.total);
2556 let cfg_attr = if plan.preds.iter().any(|p| p.is_none()) {
2557 quote!()
2558 } else {
2559 let preds = plan.preds.iter().flatten();
2560 quote!(#[cfg(any(#(#preds),*))])
2561 };
2562 defs.push(quote! {
2563 #cfg_attr
2564 const _: () = #cr::__sv_check_veto(&#target, #total);
2565 });
2566 }
2567 let plan = ResourcePlan {
2568 cfgs: resource_cfgs,
2569 claim_bases,
2570 veto_bases,
2571 };
2572
2573 for item in &graph.items {
2574 match item {
2575 Item::Node(n) => {
2576 if let Some(ex) = &n.executor
2577 && !executor_names.contains(&ex.to_string())
2578 {
2579 return Err(syn::Error::new_spanned(
2580 ex,
2581 format!(
2582 "unknown executor `{ex}`; declare it in the graph with \
2583 `executor {ex};` (declared: [{}])",
2584 executor_names.join(", ")
2585 ),
2586 ));
2587 }
2588 if names.insert(n.ident.to_string(), slots.len()).is_some() {
2589 return Err(syn::Error::new_spanned(
2590 &n.ident,
2591 format!(
2592 "duplicate node/pool name `{}`{}",
2593 n.ident,
2594 fragment_suffix(&n.fragment),
2595 ),
2596 ));
2597 }
2598 let (def, slot) =
2599 emit_node(n, &cr, &spawn_fn, &pool_names, &helpers, &observe, &plan)?;
2600 defs.push(def);
2601 slots.push(slot);
2602 }
2603 Item::Executor(x) => {
2604 let (cfg, ident) = (&x.cfg, &x.ident);
2605 defs.push(quote! {
2606 #(#cfg)*
2607 pub static #ident: #cr::SpawnerSlot = #cr::SpawnerSlot::new();
2608 });
2609 }
2610 Item::Pool(p) => {
2611 if cfg!(feature = "pool") {
2612 if let Some(ex) = &p.executor
2613 && !executor_names.contains(&ex.to_string())
2614 {
2615 return Err(syn::Error::new_spanned(
2616 ex,
2617 format!(
2618 "unknown executor `{ex}`; declare it in the graph with \
2619 `executor {ex};` (declared: [{}])",
2620 executor_names.join(", ")
2621 ),
2622 ));
2623 }
2624 let (pool_defs, pool_entry, pool_slots) =
2625 emit_pool(p, &cr, &spawn_fn, &pool_names, &helpers, &observe, &plan)?;
2626 if names.insert(p.ident.to_string(), slots.len()).is_some() {
2627 return Err(syn::Error::new_spanned(
2628 &p.ident,
2629 format!(
2630 "duplicate node/pool name `{}`{}",
2631 p.ident,
2632 fragment_suffix(&p.fragment),
2633 ),
2634 ));
2635 }
2636 defs.extend(pool_defs);
2637 pool_entries.push(pool_entry);
2638 slots.extend(pool_slots);
2639 } else {
2640 return Err(syn::Error::new_spanned(
2641 &p.ident,
2642 "a `pool` requires enabling embassy-supervisor's `pool` feature",
2643 ));
2644 }
2645 }
2646 }
2647 }
2648
2649 let m = slots.len();
2650 if m > 256 {
2651 return Err(syn::Error::new(
2652 proc_macro2::Span::call_site(),
2653 format!(
2654 "supervisor_graph!: {m} node slots declared, but at most 256 are supported \
2655 (including pool members) — graph indices are `u8`"
2656 ),
2657 ));
2658 }
2659 let (all_entries, deps_entries) = slot_tables(&slots, &names)?;
2660
2661 let shape_bits: u32 = {
2662 const READY_DEPS: u32 = 1 << 0;
2663 const EXEC_SLOTS: u32 = 1 << 1;
2664 const RESOURCES: u32 = 1 << 2;
2665 const PAUSE: u32 = 1 << 3;
2666 const ON_DEMAND: u32 = 1 << 4;
2667 const BEATS: u32 = 1 << 5;
2668 const OBSERVED: u32 = 1 << 6;
2669 const BOUND_DEPS: u32 = 1 << 7;
2670 const POOLS: u32 = 1 << 8;
2671 const CLAIMS: u32 = 1 << 9;
2672 let claims_bit = |resources: &[ResourceDecl]| {
2673 if resources
2674 .iter()
2675 .any(|r| r.kind() == ResourceKind::Divisible)
2676 {
2677 CLAIMS
2678 } else {
2679 0
2680 }
2681 };
2682 let mode_bit = |m: &Ident| match m.to_string().as_str() {
2683 "Pause" => PAUSE,
2684 "OnDemand" => ON_DEMAND,
2685 _ => 0,
2686 };
2687 let dep_bits = |deps: &[Dep]| {
2688 let mut b = 0;
2689 for d in deps {
2690 if d.ready.is_some() || d.bound.is_some() {
2691 b |= READY_DEPS;
2692 }
2693 if d.bound.is_some() {
2694 b |= BOUND_DEPS;
2695 }
2696 }
2697 b
2698 };
2699 let observed_bit = |reads: &[SignalDecl], writes: &[SignalDecl]| {
2700 if reads.iter().chain(writes).any(|s| s.observed.is_some()) {
2701 OBSERVED
2702 } else {
2703 0
2704 }
2705 };
2706 let mut bits = 0;
2707 for item in &graph.items {
2708 match item {
2709 Item::Node(n) => {
2710 bits |= mode_bit(&n.mode);
2711 bits |= dep_bits(&n.deps);
2712 bits |= observed_bit(&n.reads, &n.writes);
2713 if n.executor.is_some() {
2714 bits |= EXEC_SLOTS;
2715 }
2716 if !n.resources.is_empty() {
2717 bits |= RESOURCES;
2718 }
2719 bits |= claims_bit(&n.resources);
2720 if n.beat_timeout.is_some() {
2721 bits |= BEATS;
2722 }
2723 if n.ready_on_write.is_some() {
2724 bits |= OBSERVED;
2725 }
2726 }
2727 Item::Pool(p) => {
2728 bits |= POOLS;
2729 for m in &p.modes {
2730 bits |= mode_bit(m);
2731 }
2732 bits |= dep_bits(&p.deps);
2733 bits |= observed_bit(&p.reads, &p.writes);
2734 if p.executor.is_some() {
2735 bits |= EXEC_SLOTS;
2736 }
2737 if !p.resources.is_empty() {
2738 bits |= RESOURCES;
2739 }
2740 bits |= claims_bit(&p.resources);
2741 }
2742 Item::Executor(_) => {}
2743 }
2744 }
2745 bits
2746 };
2747 let shape_lit = proc_macro2::Literal::u32_suffixed(shape_bits);
2748 let flat = slots.iter().all(|s| s.deps.is_empty());
2749
2750 let pools_field = if cfg!(feature = "pool") {
2751 quote!( pools: &[ #(#pool_entries),* ], )
2752 } else {
2753 quote!()
2754 };
2755
2756 let trace_hooks = if cfg!(feature = "trace-hooks") && graph.name.is_none() {
2759 quote!( #cr::__sv_trace_hooks!(); )
2760 } else {
2761 quote!()
2762 };
2763
2764 let graph_ident = graph
2765 .name
2766 .clone()
2767 .unwrap_or_else(|| Ident::new("GRAPH", proc_macro2::Span::call_site()));
2768 let nodes_ident = helpers.nodes.clone();
2769 let deps_ident = match &graph.name {
2770 Some(n) => format_ident!("__SV_DEPS_{}", n),
2771 None => Ident::new("DEPS", proc_macro2::Span::call_site()),
2772 };
2773 let graph_ref_ident = helpers.graph_ref.clone();
2774 let (graph_ref_def, graph_ref_field) = if cfg!(feature = "graph-ref") {
2775 (
2776 quote!( static #graph_ref_ident: #cr::GraphRef = #cr::GraphRef::new(&#nodes_ident); ),
2777 quote!( graph_ref: &#graph_ref_ident, ),
2778 )
2779 } else {
2780 (quote!(), quote!())
2781 };
2782 let topo_alias = format_ident!("{}_TOPOLOGY", graph_ident);
2783 let (topo_ty, topo_val, deps_def) = if flat {
2784 (
2785 quote!( #cr::Flat<#shape_lit> ),
2786 quote!( #cr::Flat::new() ),
2787 quote!(),
2788 )
2789 } else {
2790 (
2791 quote!( #cr::Ordered<#m, #shape_lit> ),
2792 quote!( #cr::Ordered::new(&#deps_ident) ),
2793 quote!( const #deps_ident: [&'static [u8]; #m] = [ #(#deps_entries),* ]; ),
2794 )
2795 };
2796 Ok(quote! {
2797 #(#defs)*
2798
2799 static #nodes_ident: [::core::option::Option<&'static #cr::TaskNode>; #m] = [ #(#all_entries),* ];
2803 #deps_def
2804 #graph_ref_def
2805
2806 #[allow(non_camel_case_types)]
2807 pub type #topo_alias = #topo_ty;
2808
2809 pub static #graph_ident: #cr::Graph<#m, #topo_alias> = #cr::Graph {
2810 nodes: &#nodes_ident,
2811 topo: #topo_val,
2812 #pools_field
2813 #graph_ref_field
2814 };
2815
2816 #trace_hooks
2817 })
2818}
2819
2820#[proc_macro_attribute]
2821pub fn dataflow(args: TokenStream, item: TokenStream) -> TokenStream {
2822 dataflow_expand(args.into(), item.into())
2823 .unwrap_or_else(syn::Error::into_compile_error)
2824 .into()
2825}
2826
2827#[proc_macro_attribute]
2828pub fn dataflow_bundle(args: TokenStream, item: TokenStream) -> TokenStream {
2829 bundle_expand(args.into(), item.into())
2830 .unwrap_or_else(syn::Error::into_compile_error)
2831 .into()
2832}
2833
2834fn bundle_expand(args: TokenStream2, item: TokenStream2) -> SynResult<TokenStream2> {
2835 use quote::ToTokens;
2836
2837 let mut m: syn::ItemMod = syn::parse2(item)?;
2838 if !cfg!(feature = "dataflow") {
2839 return Err(syn::Error::new_spanned(
2840 &m.ident,
2841 "`#[dataflow_bundle]` requires the `dataflow` feature \
2842 (embassy-supervisor feature `dataflow`), like the `#[dataflow]` \
2843 fns it bundles",
2844 ));
2845 }
2846 let name: Ident = if args.is_empty() {
2847 format_ident!("BUNDLE")
2848 } else {
2849 syn::parse2(args)?
2850 };
2851 let Some((_, items)) = &m.content else {
2852 return Err(syn::Error::new_spanned(
2853 &m.ident,
2854 "`#[dataflow_bundle]` needs an inline module (`mod x { .. }`) — \
2855 the member fns' bodies are its input, and a `mod x;` declaration \
2856 does not carry them",
2857 ));
2858 };
2859
2860 let cr = quote!(::embassy_supervisor);
2861 let mut reads: Vec<DerivedEntry> = Vec::new();
2862 let mut writes: Vec<DerivedEntry> = Vec::new();
2863 let mut members = 0usize;
2864 for it in items {
2865 let syn::Item::Fn(f) = it else { continue };
2866 let Some((attr, _)) = f
2867 .attrs
2868 .iter()
2869 .find_map(embassy_supervisor_syntax::dataflow_attr)
2870 else {
2871 continue;
2872 };
2873 members += 1;
2874 let verbs: VerbTable = match &attr.meta {
2875 syn::Meta::Path(_) => VerbTable::builtin(),
2876 syn::Meta::List(l) => syn::parse2(l.tokens.clone())?,
2877 syn::Meta::NameValue(_) => {
2878 return Err(syn::Error::new_spanned(
2879 attr,
2880 "`#[dataflow]` takes verb registrations, not a value",
2881 ));
2882 }
2883 };
2884 let Some(param) = node_param(&f.sig) else {
2885 return Err(syn::Error::new_spanned(
2886 &f.sig.ident,
2887 "`#[dataflow]` needs a `&'static TaskNode` parameter — the \
2888 verbs it derives from are called through it",
2889 ));
2890 };
2891 let fn_cfgs: Vec<TokenStream2> = f
2892 .attrs
2893 .iter()
2894 .filter_map(|a| match &a.meta {
2895 syn::Meta::List(l) if l.path.is_ident("cfg") => Some(l.tokens.clone()),
2896 _ => None,
2897 })
2898 .collect();
2899 rewrite_verb_calls(
2900 f.block.to_token_stream(),
2901 ¶m.to_string(),
2902 &verbs,
2903 &mut |call| {
2904 let mut cfgs = fn_cfgs.clone();
2905 cfgs.extend(call.cfgs.iter().cloned());
2906 record_derived(
2907 if call.write { &mut writes } else { &mut reads },
2908 &call,
2909 cfgs,
2910 );
2911 Ok(None)
2912 },
2913 )?;
2914 }
2915 if members == 0 {
2916 return Err(syn::Error::new_spanned(
2917 &m.ident,
2918 "`#[dataflow_bundle]` found no `#[dataflow]` fn at the module's \
2919 top level — nothing to bundle",
2920 ));
2921 }
2922
2923 let reads_ident = format_ident!("__SV_DATAFLOW_READS_{}", name);
2924 let writes_ident = format_ident!("__SV_DATAFLOW_WRITES_{}", name);
2925 let read_entries: Vec<TokenStream2> =
2926 reads.iter().map(|e| derived_entry_tokens(&cr, e)).collect();
2927 let write_entries: Vec<TokenStream2> = writes
2928 .iter()
2929 .map(|e| derived_entry_tokens(&cr, e))
2930 .collect();
2931 let nr = derived_prefix_expr(&reads);
2932 let nw = derived_prefix_expr(&writes);
2933 let statics = quote! {
2934 #[doc(hidden)]
2935 #[allow(non_upper_case_globals)]
2936 pub static #reads_ident: [#cr::Coupling; #nr] = [#(#read_entries),*];
2937 #[doc(hidden)]
2938 #[allow(non_upper_case_globals)]
2939 pub static #writes_ident: [#cr::Coupling; #nw] = [#(#write_entries),*];
2940 };
2941 m.content
2942 .as_mut()
2943 .expect("checked inline above")
2944 .1
2945 .push(syn::Item::Verbatim(statics));
2946 Ok(m.to_token_stream())
2947}
2948
2949fn derived_predicate(alts: &[Vec<TokenStream2>]) -> Option<TokenStream2> {
2950 if alts.iter().any(|a| a.is_empty()) {
2951 return None;
2952 }
2953 let one = |alt: &Vec<TokenStream2>| -> TokenStream2 {
2954 match alt.as_slice() {
2955 [p] => p.clone(),
2956 many => quote!(all(#(#many),*)),
2957 }
2958 };
2959 match alts {
2960 [] => None,
2961 [alt] => Some(one(alt)),
2962 many => {
2963 let terms: Vec<TokenStream2> = many.iter().map(one).collect();
2964 Some(quote!(any(#(#terms),*)))
2965 }
2966 }
2967}
2968
2969fn derived_prefix_expr<E: DerivedAlts>(entries: &[E]) -> TokenStream2 {
2970 let attrs: Vec<Vec<Attribute>> = entries
2971 .iter()
2972 .map(|e| match derived_predicate(e.alts()) {
2973 None => Vec::new(),
2974 Some(pred) => vec![syn::parse_quote!(#[cfg(#pred)])],
2975 })
2976 .collect();
2977 cfg_aware_len(attrs.iter())
2978}
2979
2980trait DerivedAlts {
2981 fn alts(&self) -> &[Vec<TokenStream2>];
2982}
2983
2984struct DerivedEntry {
2985 path: String,
2986 target: syn::Expr,
2987 alts: Vec<Vec<TokenStream2>>,
2988}
2989impl DerivedAlts for DerivedEntry {
2990 fn alts(&self) -> &[Vec<TokenStream2>] {
2991 &self.alts
2992 }
2993}
2994
2995fn record_derived(
2996 list: &mut Vec<DerivedEntry>,
2997 call: &embassy_supervisor_syntax::VerbCall,
2998 cfgs: Vec<TokenStream2>,
2999) -> usize {
3000 match list.iter().position(|e| e.path == call.path) {
3001 Some(k) => {
3002 let text = |a: &[TokenStream2]| {
3003 a.iter()
3004 .map(|t| t.to_string())
3005 .collect::<Vec<_>>()
3006 .join(",")
3007 };
3008 if !list[k].alts.iter().any(|a| text(a) == text(&cfgs)) {
3009 list[k].alts.push(cfgs);
3010 }
3011 k
3012 }
3013 None => {
3014 list.push(DerivedEntry {
3015 path: call.path.clone(),
3016 target: call.target.clone(),
3017 alts: vec![cfgs],
3018 });
3019 list.len() - 1
3020 }
3021 }
3022}
3023
3024fn derived_entry_tokens(cr: &TokenStream2, e: &DerivedEntry) -> TokenStream2 {
3025 let (path, target) = (&e.path, &e.target);
3026 let plain = quote!( #cr::Coupling::new(#path, & #target) );
3027 match derived_predicate(&e.alts) {
3028 None => plain,
3029 Some(pred) => quote!( #[cfg(#pred)] #plain ),
3030 }
3031}
3032
3033fn dataflow_expand(args: TokenStream2, item: TokenStream2) -> SynResult<TokenStream2> {
3034 use quote::ToTokens;
3035
3036 let mut f: syn::ItemFn = syn::parse2(item)?;
3037 if !cfg!(feature = "dataflow") {
3038 return Err(syn::Error::new_spanned(
3039 &f.sig.ident,
3040 "`#[dataflow]` requires the `dataflow` feature \
3041 (embassy-supervisor feature `dataflow`) — it derives this \
3042 fn's coupling tables from the supervisor verbs it calls",
3043 ));
3044 }
3045 let verbs: VerbTable = syn::parse2(args)?;
3046 let Some(param) = node_param(&f.sig) else {
3047 return Err(syn::Error::new_spanned(
3048 &f.sig.ident,
3049 "`#[dataflow]` needs a `&'static TaskNode` parameter — the verbs \
3050 it derives from are called through it",
3051 ));
3052 };
3053 let cr = quote!(::embassy_supervisor);
3054 let reads_ident = format_ident!("__SV_DATAFLOW_READS_{}", f.sig.ident);
3055 let writes_ident = format_ident!("__SV_DATAFLOW_WRITES_{}", f.sig.ident);
3056
3057 let mut reads: Vec<DerivedEntry> = Vec::new();
3058 let mut writes: Vec<DerivedEntry> = Vec::new();
3059 rewrite_verb_calls(
3060 f.block.to_token_stream(),
3061 ¶m.to_string(),
3062 &verbs,
3063 &mut |call| {
3064 if !cfg!(feature = "liveness")
3065 && matches!(call.verb.as_str(), "beat_put" | "beat_writer")
3066 && call.cfgs.is_empty()
3067 {
3068 return Err(syn::Error::new_spanned(
3069 &call.target,
3070 format!(
3071 "`{}` carries the node's sign of life, which requires \
3072 the `liveness` feature (embassy-supervisor feature \
3073 `liveness`). Without it this would be a heartbeat the \
3074 build silently does not make: use `{}` for the write \
3075 alone, or enable the feature",
3076 call.verb,
3077 call.verb.trim_start_matches("beat_"),
3078 ),
3079 ));
3080 }
3081 let needs = match call.verb.as_str() {
3084 "open" | "lease" if !cfg!(feature = "data-deps") => Some("`data-deps` feature"),
3085 "veto" if !cfg!(feature = "veto") => Some("`veto` feature"),
3086 "retire" if !cfg!(all(feature = "data-deps", feature = "readiness")) => {
3087 Some("`data-deps` and `readiness` features")
3088 }
3089 _ => None,
3090 };
3091 if let Some(needs) = needs
3092 && call.cfgs.is_empty()
3093 {
3094 return Err(syn::Error::new_spanned(
3095 &call.target,
3096 format!(
3097 "`{}` requires the {needs} (embassy-supervisor features of \
3098 the same names), which add the verb to `TaskNode`",
3099 call.verb,
3100 ),
3101 ));
3102 }
3103 record_derived(
3104 if call.write { &mut writes } else { &mut reads },
3105 &call,
3106 call.cfgs.clone(),
3107 );
3108 Ok(None)
3109 },
3110 )?;
3111 let body = rewrite_verb_calls(
3112 f.block.to_token_stream(),
3113 ¶m.to_string(),
3114 &verbs,
3115 &mut |call| {
3116 let (list, table) = if call.write {
3117 (&writes, &writes_ident)
3118 } else {
3119 (&reads, &reads_ident)
3120 };
3121 let k = list
3122 .iter()
3123 .position(|e| e.path == call.path)
3124 .expect("collected in the first pass over this same body");
3125 let prefix = derived_prefix_expr(&list[..k]);
3126 let target = &call.target;
3127 Ok(Some(
3128 quote!( #cr::Sig { entry: &#table[#prefix], target: & #target } ),
3129 ))
3130 },
3131 )?;
3132 f.block = syn::parse2(body)?;
3133
3134 let read_entries: Vec<TokenStream2> =
3135 reads.iter().map(|e| derived_entry_tokens(&cr, e)).collect();
3136 let write_entries: Vec<TokenStream2> = writes
3137 .iter()
3138 .map(|e| derived_entry_tokens(&cr, e))
3139 .collect();
3140 let nr = derived_prefix_expr(&reads);
3141 let nw = derived_prefix_expr(&writes);
3142 Ok(quote! {
3143 #f
3144
3145 #[doc(hidden)]
3146 #[allow(non_upper_case_globals)]
3147 pub static #reads_ident: [#cr::Coupling; #nr] = [#(#read_entries),*];
3148 #[doc(hidden)]
3149 #[allow(non_upper_case_globals)]
3150 pub static #writes_ident: [#cr::Coupling; #nw] = [#(#write_entries),*];
3151 })
3152}
3153
3154#[proc_macro]
3155pub fn supervisor_graph(input: TokenStream) -> TokenStream {
3156 let graph = syn::parse_macro_input!(input as GraphSpec);
3157 expand(graph)
3158 .unwrap_or_else(syn::Error::into_compile_error)
3159 .into()
3160}
3161
3162#[proc_macro]
3163pub fn supervisor_fragment(input: TokenStream) -> TokenStream {
3164 fragment_expand(input.into())
3165 .unwrap_or_else(syn::Error::into_compile_error)
3166 .into()
3167}
3168
3169fn fragment_expand(input: TokenStream2) -> SynResult<TokenStream2> {
3170 struct FragmentSpec {
3171 name: Ident,
3172 items: TokenStream2,
3173 }
3174 impl Parse for FragmentSpec {
3175 fn parse(input: ParseStream) -> SynResult<Self> {
3176 input.parse::<kw::name>()?;
3177 input.parse::<Token![:]>()?;
3178 let name: Ident = input.parse()?;
3179 input.parse::<Token![;]>()?;
3180 let items: TokenStream2 = input.parse()?;
3181 Ok(FragmentSpec { name, items })
3182 }
3183 }
3184 let spec: FragmentSpec = syn::parse2(input)?;
3185 let name = &spec.name;
3186
3187 validate_dollars(spec.items.clone())?;
3188 let items = normalize_fragment_crate(spec.items);
3189
3190 let substituted = substitute_dollar_crate(items.clone(), "e!(__sv_fragment_crate));
3191 let spec_parsed = syn::parse2::<GraphSpec>(substituted)?;
3192 if let Some(ex) = &spec_parsed.default_executor {
3193 return Err(syn::Error::new_spanned(
3194 ex,
3195 "a fragment cannot declare the graph's default executor; declare it at \
3196 the compose site",
3197 ));
3198 }
3199 gate::gate(&spec_parsed)?;
3200
3201 let items = &items;
3202 let dollar = proc_macro2::Punct::new('$', proc_macro2::Spacing::Alone);
3203 let doc = format!(
3204 "A `supervisor_fragment!` relay (generated). Use from a compose site:\n\
3205 `embassy_supervisor::compose_graph! {{ fragments: [{name}], graph: {{ .. }} }}`\n\
3206 Not for direct invocation."
3207 );
3208 Ok(quote! {
3209 #[doc = #doc]
3210 #[macro_export]
3211 macro_rules! #name {
3212 (@emit #dollar cb:path, [#dollar(#dollar rest:tt)*], {#dollar(#dollar acc:tt)*}, {#dollar(#dollar g:tt)*}) => {
3213 #dollar cb! { @next [#dollar(#dollar rest)*],
3214 {#dollar(#dollar acc)* @fragment #name; #items @endfragment;},
3215 {#dollar(#dollar g)*} }
3216 };
3217 }
3218 })
3219}
3220
3221fn validate_dollars(stream: TokenStream2) -> SynResult<()> {
3222 use proc_macro2::TokenTree;
3223 let mut iter = stream.into_iter().peekable();
3224 while let Some(tt) = iter.next() {
3225 match tt {
3226 TokenTree::Group(g) => validate_dollars(g.stream())?,
3227 TokenTree::Punct(p) if p.as_char() == '$' => match iter.peek() {
3228 Some(TokenTree::Ident(i)) if i == "crate" => {}
3229 _ => {
3230 return Err(syn::Error::new(
3231 p.span(),
3232 "only `$crate` is permitted in a fragment — any other `$` \
3233 would be read as a metavariable by the relay macro",
3234 ));
3235 }
3236 },
3237 _ => {}
3238 }
3239 }
3240 Ok(())
3241}
3242
3243#[cfg(test)]
3244mod tests {
3245 use super::*;
3246
3247 fn parse_gated(src: &str) -> SynResult<GraphSpec> {
3248 let spec = syn::parse_str::<GraphSpec>(src)?;
3249 gate::gate(&spec)?;
3250 Ok(spec)
3251 }
3252
3253 #[test]
3254 fn ready_marker_requires_feature() {
3255 let res = parse_gated(
3256 "node NET = Terminate, deps: [];\n\
3257 node HTTP = Terminate, deps: [NET ready];",
3258 );
3259 if cfg!(feature = "readiness") {
3260 assert!(res.is_ok(), "marker accepted with the feature");
3261 } else {
3262 match res {
3263 Ok(_) => panic!("marker accepted without the feature"),
3264 Err(err) => assert!(
3265 err.to_string().contains("requires the `readiness` feature"),
3266 "unexpected error: {err}"
3267 ),
3268 }
3269 }
3270 }
3271
3272 #[test]
3273 fn beat_timeout_requires_feature() {
3274 let res = parse_gated("node A = Terminate, deps: [], beat_timeout: 100;");
3275 if cfg!(feature = "liveness-monitor") {
3276 assert!(res.is_ok(), "clause accepted with the feature");
3277 } else {
3278 match res {
3279 Ok(_) => panic!("clause accepted without the feature"),
3280 Err(err) => assert!(
3281 err.to_string()
3282 .contains("requires the `liveness-monitor` feature"),
3283 "unexpected error: {err}"
3284 ),
3285 }
3286 }
3287 }
3288
3289 #[test]
3290 fn cfg_gated_clauses_defer_to_rustc() {
3291 for (src, feature_on, builder) in [
3292 (
3293 "node A = Terminate, deps: [], \
3294 #[cfg(feature = \"x\")] beat_timeout: 100, \
3295 #[cfg(feature = \"x\")] beat_window: 3;",
3296 cfg!(feature = "liveness-monitor"),
3297 "with_beat_timeout",
3298 ),
3299 (
3300 "node A = Terminate, deps: [], task: w, \
3301 writes: [crate::S observed beat via it.get()], \
3302 #[cfg(feature = \"x\")] beat_timeout: 100, \
3303 #[cfg(feature = \"x\")] ready_on_write;",
3304 cfg!(all(
3305 feature = "liveness-monitor",
3306 feature = "coupling-observe",
3307 feature = "readiness"
3308 )),
3309 "with_ready_on_write",
3310 ),
3311 (
3312 "node A = Terminate, deps: [], task: w, #[cfg(feature = \"x\")] discover;",
3313 cfg!(feature = "dataflow"),
3314 "__SV_DATAFLOW_",
3315 ),
3316 ] {
3317 let spec = parse_gated(src).expect("a gated clause passes the gate");
3318 let out = expand(spec).expect("expansion succeeds").to_string();
3319 if feature_on {
3320 assert!(out.contains(builder), "{src}\n{out}");
3321 assert!(!out.contains("compile_error"), "{src}\n{out}");
3322 } else {
3323 assert!(out.contains("compile_error"), "{src}\n{out}");
3324 }
3325 }
3326 }
3327
3328 #[test]
3329 fn cfg_gated_featureless_clauses_emit_both_ways() {
3330 let spec = parse_gated(
3331 "node A = Terminate, deps: [], \
3332 #[cfg(feature = \"x\")] slot_timeout: 100, \
3333 #[cfg(feature = \"x\")] ack_timeout: 200, \
3334 #[cfg(feature = \"x\")] disabled;\n\
3335 node B = Terminate, deps: [], slot_timeout: 300;",
3336 )
3337 .expect("gated featureless clauses pass the gate");
3338 let out = expand(spec).expect("expansion succeeds").to_string();
3339 assert!(out.contains("with_slot_timeout"), "{out}");
3340 assert!(out.contains("with_ack_timeout"), "{out}");
3341 assert!(out.contains("# [cfg (feature = \"x\")] { true }"), "{out}");
3343 assert!(!out.contains("compile_error"), "{out}");
3344
3345 if cfg!(feature = "pool") {
3347 let spec = parse_gated(
3348 "pool P = [Terminate], deps: [], task: w, \
3349 policy: embassy_supervisor::DeferredShrink::new(d()), \
3350 min: 1, max: 1, #[cfg(feature = \"x\")] slot_timeout: 100;",
3351 )
3352 .expect("gated pool timeout passes the gate");
3353 let out = expand(spec).expect("expansion succeeds").to_string();
3354 assert!(out.contains("with_slot_timeout"), "{out}");
3355 assert!(!out.contains("compile_error"), "{out}");
3356 }
3357 }
3358
3359 #[test]
3360 fn beat_verb_requires_feature() {
3361 let expand = |verb: &str| {
3362 dataflow_expand(
3363 quote!(),
3364 format!("fn f(node: &'static TaskNode) {{ node.{verb}(&crate::OUT, 1); }}")
3365 .parse()
3366 .unwrap(),
3367 )
3368 };
3369 for verb in ["beat_put", "beat_writer"] {
3370 let res = expand(verb);
3371 if !cfg!(feature = "dataflow") {
3372 continue;
3373 }
3374 if cfg!(feature = "liveness") {
3375 assert!(res.is_ok(), "`{verb}` accepted with the feature");
3376 } else {
3377 match res {
3378 Ok(_) => panic!("`{verb}` accepted without the feature"),
3379 Err(err) => {
3380 let msg = err.to_string();
3381 assert!(
3382 msg.contains("requires the `liveness` feature"),
3383 "`{verb}`: {msg}"
3384 );
3385 assert!(
3386 msg.contains(verb.trim_start_matches("beat_")),
3387 "the message names the plain verb to fall back to: {msg}"
3388 );
3389 }
3390 }
3391 }
3392 }
3393 if cfg!(feature = "dataflow") {
3394 assert!(expand("put").is_ok(), "`put` needs no liveness");
3395 let gated = dataflow_expand(
3396 quote!(),
3397 "fn f(node: &'static TaskNode) { \
3398 #[cfg(feature = \"x\")] node.beat_put(&crate::OUT, 1); }"
3399 .parse()
3400 .unwrap(),
3401 );
3402 assert!(gated.is_ok(), "a cfg-gated beat verb defers to rustc");
3403 }
3404 }
3405
3406 #[test]
3407 fn coupling_clauses_require_feature() {
3408 let res =
3409 parse_gated("node A = Terminate, deps: [], reads: [crate::SIG], writes: [crate::OUT];");
3410 if cfg!(feature = "coupling") {
3411 assert!(res.is_ok(), "clauses accepted with the feature");
3412 } else {
3413 match res {
3414 Ok(_) => panic!("clauses accepted without the feature"),
3415 Err(err) => assert!(
3416 err.to_string().contains("requires the `coupling` feature"),
3417 "unexpected error: {err}"
3418 ),
3419 }
3420 }
3421 }
3422
3423 #[test]
3424 fn bound_marker_requires_feature() {
3425 let res = parse_gated(
3426 "node A = Terminate, deps: [];\nnode B = Terminate, deps: [A ready bound];",
3427 );
3428 if cfg!(all(feature = "bound-deps", feature = "readiness")) {
3429 assert!(res.is_ok(), "marker accepted with the features");
3430 } else if !cfg!(feature = "bound-deps") {
3431 match res {
3432 Ok(_) => panic!("marker accepted without the feature"),
3433 Err(err) => assert!(
3434 err.to_string()
3435 .contains("requires the `bound-deps` feature"),
3436 "unexpected error: {err}"
3437 ),
3438 }
3439 }
3440 }
3441
3442 fn gate_rejects(src: &str, feature: bool, needle: &str) {
3443 let res = parse_gated(src);
3444 if feature {
3445 assert!(res.is_ok(), "rejected with the feature: {:?}", res.err());
3446 } else {
3447 match res {
3448 Ok(_) => panic!("accepted without the feature"),
3449 Err(err) => assert!(err.to_string().contains(needle), "unexpected error: {err}"),
3450 }
3451 }
3452 }
3453
3454 #[test]
3455 fn observed_marker_requires_feature() {
3456 gate_rejects(
3457 "node A = Terminate, deps: [], task: f, writes: [crate::S observed via it.get()];",
3458 cfg!(all(feature = "coupling-observe", feature = "coupling")),
3459 "requires the `coupling-observe` feature",
3460 );
3461 }
3462
3463 #[test]
3464 fn discover_clause_requires_feature() {
3465 gate_rejects(
3466 "node A = Terminate, deps: [], task: f, discover;",
3467 cfg!(feature = "dataflow"),
3468 "requires the `dataflow` feature",
3469 );
3470 }
3471
3472 #[test]
3473 fn observe_default_requires_feature() {
3474 gate_rejects(
3475 "observe writes: it.get();\nnode A = Terminate, deps: [], task: f;",
3476 cfg!(feature = "coupling-observe"),
3477 "requires the `coupling-observe` feature",
3478 );
3479 }
3480
3481 #[test]
3482 fn local_resource_requires_feature() {
3483 gate_rejects(
3484 "node A = Terminate, deps: [], task: f, resources: [R: local Thing];",
3485 cfg!(feature = "local-resources"),
3486 "`local-resources` feature",
3487 );
3488 }
3489
3490 #[test]
3491 fn state_clause_requires_feature() {
3492 gate_rejects(
3493 "node A = Terminate, deps: [], task: f, state: Buf = Buf::new();",
3494 cfg!(feature = "heap-state"),
3495 "requires the `heap-state` feature",
3496 );
3497 }
3498
3499 #[test]
3500 fn zeroed_state_clause_requires_feature() {
3501 gate_rejects(
3502 "node A = Terminate, deps: [], task: f, state: zeroed Buf;",
3503 cfg!(feature = "heap-state"),
3504 "requires the `heap-state` feature",
3505 );
3506 }
3507
3508 #[test]
3509 #[cfg(feature = "heap-state")]
3510 fn zeroed_state_parses_as_marker() {
3511 let spec = parse_gated("node A = Terminate, deps: [], task: f, state: zeroed Buf;")
3512 .expect("marker form");
3513 let Item::Node(n) = &spec.items[0] else {
3514 panic!("node")
3515 };
3516 let Some((_, ty, StateInit::Zeroed(_))) = &n.state else {
3517 panic!("zeroed state")
3518 };
3519 assert_eq!(quote!(#ty).to_string(), "Buf");
3520 let spec = parse_gated("node A = Terminate, deps: [], task: f, state: zeroed = Z;")
3521 .expect("type named zeroed");
3522 let Item::Node(n) = &spec.items[0] else {
3523 panic!("node")
3524 };
3525 let Some((_, ty, StateInit::Expr(_))) = &n.state else {
3526 panic!("init state")
3527 };
3528 assert_eq!(quote!(#ty).to_string(), "zeroed");
3529 }
3530
3531 #[test]
3532 fn deps_clause_is_optional() {
3533 assert!(parse_gated("node A = Terminate, task: f;").is_ok());
3534 assert!(parse_gated("node A = Terminate, task: f, deps: [];").is_ok());
3535 match parse_gated("node A = Terminate, deps: [], task: f, deps: [];") {
3536 Ok(_) => panic!("duplicate deps accepted"),
3537 Err(e) => assert!(e.to_string().contains("duplicate `deps:`"), "{e}"),
3538 }
3539 }
3540
3541 #[test]
3542 fn provides_must_name_a_declared_slot() {
3543 let spec = syn::parse_str::<GraphSpec>(
3544 "node P = Terminate, deps: [], task: f, provides: [NOPE];\n\
3545 node C = Terminate, deps: [P], task: g, resources: [SLOT: shared u32];",
3546 )
3547 .unwrap();
3548 match expand(spec) {
3549 Ok(_) => panic!("unknown slot accepted"),
3550 Err(e) => assert!(
3551 e.to_string().contains("no `resources:` entry"),
3552 "unexpected error: {e}"
3553 ),
3554 }
3555 }
3556
3557 #[test]
3558 fn beat_window_requires_feature() {
3559 gate_rejects(
3560 "node A = Terminate, deps: [], task: f, beat_timeout: 10, beat_window: 3;",
3561 cfg!(feature = "liveness-monitor"),
3562 "requires the `liveness-monitor` feature",
3563 );
3564 }
3565
3566 #[test]
3567 fn ready_on_write_requires_both_features() {
3568 let src = "node A = Terminate, deps: [], task: f, beat_timeout: 10, \
3569 ready_on_write, writes: [crate::S observed beat via it.get()];";
3570 let all = cfg!(all(
3571 feature = "coupling-observe",
3572 feature = "readiness",
3573 feature = "coupling",
3574 feature = "liveness-monitor"
3575 ));
3576 let needle = if cfg!(feature = "coupling") && !cfg!(feature = "coupling-observe") {
3577 "requires the `coupling-observe` feature"
3578 } else if !cfg!(feature = "coupling") {
3579 "requires the `coupling` feature"
3580 } else if !cfg!(feature = "liveness-monitor") {
3581 "requires the `liveness-monitor` feature"
3582 } else {
3583 "requires the `readiness` feature"
3584 };
3585 gate_rejects(src, all, needle);
3586 }
3587
3588 #[cfg(feature = "dataflow")]
3589 #[test]
3590 fn dataflow_indices_bake_after_full_collection() {
3591 let item = quote::quote! {
3592 async fn f(node: &'static TaskNode) {
3593 #[cfg(feature = "x")]
3594 node.put(&crate::A, 1u8);
3595 node.put(&crate::B, 2u8);
3596 node.put(&crate::A, 3u8);
3597 }
3598 };
3599 let out = dataflow_expand(TokenStream2::new(), item)
3600 .expect("expands")
3601 .to_string();
3602 assert_eq!(
3605 out.matches("feature = \"x\"").count(),
3606 1,
3607 "stale predicate survives: {out}"
3608 );
3609 }
3610
3611 #[test]
3612 fn divisible_requires_feature() {
3613 gate_rejects(
3614 "node A = Terminate, deps: [], task: f, resources: [R: divisible];",
3615 cfg!(feature = "budget"),
3616 "`budget` feature",
3617 );
3618 }
3619
3620 #[cfg(all(feature = "budget", feature = "pool"))]
3621 #[test]
3622 fn divisible_emits_one_budget_sized_by_its_holders() {
3623 let spec = syn::parse_str::<GraphSpec>(
3624 "node A = Terminate, deps: [], task: f, resources: [P: divisible];\n\
3625 pool W = [Terminate, Terminate, Terminate], deps: [], task: g, \
3626 resources: [P: divisible], policy: DeferredShrink::new(d), min: 1, max: 3;\n\
3627 node B = Terminate, deps: [], task: f, resources: [P: divisible];",
3628 )
3629 .unwrap();
3630 let out = expand(spec).expect("expansion succeeds").to_string();
3631 assert!(out.contains("pub static P : "), "{out}");
3632 assert!(
3633 out.contains("Budget < 5usize >"),
3634 "one slot for A, three for W, one for B: {out}"
3635 );
3636 assert!(out.contains("P . claimant (0u8)"), "A takes slot 0: {out}");
3637 assert!(
3638 out.contains("P . claimant ((1u8 as usize + I) as u8)"),
3639 "W's members take 1..=3: {out}"
3640 );
3641 assert!(out.contains("P . claimant (4u8)"), "B takes slot 4: {out}");
3642 assert!(
3643 out.contains("__SV_CLAIMS_W_2"),
3644 "one claims table per member: {out}"
3645 );
3646 assert!(
3647 out.contains("(& P , 3u8)"),
3648 "member 2 releases slot 3: {out}"
3649 );
3650 assert_eq!(out.matches(". with_claims (").count(), 5, "{out}");
3651 assert!(!out.contains(". restore ("), "nothing to restore: {out}");
3652 }
3653
3654 #[cfg(feature = "budget")]
3655 #[test]
3656 fn divisible_slots_are_counted_syntactically() {
3657 let spec = syn::parse_str::<GraphSpec>(
3658 "node A = Terminate, deps: [], task: f, resources: [#[cfg(any())] P: divisible];\n\
3659 node B = Terminate, deps: [], task: f, resources: [P: divisible];",
3660 )
3661 .unwrap();
3662 let out = expand(spec).expect("expansion succeeds").to_string();
3663 assert!(
3664 out.contains("Budget < 2usize >"),
3665 "a cfg'd-out holder still takes its slot: {out}"
3666 );
3667 }
3668
3669 #[cfg(feature = "budget")]
3670 #[test]
3671 fn a_budget_may_be_provided_by_a_node() {
3672 let spec = syn::parse_str::<GraphSpec>(
3673 "node ALLOC = Terminate, deps: [], task: f, provides: [P];\n\
3674 node A = Terminate, deps: [ALLOC], task: g, resources: [P: divisible];",
3675 )
3676 .unwrap();
3677 let out = expand(spec).expect("expansion succeeds").to_string();
3678 assert!(out.contains("__SV_PROVIDES_ALLOC"), "{out}");
3679 }
3680
3681 #[cfg(feature = "budget")]
3682 #[test]
3683 fn a_budget_name_cannot_double_as_a_take_kind_slot() {
3684 let spec = syn::parse_str::<GraphSpec>(
3685 "node A = Terminate, deps: [], task: f, resources: [P: divisible];\n\
3686 node B = Terminate, deps: [], task: f, resources: [P: u32];",
3687 )
3688 .unwrap();
3689 match expand(spec) {
3690 Ok(_) => panic!("accepted"),
3691 Err(e) => assert!(e.to_string().contains("duplicate resource name"), "{e}"),
3692 }
3693 }
3694
3695 #[test]
3696 fn veto_requires_feature() {
3697 gate_rejects(
3698 "node A = Terminate, deps: [], task: f, writes: [crate::TRIP veto];",
3699 cfg!(feature = "veto"),
3700 "`veto` feature",
3701 );
3702 }
3703
3704 #[cfg(all(feature = "veto", feature = "pool"))]
3705 #[test]
3706 fn veto_writers_are_numbered_in_item_order_across_nodes_and_pools() {
3707 let spec = syn::parse_str::<GraphSpec>(
3708 "node A = Terminate, deps: [], task: f, writes: [crate::TRIP veto];\n\
3709 pool P = [Terminate, Terminate], deps: [], task: g, writes: [crate::TRIP veto], \
3710 policy: DeferredShrink::new(d), min: 1, max: 2;\n\
3711 node B = Terminate, deps: [], task: f, writes: [crate::TRIP veto observed beat];\n\
3712 node R = Terminate, deps: [], task: h, reads: [crate::TRIP];",
3713 )
3714 .unwrap();
3715 let out = expand(spec).expect("expansion succeeds").to_string();
3716 assert!(out.contains(". veto (0u8)"), "A: {out}");
3717 assert!(
3718 out.contains("__SV_WRITES_P_0") && out.contains("__SV_WRITES_P_1"),
3719 "a table per member: {out}"
3720 );
3721 assert!(
3722 out.contains(". veto (1u8)") && out.contains(". veto (2u8)"),
3723 "P's members: {out}"
3724 );
3725 assert!(
3726 out.contains(". beat () . veto (3u8)"),
3727 "B, beside its other markers: {out}"
3728 );
3729 assert!(
3730 out.contains("__sv_check_veto (& crate :: TRIP , 4usize)"),
3731 "one check per gate: {out}"
3732 );
3733 assert_eq!(out.matches("__sv_check_veto").count(), 1, "{out}");
3734 }
3735
3736 #[cfg(feature = "veto")]
3737 #[test]
3738 fn a_pool_without_veto_keeps_one_writes_table() {
3739 let spec = syn::parse_str::<GraphSpec>(
3740 "pool P = [Terminate, Terminate], deps: [], task: g, writes: [crate::OUT], \
3741 policy: DeferredShrink::new(d), min: 1, max: 2;",
3742 )
3743 .unwrap();
3744 let out = expand(spec).expect("expansion succeeds").to_string();
3745 assert!(out.contains("__SV_WRITES_P :"), "{out}");
3746 assert!(!out.contains("__SV_WRITES_P_0"), "{out}");
3747 }
3748
3749 #[cfg(feature = "veto")]
3750 #[test]
3751 fn more_than_32_veto_writers_are_rejected() {
3752 let mut src = String::new();
3753 for i in 0..33 {
3754 src.push_str(&format!(
3755 "node N{i} = Terminate, deps: [], task: f, writes: [crate::TRIP veto];\n"
3756 ));
3757 }
3758 let spec = syn::parse_str::<GraphSpec>(&src).unwrap();
3759 match expand(spec) {
3760 Ok(_) => panic!("accepted"),
3761 Err(e) => assert!(e.to_string().contains("more than 32"), "{e}"),
3762 }
3763 }
3764
3765 #[cfg(feature = "budget")]
3766 #[test]
3767 fn pool_size_cannot_share_a_claimant_slot() {
3768 let spec = syn::parse_str::<GraphSpec>(
3769 "node A = Terminate, deps: [], task: f, pool_size: 2, resources: [P: divisible];",
3770 )
3771 .unwrap();
3772 match expand(spec) {
3773 Ok(_) => panic!("accepted"),
3774 Err(e) => assert!(e.to_string().contains("lend/consume/divisible"), "{e}"),
3775 }
3776 }
3777
3778 #[cfg(feature = "veto")]
3779 #[test]
3780 fn a_veto_gate_spelled_two_ways_is_rejected() {
3781 let spec = syn::parse_str::<GraphSpec>(
3782 "node A = Terminate, deps: [], task: f, writes: [crate::TRIP veto];\n\
3783 node B = Terminate, deps: [], task: f, writes: [TRIP veto];",
3784 )
3785 .unwrap();
3786 match expand(spec) {
3787 Ok(_) => panic!("accepted"),
3788 Err(e) => {
3789 let e = e.to_string();
3790 assert!(e.contains("`crate::TRIP` and `TRIP`"), "{e}");
3791 assert!(e.contains("numbered per spelling"), "{e}");
3792 }
3793 }
3794 let spec = syn::parse_str::<GraphSpec>(
3796 "node A = Terminate, deps: [], task: f, writes: [crate::a::TRIP veto];\n\
3797 node B = Terminate, deps: [], task: f, writes: [crate::b::TRIP veto];",
3798 )
3799 .unwrap();
3800 match expand(spec) {
3801 Ok(_) => panic!("accepted"),
3802 Err(e) => assert!(e.to_string().contains("`use .. as`"), "{e}"),
3803 }
3804 let spec = syn::parse_str::<GraphSpec>(
3806 "node A = Terminate, deps: [], task: f, writes: [crate::TRIP[0] veto];\n\
3807 node B = Terminate, deps: [], task: f, writes: [crate::TRIP[1] veto];",
3808 )
3809 .unwrap();
3810 let out = expand(spec).expect("two elements, two gates").to_string();
3811 assert_eq!(out.matches("__sv_check_veto").count(), 2, "{out}");
3812 assert!(
3813 out.contains(". veto (0u8)") && !out.contains(". veto (1u8)"),
3814 "{out}"
3815 );
3816 }
3817
3818 #[cfg(feature = "veto")]
3819 #[test]
3820 fn the_veto_check_is_gated_like_its_writers() {
3821 let spec = syn::parse_str::<GraphSpec>(
3822 "#[cfg(feature = \"x\")] node A = Terminate, deps: [], task: f, \
3823 writes: [crate::TRIP veto];\n\
3824 node B = Terminate, deps: [], task: f, \
3825 writes: [#[cfg(feature = \"y\")] crate::TRIP veto];",
3826 )
3827 .unwrap();
3828 let out = expand(spec).expect("expansion succeeds").to_string();
3829 assert!(
3830 out.contains("# [cfg (any (feature = \"x\" , feature = \"y\"))] const _ : () = "),
3831 "the check carries the union of its writers' cfgs: {out}"
3832 );
3833 let spec = syn::parse_str::<GraphSpec>(
3834 "#[cfg(feature = \"x\")] node A = Terminate, deps: [], task: f, \
3835 writes: [crate::TRIP veto];\n\
3836 node B = Terminate, deps: [], task: f, writes: [crate::TRIP veto];",
3837 )
3838 .unwrap();
3839 let out = expand(spec).expect("expansion succeeds").to_string();
3840 assert!(
3841 out.contains("const _ : () = ") && !out.contains("))] const _ : () = "),
3842 "one unconditional writer keeps the check bare: {out}"
3843 );
3844 }
3845
3846 #[cfg(feature = "dataflow")]
3847 #[test]
3848 fn feature_verbs_name_their_feature_in_dataflow_bodies() {
3849 for (body, feature, needle) in [
3850 (
3851 quote::quote! { let _ = node.open(&crate::EST).await; },
3852 cfg!(feature = "data-deps"),
3853 "`data-deps` feature",
3854 ),
3855 (
3856 quote::quote! { let _ = node.lease(&crate::LNK); },
3857 cfg!(feature = "data-deps"),
3858 "`data-deps` feature",
3859 ),
3860 (
3861 quote::quote! { node.veto(&crate::TRIP); },
3862 cfg!(feature = "veto"),
3863 "`veto` feature",
3864 ),
3865 (
3866 quote::quote! { node.retire(&crate::EST, d).await; },
3867 cfg!(all(feature = "data-deps", feature = "readiness")),
3868 "`data-deps` and `readiness` features",
3869 ),
3870 ] {
3871 let item = quote::quote! {
3872 async fn f(node: &'static TaskNode) { #body }
3873 };
3874 let res = dataflow_expand(TokenStream2::new(), item);
3875 if feature {
3876 assert!(res.is_ok(), "rejected with the feature: {:?}", res.err());
3877 } else {
3878 match res {
3879 Ok(_) => panic!("accepted without the feature"),
3880 Err(e) => assert!(e.to_string().contains(needle), "{e}"),
3881 }
3882 }
3883 }
3884 }
3885
3886 #[test]
3887 fn a_serialized_slot_holds_its_holders_to_one_executor() {
3888 let ok = syn::parse_str::<GraphSpec>(
3889 "executor HIGH;\n\
3890 node A = Terminate, deps: [], executor: HIGH, task: f, \
3891 resources: [BUS: shared serialized Bus];\n\
3892 node B = Terminate, deps: [], executor: HIGH, task: f, \
3893 resources: [BUS: shared serialized Bus];",
3894 )
3895 .unwrap();
3896 assert!(expand(ok).is_ok(), "one tier: accepted");
3897 let root = syn::parse_str::<GraphSpec>(
3898 "node A = Terminate, deps: [], task: f, resources: [BUS: shared serialized Bus];\n\
3899 node B = Terminate, deps: [], task: f, resources: [BUS: shared serialized Bus];",
3900 )
3901 .unwrap();
3902 assert!(
3903 expand(root).is_ok(),
3904 "both on the supervisor's executor: accepted"
3905 );
3906 for (src, a, b) in [
3907 (
3908 "executor HIGH;\n\
3909 node A = Terminate, deps: [], task: f, resources: [BUS: shared serialized Bus];\n\
3910 node B = Terminate, deps: [], executor: HIGH, task: f, \
3911 resources: [BUS: shared serialized Bus];",
3912 "`A` runs on the supervisor's executor",
3913 "`B` on `HIGH`",
3914 ),
3915 (
3916 "executor HIGH; executor LOW;\n\
3917 node A = Terminate, deps: [], executor: HIGH, task: f, \
3918 resources: [BUS: shared serialized Bus];\n\
3919 node B = Terminate, deps: [], executor: LOW, task: f, \
3920 resources: [BUS: shared serialized Bus];",
3921 "`A` runs on `HIGH`",
3922 "`B` on `LOW`",
3923 ),
3924 ] {
3925 let spec = syn::parse_str::<GraphSpec>(src).unwrap();
3926 match expand(spec) {
3927 Ok(_) => panic!("accepted across tiers: {src}"),
3928 Err(e) => {
3929 let msg = e.to_string();
3930 assert!(msg.contains("priority ceiling"), "{msg}");
3931 assert!(msg.contains(a) && msg.contains(b), "{msg}");
3932 }
3933 }
3934 }
3935 let plain = syn::parse_str::<GraphSpec>(
3936 "executor HIGH;\n\
3937 node A = Terminate, deps: [], task: f, resources: [BUS: shared Bus];\n\
3938 node B = Terminate, deps: [], executor: HIGH, task: f, resources: [BUS: shared Bus];",
3939 )
3940 .unwrap();
3941 assert!(
3942 expand(plain).is_ok(),
3943 "without the marker a shared slot may span tiers"
3944 );
3945 }
3946}