Skip to main content

embassy_supervisor_macros/
lib.rs

1//! Proc-macro for `embassy-supervisor`.
2//!
3//! `supervisor_graph!` is the **single source** of a task graph: it declares the
4//! nodes (and an optional elastic pool), generates their `static`s, and computes
5//! the topological order at **compile time**. A dependency cycle is a compile
6//! error; an unknown dependency name is a compile error.
7//!
8//! Surface (each `node`/`pool` may be `#[cfg(...)]`-prefixed):
9//! ```text
10//! node NAME = Mode, deps: [A, B], spawn: <spawn>[, disabled];
11//! node NAME = Mode, deps: [A];                 // no `spawn:` => a parked node the app spawns
12//! pool NAME = [Mode, ..], deps: [A], spawn: <fn>, policy: [<Ty> =] <expr>, min: N, max: M;
13//! ```
14//! A pool is emitted as `ElasticPool<P>`, so the macro needs the policy type `P`. By
15//! default it derives `P` from a `Ty::new(..)`-shaped `policy:` value (e.g.
16//! `DeferredShrink::new(..)` => `P = DeferredShrink`). Give `policy: <Ty> = <expr>` to
17//! state `P` explicitly when the value isn't that shape — a const, a free fn, a builder
18//! chain (`X::new(..).with(..)`), or a qualified path.
19//! `spawn:` takes a path or a partial call to a task fn taking the node **first**
20//! (`spawn: f` => `s.spawn(f(&NAME)?)`; `spawn: f(a)` => `s.spawn(f(&NAME, a)?)`), or,
21//! for a node, a closure / ready spawn fn emitted verbatim (for anything that doesn't
22//! fit that shape). A pool's `spawn:` is the same path/partial-call form with `&POOL[j]`
23//! injected first, via a generated `spawn_<pool>::<j>` glue fn; a pool has no closure
24//! form (members are instantiated per index).
25//!
26//! A graph holds at most **256 node slots** (including pool members): all graph
27//! indices are `u8`, and the macro rejects a larger declaration at expansion.
28//!
29//! Nodes, pools, and individual deps may carry `#[cfg(...)]` attributes. A
30//! proc-macro can't evaluate `cfg`, so the node array is a fixed-length
31//! `[Option<&TaskNode>; M]` over all declared slots (each entry `Some`/`None` via a
32//! cfg-expression), the dep table is cfg-aware per-dep, and the order runs through
33//! `topo_sort_const` at const-eval (after cfg). Absent nodes are skipped at runtime.
34//!
35//! Generated items (at the call site): one `pub static` per `node`, a `[TaskNode; K]`
36//! array + `spawn_<pool>` glue fn + `<POOL>_POOL` `ElasticPool` per `pool`, plus a
37//! single `pub static GRAPH: Graph<M>` bundling the node slots, the dependency table,
38//! the topological order, and (with the `pool` feature) the pools — pass `&GRAPH` to
39//! `Supervisor::new`. The backing tables are private; read them through `GRAPH.nodes`
40//! / `GRAPH.deps` / `GRAPH.order` / `GRAPH.pools` (node count is `GRAPH.nodes.len()`).
41//!
42//! Types are referenced absolutely (`::embassy_supervisor::…`), so the consuming
43//! crate must depend on `embassy-supervisor` under its real name (not aliased).
44
45use proc_macro::TokenStream;
46use proc_macro2::TokenStream as TokenStream2;
47use quote::{format_ident, quote};
48use std::collections::HashMap;
49use syn::parse::{Parse, ParseStream};
50use syn::punctuated::Punctuated;
51use syn::{
52    Attribute, Expr, Ident, LitInt, Meta, Path, Result as SynResult, Token, Type, bracketed,
53};
54
55mod kw {
56    syn::custom_keyword!(node);
57    syn::custom_keyword!(pool);
58    syn::custom_keyword!(deps);
59    syn::custom_keyword!(spawn);
60    syn::custom_keyword!(policy);
61    syn::custom_keyword!(min);
62    syn::custom_keyword!(max);
63    syn::custom_keyword!(disabled);
64}
65
66/// A dependency reference: a node ident, optionally `#[cfg(...)]`-gated.
67#[derive(Clone)]
68struct Dep {
69    cfg: Vec<Attribute>,
70    ident: Ident,
71}
72
73/// `deps: [a, #[cfg(feature = "x")] b, …]`
74fn parse_dep_list(input: ParseStream) -> SynResult<Vec<Dep>> {
75    let content;
76    bracketed!(content in input);
77    let mut deps = Vec::new();
78    while !content.is_empty() {
79        let cfg = content.call(Attribute::parse_outer)?;
80        let ident: Ident = content.parse()?;
81        deps.push(Dep { cfg, ident });
82        if content.peek(Token![,]) {
83            content.parse::<Token![,]>()?;
84        }
85    }
86    Ok(deps)
87}
88
89/// `[Terminate, OnDemand, …]` — the bracketed mode list.
90fn parse_mode_list(input: ParseStream) -> SynResult<Vec<Ident>> {
91    let content;
92    bracketed!(content in input);
93    let punct = Punctuated::<Ident, Token![,]>::parse_terminated(&content)?;
94    Ok(punct.into_iter().collect())
95}
96
97struct NodeItem {
98    cfg: Vec<Attribute>,
99    ident: Ident,
100    mode: Ident,
101    deps: Vec<Dep>,
102    /// `None` = a parked node the app spawns itself (no `spawn:` given).
103    spawn: Option<Expr>,
104    disabled: bool,
105}
106
107struct PoolItem {
108    cfg: Vec<Attribute>,
109    ident: Ident,
110    modes: Vec<Ident>,
111    deps: Vec<Dep>,
112    /// The member task. Either a bare path (`http_task`) or a partial call carrying
113    /// extra args (`mcp_server_task(stack())`); the macro spawns member `j` as
114    /// `s.spawn(<fn>(&POOL[j] [, extra args])?)` — the node is always the first arg.
115    /// No closure form (members are instantiated per index), unlike a node's `spawn:`.
116    spawn: Expr,
117    /// The scaling policy value, emitted as the `policy:` field of the `ElasticPool`
118    /// static. The static is typed `ElasticPool<P>`, so the macro needs the policy
119    /// *type* `P`: when `policy_ty` is `None` it derives `P` from this expr via
120    /// `policy_type` (requires a `Type::new(..)` shape); when `policy_ty` is `Some`
121    /// the caller stated `P` explicitly and this expr can be any value of that type.
122    policy: Expr,
123    /// Optional explicit policy type from the `policy: <Type> = <expr>` form. `Some`
124    /// bypasses `policy_type` derivation, allowing a value the deriver can't handle
125    /// (a free fn, a const, a builder chain, a qualified path).
126    policy_ty: Option<Type>,
127    min: LitInt,
128    max: LitInt,
129}
130
131// Both variants embed a large `syn::Expr` (and `PoolItem` a bit more), so their sizes
132// are close but unequal — enough for `large_enum_variant` to flag the gap. This AST is
133// parsed once and lives only briefly in a `Vec` during expansion, so boxing a variant
134// to shave a few bytes per element buys nothing real; suppress the lint instead of
135// paying a heap allocation.
136#[allow(clippy::large_enum_variant)]
137enum Item {
138    Node(NodeItem),
139    Pool(PoolItem),
140}
141
142/// The parsed macro input: the list of `node`/`pool` declarations, in source order.
143/// Named `GraphSpec` (not `Graph`) to stay distinct from the *emitted* public type
144/// [`embassy_supervisor::Graph`] that `expand` produces as the `GRAPH` static.
145struct GraphSpec {
146    items: Vec<Item>,
147}
148
149impl Parse for GraphSpec {
150    fn parse(input: ParseStream) -> SynResult<Self> {
151        let mut items = Vec::new();
152        while !input.is_empty() {
153            let cfg = input.call(Attribute::parse_outer)?;
154            if input.peek(kw::node) {
155                items.push(Item::Node(parse_node(input, cfg)?));
156            } else if input.peek(kw::pool) {
157                items.push(Item::Pool(parse_pool(input, cfg)?));
158            } else {
159                return Err(
160                    input.error("expected `node` or `pool` (optionally `#[cfg(...)]`-prefixed)")
161                );
162            }
163        }
164        Ok(GraphSpec { items })
165    }
166}
167
168// node IDENT = MODE, deps: [..] [, spawn: <expr>] [, disabled];
169fn parse_node(input: ParseStream, cfg: Vec<Attribute>) -> SynResult<NodeItem> {
170    input.parse::<kw::node>()?;
171    let ident: Ident = input.parse()?;
172    input.parse::<Token![=]>()?;
173    let mode: Ident = input.parse()?;
174    input.parse::<Token![,]>()?;
175    input.parse::<kw::deps>()?;
176    input.parse::<Token![:]>()?;
177    let deps = parse_dep_list(input)?;
178
179    let mut spawn = None;
180    let mut disabled = false;
181    while input.peek(Token![,]) {
182        input.parse::<Token![,]>()?;
183        if input.peek(kw::spawn) {
184            input.parse::<kw::spawn>()?;
185            input.parse::<Token![:]>()?;
186            spawn = Some(input.parse::<Expr>()?);
187        } else if input.peek(kw::disabled) {
188            input.parse::<kw::disabled>()?;
189            disabled = true;
190        } else {
191            return Err(input.error("expected `spawn:` or `disabled`"));
192        }
193    }
194    input.parse::<Token![;]>()?;
195    Ok(NodeItem {
196        cfg,
197        ident,
198        mode,
199        deps,
200        spawn,
201        disabled,
202    })
203}
204
205// pool IDENT = [MODES], deps: [..], spawn: <fn>, policy: EXPR, min: N, max: M;
206fn parse_pool(input: ParseStream, cfg: Vec<Attribute>) -> SynResult<PoolItem> {
207    input.parse::<kw::pool>()?;
208    let ident: Ident = input.parse()?;
209    input.parse::<Token![=]>()?;
210    let modes = parse_mode_list(input)?;
211    input.parse::<Token![,]>()?;
212    input.parse::<kw::deps>()?;
213    input.parse::<Token![:]>()?;
214    let deps = parse_dep_list(input)?;
215    input.parse::<Token![,]>()?;
216    // The member task: a path, or a partial call supplying extra args (the macro
217    // injects `&POOL[j]` as the first argument in either case).
218    input.parse::<kw::spawn>()?;
219    input.parse::<Token![:]>()?;
220    let spawn: Expr = input.parse()?;
221    input.parse::<Token![,]>()?;
222    input.parse::<kw::policy>()?;
223    input.parse::<Token![:]>()?;
224    // Optional explicit policy type: `policy: <Ty> = <expr>`. Fork to see if a `Type`
225    // is followed by `=`; if so it's an annotation (commit on the real stream + eat the
226    // `=`), otherwise rewind and treat the whole thing as the value expr (type derived
227    // from it in `emit_pool`). For the common `Ty::new(..)` value the fork parses only a
228    // partial type and then sees `(`, not `=`, so it correctly falls back to the derive
229    // path — this keeps the bare form working unchanged.
230    let policy_ty = {
231        let fork = input.fork();
232        if fork.parse::<Type>().is_ok() && fork.peek(Token![=]) {
233            let ty: Type = input.parse()?;
234            input.parse::<Token![=]>()?;
235            Some(ty)
236        } else {
237            None
238        }
239    };
240    let policy: Expr = input.parse()?;
241    input.parse::<Token![,]>()?;
242    input.parse::<kw::min>()?;
243    input.parse::<Token![:]>()?;
244    let min: LitInt = input.parse()?;
245    input.parse::<Token![,]>()?;
246    input.parse::<kw::max>()?;
247    input.parse::<Token![:]>()?;
248    let max: LitInt = input.parse()?;
249    input.parse::<Token![;]>()?;
250    Ok(PoolItem {
251        cfg,
252        ident,
253        modes,
254        deps,
255        spawn,
256        policy,
257        policy_ty,
258        min,
259        max,
260    })
261}
262
263/// The node/pool name string: ident lowercased with `_`→`-` (`WIFI_CTRL` → "wifi-ctrl").
264fn name_string(ident: &Ident) -> String {
265    ident.to_string().to_lowercase().replace('_', "-")
266}
267
268/// Build a task-call expression with `node_ref` injected as the **first** argument:
269/// a bare path `f` => `f(node_ref)`; a partial call `f(a, b)` => `f(node_ref, a, b)`.
270/// The task fn is thus expected to take the node/`&POOL[i]` first, then any extra args.
271fn inject_node_call(task: &Expr, node_ref: &TokenStream2) -> SynResult<TokenStream2> {
272    match task {
273        Expr::Path(_) => Ok(quote!(#task(#node_ref))),
274        Expr::Call(c) => {
275            let f = &c.func;
276            let args = c.args.iter();
277            Ok(quote!(#f(#node_ref #(, #args)*)))
278        }
279        other => Err(syn::Error::new_spanned(
280            other,
281            "expected a task-fn path or a partial call like `f(extra_args)`",
282        )),
283    }
284}
285
286/// Combine an item's `#[cfg(...)]` attributes into one predicate (`all(..)` if
287/// several), used to gate its `GRAPH.nodes` slot to `Some`/`None`. `None` = always present.
288fn cfg_predicate(attrs: &[Attribute]) -> Option<TokenStream2> {
289    let preds: Vec<TokenStream2> = attrs
290        .iter()
291        .filter_map(|a| match &a.meta {
292            Meta::List(ml) if ml.path.is_ident("cfg") => Some(ml.tokens.clone()),
293            _ => None,
294        })
295        .collect();
296    match preds.len() {
297        0 => None,
298        1 => Some(preds[0].clone()),
299        _ => Some(quote!(all(#(#preds),*))),
300    }
301}
302
303/// Extract the policy *type* from a `Type::new(..)` constructor expression. Only used
304/// on the derive path (no explicit `policy: <Ty> = ..` annotation); the type is the
305/// call's path minus its last segment (`DeferredShrink::new` -> `DeferredShrink`).
306fn policy_type(expr: &Expr) -> SynResult<Path> {
307    if let Expr::Call(call) = expr
308        && let Expr::Path(p) = &*call.func
309    {
310        let n = p.path.segments.len();
311        if n >= 2 {
312            let segs: Punctuated<_, Token![::]> =
313                p.path.segments.iter().take(n - 1).cloned().collect();
314            return Ok(Path {
315                leading_colon: p.path.leading_colon,
316                segments: segs,
317            });
318        }
319    }
320    Err(syn::Error::new_spanned(
321        expr,
322        "pool `policy:` must be a `Type::new(..)` constructor (e.g. `DeferredShrink::new(..)`), \
323         or give the type explicitly: `policy: <Type> = <expr>`",
324    ))
325}
326
327/// One emitted node slot, in final index order.
328struct Slot {
329    /// Presence predicate (`None` = unconditional), gates the node slot (`GRAPH.nodes`) entry.
330    cfg_pred: Option<TokenStream2>,
331    /// `&NODE` or `&POOL[j]`.
332    reference: TokenStream2,
333    /// Raw deps, resolved to indices in the second pass.
334    deps: Vec<Dep>,
335}
336
337/// The `Option<fn(..)>` spawn expression for a node. `None` (no `spawn:`) is a
338/// parked node the app spawns itself. A path or partial call is a task fn taking
339/// `&NODE` first (plus any given args); the macro wraps it as
340/// `|s| { s.spawn(<task>(&NODE, ..)?); Ok(()) }`. Anything else (a closure, or a
341/// ready spawn fn) is emitted verbatim. Every form is cast to `spawn_fn` so it
342/// coerces cleanly inside `Option::Some(..)`.
343fn node_spawn(
344    ident: &Ident,
345    spawn: &Option<Expr>,
346    spawn_fn: &TokenStream2,
347) -> SynResult<TokenStream2> {
348    Ok(match spawn {
349        None => quote!(::core::option::Option::None),
350        // A path or a partial call: a task fn taking `&NODE` first (plus any
351        // given args); generate `|s| { s.spawn(<task>(&NODE, ..)?); Ok(()) }`.
352        Some(e @ (Expr::Path(_) | Expr::Call(_))) => {
353            let call = inject_node_call(e, &quote!(&#ident))?;
354            quote!(::core::option::Option::Some(
355                (|s| {
356                    s.spawn(#call?);
357                    ::core::result::Result::Ok(())
358                }) as #spawn_fn
359            ))
360        }
361        // Anything else (a closure, or a ready spawn fn) is emitted verbatim.
362        Some(e) => quote!(::core::option::Option::Some((#e) as #spawn_fn)),
363    })
364}
365
366/// Emit a `node`: its `pub static #ident: TaskNode` definition and its `Slot`. The
367/// caller assigns the slot index and records the name, so this touches neither.
368fn emit_node(
369    n: &NodeItem,
370    cr: &TokenStream2,
371    spawn_fn: &TokenStream2,
372) -> SynResult<(TokenStream2, Slot)> {
373    let ident = &n.ident;
374    let cfg = &n.cfg;
375    let mode = &n.mode;
376    let name = name_string(&n.ident);
377    let disabled = n.disabled;
378    let spawn = node_spawn(ident, &n.spawn, spawn_fn)?;
379    let def = quote! {
380        #(#cfg)*
381        pub static #ident: #cr::TaskNode =
382            #cr::TaskNode::new(#name, #cr::Mode::#mode, #spawn, #disabled);
383    };
384    let slot = Slot {
385        cfg_pred: cfg_predicate(cfg),
386        reference: quote!(&#ident),
387        deps: n.deps.clone(),
388    };
389    Ok((def, slot))
390}
391
392/// Emit a `pool`: the member `[TaskNode; K]` array, the `spawn_<pool>` glue fn, and
393/// the `ElasticPool` static (returned as `defs`, in that emission order), plus the
394/// pool-registry entry (for `GRAPH.pools`) and one `Slot` per member (members occupy
395/// slots but aren't name-addressable, so no name is recorded).
396fn emit_pool(
397    p: &PoolItem,
398    cr: &TokenStream2,
399    spawn_fn: &TokenStream2,
400) -> SynResult<(Vec<TokenStream2>, TokenStream2, Vec<Slot>)> {
401    let ident = &p.ident;
402    let cfg = &p.cfg;
403    let lname = name_string(&p.ident);
404    let pool_static = format_ident!("{}_POOL", ident);
405    let k = p.modes.len();
406
407    // Validate the scaling bounds at expansion time. `base10_parse::<u8>` also
408    // rejects values > 255 with a span-attached error (the `ElasticPool` fields are
409    // `u8`). `min > max` makes the policy contradict itself; `max > k` is a ceiling
410    // the pool can never reach (there are only `k` member slots) — both are
411    // declaration bugs, caught here rather than surfacing as odd runtime scaling.
412    // `max < k` is allowed (spare declared members below the ceiling), as is
413    // `min: 0` (the policy may scale the pool all the way down when idle).
414    let min_v: u8 = p.min.base10_parse()?;
415    let max_v: u8 = p.max.base10_parse()?;
416    if min_v > max_v {
417        return Err(syn::Error::new_spanned(
418            &p.min,
419            format!("pool `min:` ({min_v}) must not exceed `max:` ({max_v})"),
420        ));
421    }
422    if usize::from(max_v) > k {
423        return Err(syn::Error::new_spanned(
424            &p.max,
425            format!("pool `max:` ({max_v}) exceeds the declared member count ({k})"),
426        ));
427    }
428
429    // Build member `I`'s spawn call from the `spawn:` expr, injecting
430    // `&POOL[I]` as the first argument (see `inject_node_call`).
431    let call = inject_node_call(&p.spawn, &quote!(&#ident[I]))?;
432    // Per-member spawn fn: a generated `spawn_<pool>::<I>` wrapper.
433    let wrapper = format_ident!("spawn_{}", lname);
434    let mut defs: Vec<TokenStream2> = Vec::new();
435    defs.push(quote! {
436        #(#cfg)*
437        fn #wrapper<const I: usize>(
438            s: ::embassy_executor::Spawner,
439        ) -> ::core::result::Result<(), ::embassy_executor::SpawnError> {
440            s.spawn(#call?);
441            ::core::result::Result::Ok(())
442        }
443    });
444    let member_spawn: Vec<TokenStream2> = (0..k).map(|j| quote!(#wrapper::<#j>)).collect();
445
446    let members = p
447        .modes
448        .iter()
449        .zip(&member_spawn)
450        .enumerate()
451        .map(|(j, (mode, sp))| {
452            let nm = format!("{lname}{j}");
453            quote! {
454                #cr::TaskNode::new(
455                    #nm, #cr::Mode::#mode,
456                    ::core::option::Option::Some((#sp) as #spawn_fn), false,
457                )
458            }
459        });
460    defs.push(quote! {
461        #(#cfg)*
462        pub static #ident: [#cr::TaskNode; #k] = [ #(#members),* ];
463    });
464
465    let member_refs = (0..k).map(|j| quote!(&#ident[#j]));
466    let policy = &p.policy;
467    // The `ElasticPool<P>` type argument: honor an explicit `policy: <Ty> = ..`
468    // annotation, else derive `P` from the constructor expr (`Ty::new(..)` shape).
469    let policy_ty = match &p.policy_ty {
470        Some(ty) => quote!(#ty),
471        None => {
472            let path = policy_type(policy)?;
473            quote!(#path)
474        }
475    };
476    let (min, max) = (&p.min, &p.max);
477    defs.push(quote! {
478        #(#cfg)*
479        pub static #pool_static: #cr::ElasticPool<#policy_ty> = #cr::ElasticPool {
480            nodes: &[ #(#member_refs),* ],
481            min: #min,
482            max: #max,
483            policy: #policy,
484        };
485    });
486
487    let pool_entry = quote!( #(#cfg)* &#pool_static );
488
489    let pred = cfg_predicate(cfg);
490    let slots = (0..k)
491        .map(|j| Slot {
492            cfg_pred: pred.clone(),
493            reference: quote!(&#ident[#j]),
494            deps: p.deps.clone(),
495        })
496        .collect();
497
498    Ok((defs, pool_entry, slots))
499}
500
501/// Second pass: build the node-slot entries for `GRAPH.nodes` (`Option`, cfg-gated) and
502/// the cfg-aware dep-index entries for `GRAPH.deps`. Runs after every slot + name is
503/// known, since a dep may forward-reference a node declared later. An unknown dep name
504/// is a compile error.
505fn slot_tables(
506    slots: &[Slot],
507    names: &HashMap<String, usize>,
508) -> SynResult<(Vec<TokenStream2>, Vec<TokenStream2>)> {
509    let mut all_entries: Vec<TokenStream2> = Vec::new();
510    let mut deps_entries: Vec<TokenStream2> = Vec::new();
511    for slot in slots {
512        let reference = &slot.reference;
513        all_entries.push(match &slot.cfg_pred {
514            None => quote!(::core::option::Option::Some(#reference)),
515            Some(pred) => quote!({
516                #[cfg(#pred)]
517                { ::core::option::Option::Some(#reference) }
518                #[cfg(not(#pred))]
519                { ::core::option::Option::None }
520            }),
521        });
522
523        let mut dep_toks: Vec<TokenStream2> = Vec::new();
524        for d in &slot.deps {
525            let idx = match names.get(&d.ident.to_string()) {
526                Some(&i) => i as u8,
527                None => {
528                    return Err(syn::Error::new_spanned(
529                        &d.ident,
530                        format!("unknown dependency `{}` — not a declared node", d.ident),
531                    ));
532                }
533            };
534            let cfg = &d.cfg;
535            dep_toks.push(quote!( #(#cfg)* #idx ));
536        }
537        deps_entries.push(quote!( &[ #(#dep_toks),* ] ));
538    }
539    Ok((all_entries, deps_entries))
540}
541
542fn expand(graph: GraphSpec) -> SynResult<TokenStream2> {
543    let cr = quote!(::embassy_supervisor);
544    // The node spawn fn-pointer type. Spawn exprs (closures / const-generic fns) are
545    // cast to this so they coerce cleanly inside `Option::Some(..)`.
546    let spawn_fn = quote!(
547        fn(
548            ::embassy_executor::Spawner,
549        ) -> ::core::result::Result<(), ::embassy_executor::SpawnError>
550    );
551
552    // First pass: emit the statics/glue in declaration order, assign stable slot
553    // indices, and record each slot + its raw deps. `names` maps a node ident to its
554    // slot index for dep resolution — keyed on the *raw* ident (not the runtime
555    // `name_string`), and populated for `node`s only (pool members occupy slots but
556    // aren't name-addressable, so a dep on a pool name stays an "unknown dependency").
557    let mut defs: Vec<TokenStream2> = Vec::new();
558    let mut pool_entries: Vec<TokenStream2> = Vec::new();
559    let mut slots: Vec<Slot> = Vec::new();
560    let mut names: HashMap<String, usize> = HashMap::new();
561
562    for item in &graph.items {
563        match item {
564            Item::Node(n) => {
565                // The index is the slot's position, taken *before* the push.
566                names.insert(n.ident.to_string(), slots.len());
567                let (def, slot) = emit_node(n, &cr, &spawn_fn)?;
568                defs.push(def);
569                slots.push(slot);
570            }
571            Item::Pool(p) => {
572                // Pools are only meaningful with the supervisor's `pool` feature (which
573                // forwards to this crate). Without it, `Graph` has no `pools` field and
574                // `ElasticPool` doesn't exist — so refuse a `pool` with a clear message
575                // rather than emitting dangling references.
576                if cfg!(feature = "pool") {
577                    let (pool_defs, pool_entry, pool_slots) = emit_pool(p, &cr, &spawn_fn)?;
578                    defs.extend(pool_defs);
579                    pool_entries.push(pool_entry);
580                    slots.extend(pool_slots);
581                } else {
582                    return Err(syn::Error::new_spanned(
583                        &p.ident,
584                        "a `pool` requires enabling embassy-supervisor's `pool` feature",
585                    ));
586                }
587            }
588        }
589    }
590
591    let m = slots.len();
592    // Every graph index (dep entries, `topo_sort_const`'s queue/order) is a `u8`, so
593    // more than 256 slots would silently truncate (`i as u8`) and corrupt the order.
594    // 256 slots means max index 255 and max per-node dep count 255 — both fit exactly.
595    if m > 256 {
596        return Err(syn::Error::new(
597            proc_macro2::Span::call_site(),
598            format!(
599                "supervisor_graph!: {m} node slots declared, but at most 256 are supported \
600                 (including pool members) — graph indices are `u8`"
601            ),
602        ));
603    }
604    let (all_entries, deps_entries) = slot_tables(&slots, &names)?;
605
606    // `Graph.pools` is `#[cfg(feature = "pool")]`; emit that field iff this macro was
607    // built with pool support (forwarded from the supervisor's `pool` feature).
608    let pools_field = if cfg!(feature = "pool") {
609        quote!( pools: &[ #(#pool_entries),* ], )
610    } else {
611        quote!()
612    };
613
614    Ok(quote! {
615        #(#defs)*
616
617        // Private backing tables — the application uses `GRAPH`. The topological order
618        // and pools are inlined into the `GRAPH` literal below; the node count is
619        // `GRAPH.nodes.len()`.
620        static NODES: [::core::option::Option<&'static #cr::TaskNode>; #m] = [ #(#all_entries),* ];
621        const DEPS: [&'static [u8]; #m] = [ #(#deps_entries),* ];
622
623        /// The compile-time task graph — node slots, dependency table, topological order,
624        /// and (with the `pool` feature) the elastic pools. Pass to `Supervisor::new`.
625        pub static GRAPH: #cr::Graph<#m> = #cr::Graph {
626            nodes: &NODES,
627            deps: &DEPS,
628            order: #cr::topo_sort_const(&DEPS),
629            #pools_field
630        };
631    })
632}
633
634/// Declare a supervised task graph; see the crate docs for the surface syntax.
635#[proc_macro]
636pub fn supervisor_graph(input: TokenStream) -> TokenStream {
637    let graph = syn::parse_macro_input!(input as GraphSpec);
638    expand(graph)
639        .unwrap_or_else(syn::Error::into_compile_error)
640        .into()
641}