Skip to main content

embassy_supervisor_macros/
lib.rs

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/// The `veto` contributor slots of one item's `writes:` list: the base slot
154/// per entry (aligned with the list, `None` where the marker is absent) and
155/// the member offset a pool table adds to it.
156#[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        // Entries may be individually `#[cfg]`-gated, so the length is summed
178        // the same way the dep overlays do it.
179        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                    // `it` names the signal inside the accessor, so one
192                    // graph-level default serves every entry in the direction.
193                    // With neither a `via` nor a default, the signal answers
194                    // for itself through the `Observable` facade.
195                    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    // Adoptions may be individually cfg-gated (a feature-gated accessor is
225    // ordinary), so the array length is summed cfg-aware like the dep overlays.
226    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        // Each term carries its adoption's `#[cfg]`, as statements in a
254        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
414/// Graph-wide facts the per-item emitters consult: resource slots and the
415/// contributor slots of `veto` writes.
416struct ResourcePlan {
417    /// Per resource name: the first declaring entry's `#[cfg]`s, and whether
418    /// the name is a pool's per-member slot array (which `provides:` may not
419    /// name).
420    cfgs: HashMap<String, (Vec<Attribute>, bool)>,
421    /// `(owner, resource)` -> the owner's base slot in that `divisible` budget.
422    claim_bases: HashMap<(String, String), u8>,
423    /// `(owner, signal display)` -> the owner's base contributor slot in that
424    /// `VetoGate`.
425    veto_bases: HashMap<(String, String), u8>,
426}
427
428impl ResourcePlan {
429    /// `owner`'s base contributor slot per entry of its `writes:`, aligned with
430    /// the list (`None` where the entry carries no `veto`).
431    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    /// `owner`'s base slot per entry of its `resources:`, aligned with the list
443    /// (`None` for every kind but `divisible`).
444    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
456/// The claims table for one holder: `(&BUDGET, slot)` per `divisible` entry,
457/// wired with `.with_claims(..)` so a stop releases each slot. `offset` is the
458/// pool member index (0 for a node), added to the entry's base slot.
459fn 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
545/// `" (from fragment \`X\`)"` when the item was forwarded through a
546/// `supervisor_fragment!` relay, else empty — error-message attribution.
547fn fragment_suffix(fragment: &Option<String>) -> String {
548    match fragment {
549        Some(f) => format!(" (from fragment `{f}`)"),
550        None => String::new(),
551    }
552}
553
554/// The `[&'static TaskNode; n]` element and length tokens for the deps
555/// carrying one marker, cfg-aware like `gate_tokens`. A dep naming a pool
556/// resolves to that pool's floor member (`&POOL[0]`), matching how `deps: [POOL]`
557/// resolves for spawn ordering.
558fn 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
582/// Extract the policy *type* from a `Type::new(..)` constructor expression. Only used
583/// on the derive path (no explicit `policy: <Ty> = ..` annotation); the type is the
584/// call's path minus its last segment (`DeferredShrink::new` -> `DeferredShrink`).
585fn 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
606/// One emitted node slot, in final index order.
607struct Slot {
608    /// Presence predicate (`None` = unconditional), gates the node slot (`GRAPH.nodes`) entry.
609    cfg_pred: Option<TokenStream2>,
610    /// `&NODE` or `&POOL[j]`.
611    reference: TokenStream2,
612    /// Raw deps, resolved to indices in the second pass.
613    deps: Vec<Dep>,
614    /// The `supervisor_fragment!` the owning item came from, for error
615    /// attribution when a dep fails to resolve across the relay.
616    fragment: Option<String>,
617}
618
619/// The `Option<fn(..)>` spawn expression for a node. `None` (no `spawn:`) is a
620/// parked node the app spawns itself. A path or partial call is a task fn taking
621/// `&NODE` first (plus any given args); the macro wraps it as
622/// `|s| { s.spawn(<task>(&NODE, ..)?); Ok(()) }`. Anything else (a closure, or a
623/// ready spawn fn) is emitted verbatim. Every form is cast to `spawn_fn` so it
624/// coerces cleanly inside `Option::Some(..)`.
625#[allow(clippy::too_many_arguments)]
626fn node_spawn(
627    ident: &Ident,
628    spawn: &Option<Expr>,
629    executor: &Option<Ident>,
630    resources: &[ResourceDecl],
631    // `state:`: fallibly box the init value in the glue, BEFORE the resource
632    // takes (a failed alloc strands nothing) — `SpawnError::Busy`, retryable.
633    state: Option<(&syn::Type, &StateInit)>,
634    spawn_fn: &TokenStream2,
635    cr: &TokenStream2,
636    helpers: &HelperIdents,
637) -> SynResult<TokenStream2> {
638    // `resources:` glue prelude: every entry is PROBED here — the fail-closed
639    // check that turns "unprovided" into `SpawnError::Busy` out of
640    // `Supervisor::start` — and read by the shell itself at first poll
641    // (`take()` for lend/consume, the non-destructive `get()` for `shared`): a
642    // value moved through the task-fn call would be dropped, unrecoverable,
643    // when the claim fails (`Busy` while the previous instance's storage is
644    // still releasing), and even a `Copy` handle passed as an argument sits in
645    // the task arena for the whole run beside the worker's own copy
646    // (rust-lang/rust#62958). Nothing crosses the call.
647    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        // `executor:` needs the macro to perform the spawn, so it composes only
667        // with the path / partial-call `spawn:` forms below.
668        (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        // A path or partial call: wrap the task fn as spawn glue.
676        // With `executor:` the glue spawns through the named `SpawnerSlot`; an
677        // unfilled slot fails with `SpawnError::Busy`.
678        // `SendSpawner::spawn` requires `Send` on the arguments, not the future,
679        // so any `task:` worker can route through any executor.
680        (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, &quote!(&#ident), &quote!(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, &quote!(&#ident), &quote!(__sp));
698                    quote!(::core::option::Option::Some(
699                        (|_s| {
700                            // The supervisor awaits this slot's `ready()` before
701                            // invoking the glue (the node carries `.with_executor(&EX)`
702                            // and the bring-up bounds the wait), so `get()` is already
703                            // filled; `ok_or` is the belt-and-braces unfilled guard.
704                            // Resources are taken AFTER the spawner guard, so an
705                            // unfilled executor never consumes (and strands) them.
706                            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        // Anything else (a closure, or a ready spawn fn) is emitted verbatim.
726        // NOTE: with the `trace` feature such a node is not auto-mapped — the
727        // closure owns the SpawnToken; call `adopt`/`set_task_id` in it yourself.
728        (Some(e), None) => quote!(::core::option::Option::Some((#e) as #spawn_fn)),
729    })
730}
731
732/// `let __state = …?;` for a `state:` clause: the init form boxes the value,
733/// the `zeroed` form allocates zero-filled memory with no value built first.
734fn 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            // Only the call carries the marker's span, so a missing `Zeroable`
746            // impl points at `zeroed`; the binding keeps the glue's hygiene.
747            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
755/// The spawn statement(s) for the generated glue. Plain `s.spawn(<call>?)`
756/// normally; with the `trace` feature the `SpawnToken` is bound first so its task
757/// id can be captured into the node (`set_task_id`) — the id→node mapping the
758/// supervisor's `trace` recorders resolve against (in embassy-executor 0.10 the
759/// task-fn call returns `Result<SpawnToken, SpawnError>` and `Spawner::spawn`
760/// itself is infallible, so the token is available between the two).
761///
762/// Three shapes, resolved at expansion by the macro crate's own features:
763/// * `trace` on → bind the token and `adopt` it (`set_task_id` + name stamp under
764///   `metadata-names`).
765/// * `trace` off but `metadata-names` on → bind the token and `stamp_name` only:
766///   the node name reaches the task Metadata (for rtos-trace/SystemView) with no id
767///   capture and no dependency on the `_embassy_trace_*` hooks.
768/// * neither → plain infallible spawn.
769fn spawn_stmts(call: &TokenStream2, node_ref: &TokenStream2, sp: &TokenStream2) -> TokenStream2 {
770    if cfg!(feature = "trace") {
771        // `adopt` = set_task_id + (under metadata-names) Metadata name stamp.
772        quote! {
773            let __token = #call?;
774            (#node_ref).adopt(&__token);
775            #sp.spawn(__token);
776        }
777    } else if cfg!(feature = "metadata-names") {
778        // Name-only path: stamp the node name into the task Metadata, nothing else.
779        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/// Emit the `#[embassy_executor::task]` shell for a `task:` clause: a concrete,
790/// non-generic task fn that takes only the node and awaits the user's worker with
791/// the node injected first. This is how a **generic** worker becomes spawnable —
792/// embassy forbids generic tasks (one static `TaskPool` per concrete future type),
793/// so a monomorphized shell is stamped per declaration. Worker args are evaluated
794/// inside the shell — at the task's first poll, on the node's own executor — so
795/// the DSL never needs the arg types and a cross-core node builds its resources on
796/// the core that runs them.
797///
798/// Returns the shell item and a path `Expr` naming it, which feeds the ordinary
799/// `spawn:` path-form glue (executor routing and trace `adopt` compose unchanged).
800// One argument per independent codegen input; a bundling struct would only
801// rename the coupling.
802#[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    // Aligned with `resources`: the holder's slot in each `divisible` entry's
810    // budget (a pool shell gets the member's `Claimant` as a parameter instead).
811    claims: &[Option<u8>],
812    exit: Option<&syn::Type>,
813    // `state: Type = ..`: the shell owns the glue-boxed state across the worker
814    // call (worker sees `&mut Type`) and DROPS it first thing after the worker
815    // returns — reclaimed before restores/exit-provide/mark_exited.
816    state: Option<(&syn::Type, &StateInit)>,
817    // Pool shells restore lend entries to a slot REFERENCE parameter (the
818    // member's own array element, passed by the wrapper) instead of a slot
819    // named statically — restore-to-same-index by construction.
820    pool_member: bool,
821    // `cancel`: drive the worker under `run_cancellable` and DON'T lead its
822    // arguments with the node — the worker is a plain future that never returns
823    // on its own, so the shell owns the shutdown race on its behalf.
824    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    // `resources:` handling: every entry is read BY THE SHELL at first poll —
837    // `take()` for lend/consume, the non-destructive `get()` for `shared` —
838    // never through the task-fn call, where a `Busy` storage claim would drop
839    // an owned value unrecoverably and where even a `Copy` handle argument
840    // would sit in the task arena for the whole run beside the worker's own
841    // copy (rust-lang/rust#62958). The glue probed the slot, so a read that
842    // still comes up empty means an out-of-band `take()` won the race: the
843    // shell exits immediately (`mark_lost_resource` — a warn plus the
844    // completion record), reading as a failed activation rather than a panic.
845    //
846    // Lend entries are lent to the worker as `&mut` and restored to their slot
847    // after the worker returns — i.e. after its clean shutdown ack — so a
848    // Terminate respawn re-takes the SAME instance instead of re-acquiring
849    // hardware. A `Pause` worker parks instead of returning and simply retains
850    // them. `consume` and `shared` forward by value with no restore: a consumed
851    // slot stays empty until the app re-`provide()`s (fail-closed respawn), a
852    // shared slot was never emptied. A `divisible` entry forwards the member's
853    // `Claimant` by value; its share is released by the supervisor, not here.
854    //
855    // Per-entry `#[cfg]` rides on params, reads, worker-call arguments, and
856    // restore statements alike, so a cfg'd-out entry disappears from the whole
857    // chain (the worker fn must gate its matching parameter the same way).
858    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                // A pool shell is shared by its members and cannot name `RES[I]`
866                // itself, so member `I`'s slot element rides in as a reference.
867                // (`shared` slots are pool-wide statics, nameable directly.)
868                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                // Likewise the member's slot in a budget: the wrapper builds
874                // the `Claimant` and passes it in.
875                ResourceKind::Divisible if pool_member => {
876                    let var = format_ident!("__r{}", i);
877                    Some(quote!(#(#cfg)* #var: #cr::Claimant))
878                }
879                _ => None, // read from the slot static inside the shell
880            }
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            // Reclaim the bulk FIRST: before restores, exit-provide, and the
961            // completion record, so has_exited() implies the heap is back.
962            quote!(::core::mem::drop(__state);),
963        ),
964        None => (quote!(), vec![], quote!()),
965    };
966    // `cancel` workers take no node: the shell holds it and races the worker's
967    // future against the shutdown signal itself, which is the whole point of the
968    // flag — the worker stays a plain async fn with no supervisor in its
969    // signature.
970    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    // Unsuffixed literal: `#[task]`'s own parser wants a plain integer.
979    let ps = LitInt::new(&pool_size.to_string(), proc_macro2::Span::call_site());
980    // A diverging (`-> !`) worker makes the trailing statements unreachable —
981    // legitimate (a detached/`Pause` worker retains its resources forever), so
982    // silence rustc's `unreachable_code` lint on the generated body. Always
983    // emitted: the completion record below is an unconditional trailing
984    // statement.
985    let allow_unreachable = quote!(#[allow(unreachable_code)]);
986    // `exit: Type`: bind the worker's return value and provide() it into the
987    // node's exit slot BEFORE mark_exited, so has_exited() implies the value is
988    // present. A worker whose return type mismatches the declared `exit:` fails
989    // at this provide with a plain rustc type error on the shell.
990    // Under `cancel` the worker may not have returned at all — the shell holds a
991    // `Result<Output, Aborted>` — so the exit value is provided only on a real
992    // completion. An aborted worker leaves `<NODE>_EXIT` empty (and
993    // `shutdown_requested()` set), which is how a waiter tells "it finished" from
994    // "it was stopped".
995    //
996    // A DIVERGING worker (`-> !`) makes that provide dead code: its future has
997    // no output, so the slot could never be filled and every `wait_take()` on
998    // it would hang forever. The blanket allow above would hide that, so the
999    // provide re-DENIES `unreachable_code` on itself — the one statement in the
1000    // shell where unreachability is a declaration error rather than a
1001    // legitimate parked/detached worker. Spanned on the declared `exit:` type,
1002    // so rustc points at the clause the user has to remove (a bare diverging
1003    // worker stays legal: that is what `cancel` is for).
1004    let exit_ident = format_ident!("{}_EXIT", owner);
1005    // The worker's return value binding. Created ONCE, with the macro's own
1006    // call-site span, and interpolated into both the `let` below and the
1007    // `provide` — writing `__out` literally inside `quote_spanned!(exit.span())`
1008    // would give the two occurrences different hygiene contexts, and the
1009    // provide would fail to see the binding whenever the `exit:` clause's span
1010    // came from a different expansion than the shell's (e.g. a node declared at
1011    // a `compose_graph!` site alongside `supervisor_fragment!` fragments).
1012    //
1013    // The span is the `exit:` type's, matching the `provide` below, so the
1014    // clause's own diagnostics (the `unreachable_code` deny on a diverging
1015    // worker) keep pointing at the clause rather than at the whole graph.
1016    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        // Every token of the statement carries the `exit:` type's span, so the
1020        // lint's own label lands on that clause instead of the whole item.
1021        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    // Pin the worker in the shell frame so its state is stored once. This
1028    // avoids doubling it as a callee argument, which matters in static task
1029    // storage.
1030    // With `fault-inject` the pinned worker is wrapped in `Injected`: stall,
1031    // hog and crash apply transparently. The wrapper still resolves to an
1032    // `Option`, so diverging-`exit:` remains a lint error as usual.
1033    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    // Record the completion (and ack any pending shutdown handshake): a worker
1098    // that returns on its own reads as down, not running forever, and a control
1099    // Activate can respawn it. A per-node shell names its node STATIC here (like
1100    // the `_EXIT` provide above) so `__node`'s last use is the worker call — a
1101    // local dead before the await stays out of the task arena. A pool shell is
1102    // shared by its members, so only the parameter knows the node.
1103    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
1125/// Emit a `node`: its `pub static #ident: TaskNode` definition and its `Slot`. The
1126/// caller assigns the slot index and records the name, so this touches neither.
1127/// A `task:` node additionally emits its generated shell ahead of the static.
1128fn emit_node(
1129    n: &NodeItem,
1130    cr: &TokenStream2,
1131    spawn_fn: &TokenStream2,
1132    // Threaded to ready_tokens: a ready dep naming a pool refs its floor member.
1133    pool_names: &std::collections::HashSet<String>,
1134    helpers: &HelperIdents,
1135    observe: &ObserveDefaults,
1136    // Graph-wide resource facts: `provides:` name resolution and this node's
1137    // slot in each `divisible` budget.
1138    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            // A node's take-kind slots hold ONE value, so extra instances
1153            // could only race the shell's take; a divisible slot is ONE
1154            // claimant, so extra instances would clobber each other's want
1155            // and share one grant. Reject the combination instead of letting
1156            // the loser exit as a lost resource.
1157            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    // `executor: NAME` routes the node through that SpawnerSlot; the supervisor
1199    // awaits the slot before spawning (see `TaskNode::with_executor`).
1200    let mut with_exec = match &n.executor {
1201        Some(ex) => quote!( .with_executor(&#ex) ),
1202        None => quote!(),
1203    };
1204    // With `fault-inject`, shelled `task:` nodes can be stalled, crashed or hogged.
1205    // Hand-written `spawn:` tasks only support wedge.
1206    if cfg!(feature = "fault-inject") && matches!(n.source, Some(TaskSource::Shell(_))) {
1207        with_exec.extend(quote!( .with_shell() ));
1208    }
1209    // `resources: [NAME: Type, ..]` — one `pub static NAME: ResourceSlot<Type>`
1210    // per entry (main moves the resource in with `NAME.provide(..)`), plus a
1211    // type-erased gate array wired into the node so the supervisor can await
1212    // provisioning/restore before each (re)spawn (see `TaskNode::with_resources`).
1213    // The unsized coercion `&NAME` -> `&dyn ResourceGate` happens in the static
1214    // initializer, where it is allowed.
1215    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        // `shared` slots and `divisible` budgets are emitted once per graph in
1220        // `expand` (several items may declare the same one); only this node's
1221        // exclusive (take-kind) slots are emitted here.
1222        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                // `local` entries use the graph-site slot type (emitted once per
1231                // graph in `expand`): same provide/take protocol as `ResourceSlot`
1232                // but without its `T: Send` bound, for `!Send` driver handles local
1233                // to one executor. `consume` changes only shell codegen (by-value
1234                // arg, no restore) — the slot type is the same either way.
1235                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            // `beat_window:` alone is rejected as a shape error.
1306            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    // The node's view of its own graph: what a data-driven dependency resolves
1331    // its producer through. Const, and a cycle only in the address sense — the
1332    // graph names the nodes, each node names the graph.
1333    let graph_ref = &helpers.graph_ref;
1334    let with_graph = cfg!(feature = "data-deps").then(|| quote!( .with_graph(&#graph_ref) ));
1335    // `deps: [X ready, ..]` — the ready-marked subset becomes a per-node
1336    // `[&'static TaskNode; n]` array wired via `.with_ready_deps`: bring-up
1337    // awaits each one's set_ready() (bounded by slot_timeout) after the
1338    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    // Graph-wide resource facts: the pool's base slot in each `divisible`
1477    // budget (member `j` holds base + j).
1478    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, &quote!(&#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    // `deps: [X ready, ..]` — ONE shared ready-dep array for the whole pool
1696    // (markers apply to every member; growth also checks it synchronously).
1697    // The three dep-marker overlays and the two coupling tables: one table per
1698    // POOL, shared by every member. A pool is a single declaration instantiated
1699    // K times, so its members gate on the same deps and exchange the same
1700    // signals — and sharing the statics keeps the flash cost independent of the
1701    // member count.
1702    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    // A `veto` write gives every member its own contributor slot, so such a
1736    // pool emits one writes table per member (the flash cost is the opt-in's);
1737    // any other pool shares one table across its members, as before.
1738    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    // Empty token streams for absent clauses, so pushing unconditionally is a
1780    // no-op rather than a special case.
1781    {
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                // Probes without moving the value, so it is safe to call from any
2137                // executor even when T is !Send.
2138                fn is_filled(&self) -> bool {
2139                    self.slot.lock(|c| {
2140                        // SAFETY: lock held; we only read, never move.
2141                        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    /// A graph-wide slot — a `shared` slot or a `divisible` budget — that
2175    /// several items may declare and the graph emits once.
2176    struct GraphSlotPlan<'a> {
2177        /// First declaration — supplies the emitted static's ident (span), type,
2178        decl: &'a ResourceDecl,
2179        /// Kinds+type token string every re-declaration must match.
2180        sig: String,
2181        /// One entry per declaring site: `None` = unconditional (the slot is
2182        /// then unconditional too), `Some(pred)` = that site's combined
2183        preds: Vec<Option<TokenStream2>>,
2184        owners: Vec<String>,
2185        /// The first declarer's `executor:` (`None` = the supervisor's own),
2186        /// which a `serialized` slot holds every other declarer to.
2187        executor: Option<String>,
2188        /// `divisible`: claimant slots handed out so far (a node takes one, a
2189        /// pool one per member), which sizes the emitted `Budget<K>`.
2190        slots: usize,
2191    }
2192    let mut shared_plans: Vec<(String, GraphSlotPlan)> = Vec::new();
2193    // (owner, resource) -> the owner's base slot in that budget.
2194    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                            // `serialized`: every holder on one executor, so no
2256                            // higher-tier waiter can be starved by a lower-tier
2257                            // holder — priority ceiling by construction, since
2258                            // embassy can neither boost a holder nor migrate a
2259                            // task. Syntactic: `#[cfg]`s are not consulted.
2260                            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                        // Counted syntactically: a `#[cfg]`'d-out declarer still
2303                        // takes its slots, so the budget can only be oversized.
2304                        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    // `local` slots are confined to a single executor. Every consumer and the
2337    // provider must run on that executor; `#[cfg]`s are not checked.
2338    {
2339        let tier = |e: &Option<String>| match e {
2340            Some(x) => format!("`{x}`"),
2341            None => "the supervisor's executor".to_string(),
2342        };
2343        // slot -> (first declarer, its executor)
2344        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    // `veto` contributor slots: per gate (by its display text), writers are
2465    // numbered in item order — a node takes one slot, a pool one per member —
2466    // and the gate is checked once for the total. Counted syntactically, so a
2467    // `#[cfg]`'d-out writer still reserves its slot: the check can only be
2468    // stricter than the build. The text is load-bearing here (the slot is a
2469    // bit of the static it resolves to), so one gate named two ways — `TRIP`
2470    // beside `crate::TRIP` — is rejected rather than numbered twice.
2471    struct VetoPlan {
2472        key: String,
2473        target: TokenStream2,
2474        total: usize,
2475        /// One entry per writer, `None` = unconditional: gates the check the
2476        /// way `GraphSlotPlan::preds` gates a shared static, so a gate whose
2477        /// writers all sit behind a `#[cfg]` is not named in a build without it.
2478        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    // Last path segment (plus index) -> the first spelling seen for it.
2483    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    // The hook bodies are the supervisor's (`__sv_trace_hooks!`): which executor
2757    // hook API they speak is decided by that crate's build, not this one's.
2758    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        // Private backing tables — the application uses the graph static. The
2800        // topology (dep table + order, or `Flat`) and pools are inlined into
2801        // its literal below; the node count is `.nodes.len()`.
2802        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            &param.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        &param.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            // The verbs a feature adds to `TaskNode`: name the feature here
3082            // rather than leave rustc to report a missing method.
3083            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        &param.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(), &quote!(__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        // The gated `disabled` becomes a cfg-block bool, not a bare literal.
3342        assert!(out.contains("# [cfg (feature = \"x\")] { true }"), "{out}");
3343        assert!(!out.contains("compile_error"), "{out}");
3344
3345        // Pool timeouts ride the same emitter.
3346        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        // The third call makes `A` unconditional, so the only `feature = "x"`
3603        // left is the statement's own attribute; a second occurrence is a call
3604        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        // Different statics that happen to share an ident are two gates.
3795        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        // An indexed gate is keyed by its index too.
3805        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}