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 item may be `#[cfg(...)]`-prefixed):
9//! ```text
10//! node NAME = Mode, deps: [A, B], spawn: <spawn>[, executor: EXEC][, disabled];
11//! node NAME = Mode, deps: [A];                 // no `spawn:` => a parked node the app spawns
12//! executor EXEC;                               // runtime-filled SendSpawner slot
13//! pool NAME = [Mode, ..], deps: [A][, executor: EXEC], spawn: <fn>,
14//!     policy: [<Ty> =] <expr>, min: N, max: M;
15//! ```
16//! `deps:` entries name a `node` or a `pool`; a `pool` dep resolves to that pool's floor
17//! member (member 0, the `min`-kept one), i.e. "start after the pool is up". A repeated
18//! dep or a redeclared node/pool name is a compile error.
19//!
20//! An `executor NAME;` slot may carry `#[cfg(...)]`, but validation does not model cfg
21//! predicates: a node referencing a slot that is cfg'd *out* while the node is cfg'd
22//! *in* surfaces as rustc's `cannot find value NAME`, not a macro error — don't gate an
23//! executor slot more restrictively than the nodes that reference it.
24//! `executor EXEC;` emits a `pub static EXEC: SpawnerSlot`; the app fills it with a
25//! `SendSpawner` (`InterruptExecutor::start`, `Spawner::make_send`) before
26//! `Supervisor::start`, and nodes carrying `executor: EXEC` spawn through it instead
27//! of the supervisor's own executor (their futures must be `Send`; an unfilled slot
28//! fails the spawn with `SpawnError::Busy`).
29//! A pool is emitted as `ElasticPool<P>`, so the macro needs the policy type `P`. By
30//! default it derives `P` from a `Ty::new(..)`-shaped `policy:` value (e.g.
31//! `DeferredShrink::new(..)` => `P = DeferredShrink`). Give `policy: <Ty> = <expr>` to
32//! state `P` explicitly when the value isn't that shape — a const, a free fn, a builder
33//! chain (`X::new(..).with(..)`), or a qualified path.
34//! `spawn:` takes a path or a partial call to a task fn taking the node **first**
35//! (`spawn: f` => `s.spawn(f(&NAME)?)`; `spawn: f(a)` => `s.spawn(f(&NAME, a)?)`), or,
36//! for a node, a closure / ready spawn fn emitted verbatim (for anything that doesn't
37//! fit that shape). A pool's `spawn:` is the same path/partial-call form with `&POOL[j]`
38//! injected first, via a generated `spawn_<pool>::<j>` glue fn; a pool has no closure
39//! form (members are instantiated per index).
40//!
41//! A graph holds at most **256 node slots** (including pool members): all graph
42//! indices are `u8`, and the macro rejects a larger declaration at expansion.
43//!
44//! Nodes, pools, and individual deps may carry `#[cfg(...)]` attributes. A
45//! proc-macro can't evaluate `cfg`, so the node array is a fixed-length
46//! `[Option<&TaskNode>; M]` over all declared slots (each entry `Some`/`None` via a
47//! cfg-expression), the dep table is cfg-aware per-dep, and the order runs through
48//! `topo_sort_const` at const-eval (after cfg). Absent nodes are skipped at runtime.
49//!
50//! Generated items (at the call site): one `pub static` per `node`, a `[TaskNode; K]`
51//! array + `spawn_<pool>` glue fn + `<POOL>_POOL` `ElasticPool` per `pool`, plus a
52//! single `pub static GRAPH: Graph<M>` bundling the node slots, the dependency table,
53//! the topological order, and (with the `pool` feature) the pools — pass `&GRAPH` to
54//! `Supervisor::new`. The backing tables are private; read them through `GRAPH.nodes`
55//! / `GRAPH.deps` / `GRAPH.order` / `GRAPH.pools` (node count is `GRAPH.nodes.len()`).
56//!
57//! With the supervisor's `trace` feature (forwarded here) the generated spawn glue
58//! also captures each `SpawnToken`'s task id into its node (`set_task_id`), and
59//! `trace-names` stamps the node name into the task Metadata. With `trace-hooks`
60//! the macro additionally defines the seven `_embassy_trace_*` hook symbols at the
61//! declaration site (the supervisor crate is `forbid(unsafe_code)` and cannot),
62//! forwarding to the supervisor's `trace` recorders — requires an edition-2024
63//! consumer, and exactly one graph declaration (or hook set) per binary.
64//!
65//! Types are referenced absolutely (`::embassy_supervisor::…`), so the consuming
66//! crate must depend on `embassy-supervisor` under its real name (not aliased).
67
68use proc_macro::TokenStream;
69use proc_macro2::TokenStream as TokenStream2;
70use quote::{format_ident, quote};
71use std::collections::HashMap;
72use syn::parse::{Parse, ParseStream};
73use syn::punctuated::Punctuated;
74use syn::{
75    Attribute, Expr, Ident, LitInt, Meta, Path, Result as SynResult, Token, Type, bracketed,
76};
77
78mod kw {
79    syn::custom_keyword!(node);
80    syn::custom_keyword!(pool);
81    syn::custom_keyword!(deps);
82    syn::custom_keyword!(spawn);
83    syn::custom_keyword!(policy);
84    syn::custom_keyword!(min);
85    syn::custom_keyword!(max);
86    syn::custom_keyword!(disabled);
87    syn::custom_keyword!(executor);
88}
89
90/// A dependency reference: a node ident, optionally `#[cfg(...)]`-gated.
91#[derive(Clone)]
92struct Dep {
93    cfg: Vec<Attribute>,
94    ident: Ident,
95}
96
97/// `deps: [a, #[cfg(feature = "x")] b, …]`
98fn parse_dep_list(input: ParseStream) -> SynResult<Vec<Dep>> {
99    let content;
100    bracketed!(content in input);
101    let mut deps = Vec::new();
102    while !content.is_empty() {
103        let cfg = content.call(Attribute::parse_outer)?;
104        let ident: Ident = content.parse()?;
105        deps.push(Dep { cfg, ident });
106        if content.peek(Token![,]) {
107            content.parse::<Token![,]>()?;
108        }
109    }
110    Ok(deps)
111}
112
113/// `[Terminate, OnDemand, …]` — the bracketed mode list.
114fn parse_mode_list(input: ParseStream) -> SynResult<Vec<Ident>> {
115    let content;
116    bracketed!(content in input);
117    let punct = Punctuated::<Ident, Token![,]>::parse_terminated(&content)?;
118    Ok(punct.into_iter().collect())
119}
120
121struct NodeItem {
122    cfg: Vec<Attribute>,
123    ident: Ident,
124    mode: Ident,
125    deps: Vec<Dep>,
126    /// `None` = a parked node the app spawns itself (no `spawn:` given).
127    spawn: Option<Expr>,
128    disabled: bool,
129    /// `executor: NAME` — spawn through the named [`SpawnerSlot`] (a
130    /// `SendSpawner` the app registers at runtime) instead of the supervisor's
131    /// own `Spawner`. `None` = the default executor.
132    executor: Option<Ident>,
133}
134
135/// `executor NAME;` — declares a `pub static NAME: SpawnerSlot` the application
136/// fills with a `SendSpawner` before (or concurrently with) `Supervisor::start` (an
137/// InterruptExecutor tier, core1, ...). Nodes reference it with `executor: NAME`; the
138/// supervisor awaits the slot before spawning such a node (bounded by
139/// `SLOT_READY_TIMEOUT`, then `SpawnError::Busy`).
140struct ExecutorItem {
141    cfg: Vec<Attribute>,
142    ident: Ident,
143}
144
145struct PoolItem {
146    cfg: Vec<Attribute>,
147    ident: Ident,
148    modes: Vec<Ident>,
149    deps: Vec<Dep>,
150    /// The member task. Either a bare path (`http_task`) or a partial call carrying
151    /// extra args (`mcp_server_task(stack())`); the macro spawns member `j` as
152    /// `s.spawn(<fn>(&POOL[j] [, extra args])?)` — the node is always the first arg.
153    /// No closure form (members are instantiated per index), unlike a node's `spawn:`.
154    spawn: Expr,
155    /// The scaling policy value, emitted as the `policy:` field of the `ElasticPool`
156    /// static. The static is typed `ElasticPool<P>`, so the macro needs the policy
157    /// *type* `P`: when `policy_ty` is `None` it derives `P` from this expr via
158    /// `policy_type` (requires a `Type::new(..)` shape); when `policy_ty` is `Some`
159    /// the caller stated `P` explicitly and this expr can be any value of that type.
160    policy: Expr,
161    /// Optional explicit policy type from the `policy: <Type> = <expr>` form. `Some`
162    /// bypasses `policy_type` derivation, allowing a value the deriver can't handle
163    /// (a free fn, a const, a builder chain, a qualified path).
164    policy_ty: Option<Type>,
165    /// `executor: NAME` — spawn every member through the named [`SpawnerSlot`]
166    /// (e.g. a worker pool on the second core, scaled by this core's supervisor).
167    executor: Option<Ident>,
168    min: LitInt,
169    max: LitInt,
170}
171
172// Both variants embed a large `syn::Expr` (and `PoolItem` a bit more), so their sizes
173// are close but unequal — enough for `large_enum_variant` to flag the gap. This AST is
174// parsed once and lives only briefly in a `Vec` during expansion, so boxing a variant
175// to shave a few bytes per element buys nothing real; suppress the lint instead of
176// paying a heap allocation.
177#[allow(clippy::large_enum_variant)]
178enum Item {
179    Node(NodeItem),
180    Pool(PoolItem),
181    Executor(ExecutorItem),
182}
183
184/// The parsed macro input: the list of `node`/`pool` declarations, in source order.
185/// Named `GraphSpec` (not `Graph`) to stay distinct from the *emitted* public type
186/// [`embassy_supervisor::Graph`] that `expand` produces as the `GRAPH` static.
187struct GraphSpec {
188    items: Vec<Item>,
189}
190
191impl Parse for GraphSpec {
192    fn parse(input: ParseStream) -> SynResult<Self> {
193        let mut items = Vec::new();
194        while !input.is_empty() {
195            let cfg = input.call(Attribute::parse_outer)?;
196            if input.peek(kw::node) {
197                items.push(Item::Node(parse_node(input, cfg)?));
198            } else if input.peek(kw::pool) {
199                items.push(Item::Pool(parse_pool(input, cfg)?));
200            } else if input.peek(kw::executor) {
201                // `executor NAME;` — a runtime-filled SendSpawner slot; nodes
202                // carrying `executor: NAME` spawn through it (the supervisor awaits
203                // the slot before spawning them).
204                input.parse::<kw::executor>()?;
205                let ident: Ident = input.parse()?;
206                input.parse::<Token![;]>()?;
207                items.push(Item::Executor(ExecutorItem { cfg, ident }));
208            } else {
209                return Err(input.error(
210                    "expected `node`, `pool`, or `executor` (optionally `#[cfg(...)]`-prefixed)",
211                ));
212            }
213        }
214        Ok(GraphSpec { items })
215    }
216}
217
218// node IDENT = MODE, deps: [..] [, spawn: <expr>] [, disabled];
219fn parse_node(input: ParseStream, cfg: Vec<Attribute>) -> SynResult<NodeItem> {
220    input.parse::<kw::node>()?;
221    let ident: Ident = input.parse()?;
222    input.parse::<Token![=]>()?;
223    let mode: Ident = input.parse()?;
224    input.parse::<Token![,]>()?;
225    input.parse::<kw::deps>()?;
226    input.parse::<Token![:]>()?;
227    let deps = parse_dep_list(input)?;
228
229    let mut spawn = None;
230    let mut disabled = false;
231    let mut executor = None;
232    while input.peek(Token![,]) {
233        input.parse::<Token![,]>()?;
234        if input.peek(kw::spawn) {
235            input.parse::<kw::spawn>()?;
236            input.parse::<Token![:]>()?;
237            spawn = Some(input.parse::<Expr>()?);
238        } else if input.peek(kw::disabled) {
239            input.parse::<kw::disabled>()?;
240            disabled = true;
241        } else if input.peek(kw::executor) {
242            input.parse::<kw::executor>()?;
243            input.parse::<Token![:]>()?;
244            executor = Some(input.parse::<Ident>()?);
245        } else {
246            return Err(input.error("expected `spawn:`, `executor:`, or `disabled`"));
247        }
248    }
249    input.parse::<Token![;]>()?;
250    Ok(NodeItem {
251        cfg,
252        ident,
253        mode,
254        deps,
255        spawn,
256        disabled,
257        executor,
258    })
259}
260
261// pool IDENT = [MODES], deps: [..][, executor: EXEC], spawn: <fn>, policy: EXPR, min: N, max: M;
262fn parse_pool(input: ParseStream, cfg: Vec<Attribute>) -> SynResult<PoolItem> {
263    input.parse::<kw::pool>()?;
264    let ident: Ident = input.parse()?;
265    input.parse::<Token![=]>()?;
266    let modes = parse_mode_list(input)?;
267    input.parse::<Token![,]>()?;
268    input.parse::<kw::deps>()?;
269    input.parse::<Token![:]>()?;
270    let deps = parse_dep_list(input)?;
271    input.parse::<Token![,]>()?;
272    // Optional `executor: NAME,` — run the whole pool on the named SpawnerSlot's
273    // executor (e.g. a worker pool on the second core, scaled from this one).
274    let executor = if input.peek(kw::executor) {
275        input.parse::<kw::executor>()?;
276        input.parse::<Token![:]>()?;
277        let ex: Ident = input.parse()?;
278        input.parse::<Token![,]>()?;
279        Some(ex)
280    } else {
281        None
282    };
283    // The member task: a path, or a partial call supplying extra args (the macro
284    // injects `&POOL[j]` as the first argument in either case).
285    input.parse::<kw::spawn>()?;
286    input.parse::<Token![:]>()?;
287    let spawn: Expr = input.parse()?;
288    input.parse::<Token![,]>()?;
289    input.parse::<kw::policy>()?;
290    input.parse::<Token![:]>()?;
291    // Optional explicit policy type: `policy: <Ty> = <expr>`. Fork to see if a `Type`
292    // is followed by `=`; if so it's an annotation (commit on the real stream + eat the
293    // `=`), otherwise rewind and treat the whole thing as the value expr (type derived
294    // from it in `emit_pool`). For the common `Ty::new(..)` value the fork parses only a
295    // partial type and then sees `(`, not `=`, so it correctly falls back to the derive
296    // path — this keeps the bare form working unchanged.
297    let policy_ty = {
298        let fork = input.fork();
299        if fork.parse::<Type>().is_ok() && fork.peek(Token![=]) {
300            let ty: Type = input.parse()?;
301            input.parse::<Token![=]>()?;
302            Some(ty)
303        } else {
304            None
305        }
306    };
307    let policy: Expr = input.parse()?;
308    input.parse::<Token![,]>()?;
309    input.parse::<kw::min>()?;
310    input.parse::<Token![:]>()?;
311    let min: LitInt = input.parse()?;
312    input.parse::<Token![,]>()?;
313    input.parse::<kw::max>()?;
314    input.parse::<Token![:]>()?;
315    let max: LitInt = input.parse()?;
316    input.parse::<Token![;]>()?;
317    Ok(PoolItem {
318        cfg,
319        ident,
320        modes,
321        deps,
322        spawn,
323        policy,
324        policy_ty,
325        executor,
326        min,
327        max,
328    })
329}
330
331/// The node/pool name string: ident lowercased with `_`→`-` (`WIFI_CTRL` → "wifi-ctrl").
332fn name_string(ident: &Ident) -> String {
333    ident.to_string().to_lowercase().replace('_', "-")
334}
335
336/// Build a task-call expression with `node_ref` injected as the **first** argument:
337/// a bare path `f` => `f(node_ref)`; a partial call `f(a, b)` => `f(node_ref, a, b)`.
338/// The task fn is thus expected to take the node/`&POOL[i]` first, then any extra args.
339fn inject_node_call(task: &Expr, node_ref: &TokenStream2) -> SynResult<TokenStream2> {
340    match task {
341        Expr::Path(_) => Ok(quote!(#task(#node_ref))),
342        Expr::Call(c) => {
343            let f = &c.func;
344            let args = c.args.iter();
345            Ok(quote!(#f(#node_ref #(, #args)*)))
346        }
347        other => Err(syn::Error::new_spanned(
348            other,
349            "expected a task-fn path or a partial call like `f(extra_args)`",
350        )),
351    }
352}
353
354/// Combine an item's `#[cfg(...)]` attributes into one predicate (`all(..)` if
355/// several), used to gate its `GRAPH.nodes` slot to `Some`/`None`. `None` = always present.
356fn cfg_predicate(attrs: &[Attribute]) -> Option<TokenStream2> {
357    let preds: Vec<TokenStream2> = attrs
358        .iter()
359        .filter_map(|a| match &a.meta {
360            Meta::List(ml) if ml.path.is_ident("cfg") => Some(ml.tokens.clone()),
361            _ => None,
362        })
363        .collect();
364    match preds.len() {
365        0 => None,
366        1 => Some(preds[0].clone()),
367        _ => Some(quote!(all(#(#preds),*))),
368    }
369}
370
371/// Extract the policy *type* from a `Type::new(..)` constructor expression. Only used
372/// on the derive path (no explicit `policy: <Ty> = ..` annotation); the type is the
373/// call's path minus its last segment (`DeferredShrink::new` -> `DeferredShrink`).
374fn policy_type(expr: &Expr) -> SynResult<Path> {
375    if let Expr::Call(call) = expr
376        && let Expr::Path(p) = &*call.func
377    {
378        let n = p.path.segments.len();
379        if n >= 2 {
380            let segs: Punctuated<_, Token![::]> =
381                p.path.segments.iter().take(n - 1).cloned().collect();
382            return Ok(Path {
383                leading_colon: p.path.leading_colon,
384                segments: segs,
385            });
386        }
387    }
388    Err(syn::Error::new_spanned(
389        expr,
390        "pool `policy:` must be a `Type::new(..)` constructor (e.g. `DeferredShrink::new(..)`), \
391         or give the type explicitly: `policy: <Type> = <expr>`",
392    ))
393}
394
395/// One emitted node slot, in final index order.
396struct Slot {
397    /// Presence predicate (`None` = unconditional), gates the node slot (`GRAPH.nodes`) entry.
398    cfg_pred: Option<TokenStream2>,
399    /// `&NODE` or `&POOL[j]`.
400    reference: TokenStream2,
401    /// Raw deps, resolved to indices in the second pass.
402    deps: Vec<Dep>,
403}
404
405/// The `Option<fn(..)>` spawn expression for a node. `None` (no `spawn:`) is a
406/// parked node the app spawns itself. A path or partial call is a task fn taking
407/// `&NODE` first (plus any given args); the macro wraps it as
408/// `|s| { s.spawn(<task>(&NODE, ..)?); Ok(()) }`. Anything else (a closure, or a
409/// ready spawn fn) is emitted verbatim. Every form is cast to `spawn_fn` so it
410/// coerces cleanly inside `Option::Some(..)`.
411fn node_spawn(
412    ident: &Ident,
413    spawn: &Option<Expr>,
414    executor: &Option<Ident>,
415    spawn_fn: &TokenStream2,
416) -> SynResult<TokenStream2> {
417    Ok(match (spawn, executor) {
418        (None, None) => quote!(::core::option::Option::None),
419        // `executor:` needs the macro to perform the spawn, so it composes only
420        // with the path / partial-call `spawn:` forms below.
421        (None, Some(ex)) => {
422            return Err(syn::Error::new_spanned(
423                ex,
424                "`executor:` requires a `spawn:` (a parked node is spawned by the \
425                 application, which picks its own spawner)",
426            ));
427        }
428        // A path or a partial call: a task fn taking `&NODE` first (plus any
429        // given args); generate `|s| { s.spawn(<task>(&NODE, ..)?); Ok(()) }`.
430        // With `executor: NAME` the glue ignores the supervisor's `Spawner` and
431        // spawns through the named `SpawnerSlot` (a `SendSpawner` the app
432        // registers at runtime): an unfilled slot fails the spawn with
433        // `SpawnError::Busy` — loud misconfiguration, not a missing task. The
434        // task future must then be `Send` (enforced by `SendSpawner::spawn`).
435        (Some(e @ (Expr::Path(_) | Expr::Call(_))), executor) => {
436            let call = inject_node_call(e, &quote!(&#ident))?;
437            match executor {
438                None => {
439                    let stmts = spawn_stmts(&call, &quote!(&#ident), &quote!(s));
440                    quote!(::core::option::Option::Some(
441                        (|s| {
442                            #stmts
443                            ::core::result::Result::Ok(())
444                        }) as #spawn_fn
445                    ))
446                }
447                Some(ex) => {
448                    let stmts = spawn_stmts(&call, &quote!(&#ident), &quote!(__sp));
449                    quote!(::core::option::Option::Some(
450                        (|_s| {
451                            // The supervisor awaits this slot's `ready()` before
452                            // invoking the glue (the node carries `.with_executor(&EX)`
453                            // and the bring-up bounds the wait), so `get()` is already
454                            // filled; `ok_or` is the belt-and-braces unfilled guard.
455                            let __sp = #ex
456                                .get()
457                                .ok_or(::embassy_executor::SpawnError::Busy)?;
458                            #stmts
459                            ::core::result::Result::Ok(())
460                        }) as #spawn_fn
461                    ))
462                }
463            }
464        }
465        (Some(_), Some(ex)) => {
466            return Err(syn::Error::new_spanned(
467                ex,
468                "`executor:` cannot be combined with a verbatim spawn closure (the \
469                 closure owns the spawn; use the named SpawnerSlot inside it instead)",
470            ));
471        }
472        // Anything else (a closure, or a ready spawn fn) is emitted verbatim.
473        // NOTE: with the `trace` feature such a node is not auto-mapped — the
474        // closure owns the SpawnToken; call `adopt`/`set_task_id` in it yourself.
475        (Some(e), None) => quote!(::core::option::Option::Some((#e) as #spawn_fn)),
476    })
477}
478
479/// The spawn statement(s) for the generated glue. Plain `s.spawn(<call>?)`
480/// normally; with the `trace` feature the `SpawnToken` is bound first so its task
481/// id can be captured into the node (`set_task_id`) — the id→node mapping the
482/// supervisor's `trace` recorders resolve against (in embassy-executor 0.10 the
483/// task-fn call returns `Result<SpawnToken, SpawnError>` and `Spawner::spawn`
484/// itself is infallible, so the token is available between the two). Under
485/// `trace-names` the node's name is also stamped into the task Metadata so
486/// external consumers (rtos-trace/SystemView) see names instead of raw ids.
487fn spawn_stmts(call: &TokenStream2, node_ref: &TokenStream2, sp: &TokenStream2) -> TokenStream2 {
488    if cfg!(feature = "trace") {
489        // `adopt` = set_task_id + (under trace-names) Metadata name stamp.
490        quote! {
491            let __token = #call?;
492            (#node_ref).adopt(&__token);
493            #sp.spawn(__token);
494        }
495    } else {
496        quote!(#sp.spawn(#call?);)
497    }
498}
499
500/// Emit a `node`: its `pub static #ident: TaskNode` definition and its `Slot`. The
501/// caller assigns the slot index and records the name, so this touches neither.
502fn emit_node(
503    n: &NodeItem,
504    cr: &TokenStream2,
505    spawn_fn: &TokenStream2,
506) -> SynResult<(TokenStream2, Slot)> {
507    let ident = &n.ident;
508    let cfg = &n.cfg;
509    let mode = &n.mode;
510    let name = name_string(&n.ident);
511    let disabled = n.disabled;
512    let spawn = node_spawn(ident, &n.spawn, &n.executor, spawn_fn)?;
513    // `executor: NAME` routes the node through that SpawnerSlot; the supervisor
514    // awaits the slot before spawning (see `TaskNode::with_executor`).
515    let with_exec = match &n.executor {
516        Some(ex) => quote!( .with_executor(&#ex) ),
517        None => quote!(),
518    };
519    let def = quote! {
520        #(#cfg)*
521        pub static #ident: #cr::TaskNode =
522            #cr::TaskNode::new(#name, #cr::Mode::#mode, #spawn, #disabled) #with_exec;
523    };
524    let slot = Slot {
525        cfg_pred: cfg_predicate(cfg),
526        reference: quote!(&#ident),
527        deps: n.deps.clone(),
528    };
529    Ok((def, slot))
530}
531
532/// Emit a `pool`: the member `[TaskNode; K]` array, the `spawn_<pool>` glue fn, and
533/// the `ElasticPool` static (returned as `defs`, in that emission order), plus the
534/// pool-registry entry (for `GRAPH.pools`) and one `Slot` per member (members occupy
535/// slots but aren't name-addressable, so no name is recorded).
536fn emit_pool(
537    p: &PoolItem,
538    cr: &TokenStream2,
539    spawn_fn: &TokenStream2,
540) -> SynResult<(Vec<TokenStream2>, TokenStream2, Vec<Slot>)> {
541    let ident = &p.ident;
542    let cfg = &p.cfg;
543    let lname = name_string(&p.ident);
544    let pool_static = format_ident!("{}_POOL", ident);
545    let k = p.modes.len();
546
547    // Validate the scaling bounds at expansion time. `base10_parse::<u8>` also
548    // rejects values > 255 with a span-attached error (the `ElasticPool` fields are
549    // `u8`). `min > max` makes the policy contradict itself; `max > k` is a ceiling
550    // the pool can never reach (there are only `k` member slots) — both are
551    // declaration bugs, caught here rather than surfacing as odd runtime scaling.
552    // `max < k` is allowed (spare declared members below the ceiling), as is
553    // `min: 0` (the policy may scale the pool all the way down when idle).
554    let min_v: u8 = p.min.base10_parse()?;
555    let max_v: u8 = p.max.base10_parse()?;
556    if min_v > max_v {
557        return Err(syn::Error::new_spanned(
558            &p.min,
559            format!("pool `min:` ({min_v}) must not exceed `max:` ({max_v})"),
560        ));
561    }
562    if usize::from(max_v) > k {
563        return Err(syn::Error::new_spanned(
564            &p.max,
565            format!("pool `max:` ({max_v}) exceeds the declared member count ({k})"),
566        ));
567    }
568
569    // Build member `I`'s spawn call from the `spawn:` expr, injecting
570    // `&POOL[I]` as the first argument (see `inject_node_call`).
571    let call = inject_node_call(&p.spawn, &quote!(&#ident[I]))?;
572    // Per-member spawn fn: a generated `spawn_<pool>::<I>` wrapper. Same optional
573    // trace capture as a node's closure, against member `I`'s slot. With
574    // `executor: NAME` the wrapper ignores the supervisor's `Spawner` and spawns
575    // through the named SpawnerSlot; each member node carries `.with_executor(&EX)`,
576    // so the supervisor awaits the slot (bounded) before invoking the wrapper and
577    // the wrapper's `get()` is already filled (`SpawnError::Busy` guards a never-
578    // filled slot; member futures must be `Send`). A whole worker pool can thus live
579    // on another executor — e.g. the second core — while this core scales it.
580    let (param, prelude, sp_tokens) = match &p.executor {
581        None => (quote!(s), quote!(), quote!(s)),
582        Some(ex) => (
583            quote!(_s),
584            quote! {
585                let __sp = #ex
586                    .get()
587                    .ok_or(::embassy_executor::SpawnError::Busy)?;
588            },
589            quote!(__sp),
590        ),
591    };
592    let pool_spawn_stmts = spawn_stmts(&call, &quote!(&#ident[I]), &sp_tokens);
593    let wrapper = format_ident!("spawn_{}", lname);
594    let mut defs: Vec<TokenStream2> = Vec::new();
595    defs.push(quote! {
596        #(#cfg)*
597        fn #wrapper<const I: usize>(
598            #param: ::embassy_executor::Spawner,
599        ) -> ::core::result::Result<(), ::embassy_executor::SpawnError> {
600            #prelude
601            #pool_spawn_stmts
602            ::core::result::Result::Ok(())
603        }
604    });
605    let member_spawn: Vec<TokenStream2> = (0..k).map(|j| quote!(#wrapper::<#j>)).collect();
606
607    // `executor: NAME` on the pool routes every member through that SpawnerSlot; the
608    // supervisor awaits it before spawning each member (see `TaskNode::with_executor`).
609    let member_with_exec = match &p.executor {
610        Some(ex) => quote!( .with_executor(&#ex) ),
611        None => quote!(),
612    };
613    let members = p
614        .modes
615        .iter()
616        .zip(&member_spawn)
617        .enumerate()
618        .map(|(j, (mode, sp))| {
619            let nm = format!("{lname}{j}");
620            quote! {
621                #cr::TaskNode::new(
622                    #nm, #cr::Mode::#mode,
623                    ::core::option::Option::Some((#sp) as #spawn_fn), false,
624                ) #member_with_exec
625            }
626        });
627    defs.push(quote! {
628        #(#cfg)*
629        pub static #ident: [#cr::TaskNode; #k] = [ #(#members),* ];
630    });
631
632    let member_refs = (0..k).map(|j| quote!(&#ident[#j]));
633    let policy = &p.policy;
634    // The `ElasticPool<P>` type argument: honor an explicit `policy: <Ty> = ..`
635    // annotation, else derive `P` from the constructor expr (`Ty::new(..)` shape).
636    let policy_ty = match &p.policy_ty {
637        Some(ty) => quote!(#ty),
638        None => {
639            let path = policy_type(policy)?;
640            quote!(#path)
641        }
642    };
643    // Emit the *validated* u8 values (`min_v`/`max_v`), not the raw literals: a
644    // suffixed literal like `min: 3usize` parses as u8 above but would emit a
645    // mismatched-type rustc error into the u8 field.
646    defs.push(quote! {
647        #(#cfg)*
648        pub static #pool_static: #cr::ElasticPool<#policy_ty> = #cr::ElasticPool {
649            nodes: &[ #(#member_refs),* ],
650            min: #min_v,
651            max: #max_v,
652            policy: #policy,
653        };
654    });
655
656    let pool_entry = quote!( #(#cfg)* &#pool_static );
657
658    let pred = cfg_predicate(cfg);
659    let slots = (0..k)
660        .map(|j| Slot {
661            cfg_pred: pred.clone(),
662            reference: quote!(&#ident[#j]),
663            deps: p.deps.clone(),
664        })
665        .collect();
666
667    Ok((defs, pool_entry, slots))
668}
669
670/// Second pass: build the node-slot entries for `GRAPH.nodes` (`Option`, cfg-gated) and
671/// the cfg-aware dep-index entries for `GRAPH.deps`. Runs after every slot + name is
672/// known, since a dep may forward-reference a node declared later. An unknown dep name
673/// is a compile error.
674fn slot_tables(
675    slots: &[Slot],
676    names: &HashMap<String, usize>,
677) -> SynResult<(Vec<TokenStream2>, Vec<TokenStream2>)> {
678    let mut all_entries: Vec<TokenStream2> = Vec::new();
679    let mut deps_entries: Vec<TokenStream2> = Vec::new();
680    for slot in slots {
681        let reference = &slot.reference;
682        all_entries.push(match &slot.cfg_pred {
683            None => quote!(::core::option::Option::Some(#reference)),
684            Some(pred) => quote!({
685                #[cfg(#pred)]
686                { ::core::option::Option::Some(#reference) }
687                #[cfg(not(#pred))]
688                { ::core::option::Option::None }
689            }),
690        });
691
692        let mut dep_toks: Vec<TokenStream2> = Vec::new();
693        // Duplicate deps are a compile error: `deps: [A, A]` would emit a doubled
694        // index, which `topo_sort_const` counts twice in the in-degree but decrements
695        // once — misreported as a dependency cycle. Compared by *resolved* slot index
696        // (so a repeated pool name trips it too); two cfg-gated variants of the same
697        // dep are allowed only when their cfg predicates differ.
698        let mut seen: Vec<(u8, String)> = Vec::new();
699        for d in &slot.deps {
700            let idx = match names.get(&d.ident.to_string()) {
701                Some(&i) => i as u8,
702                None => {
703                    return Err(syn::Error::new_spanned(
704                        &d.ident,
705                        format!(
706                            "unknown dependency `{}` — not a declared node or pool",
707                            d.ident
708                        ),
709                    ));
710                }
711            };
712            let cfg = &d.cfg;
713            let cfg_key = quote!( #(#cfg)* ).to_string();
714            if seen.iter().any(|(i, k)| *i == idx && *k == cfg_key) {
715                return Err(syn::Error::new_spanned(
716                    &d.ident,
717                    format!("duplicate dependency `{}`", d.ident),
718                ));
719            }
720            seen.push((idx, cfg_key));
721            dep_toks.push(quote!( #(#cfg)* #idx ));
722        }
723        deps_entries.push(quote!( &[ #(#dep_toks),* ] ));
724    }
725    Ok((all_entries, deps_entries))
726}
727
728fn expand(graph: GraphSpec) -> SynResult<TokenStream2> {
729    let cr = quote!(::embassy_supervisor);
730    // The node spawn fn-pointer type. Spawn exprs (closures / const-generic fns) are
731    // cast to this so they coerce cleanly inside `Option::Some(..)`.
732    let spawn_fn = quote!(
733        fn(
734            ::embassy_executor::Spawner,
735        ) -> ::core::result::Result<(), ::embassy_executor::SpawnError>
736    );
737
738    // First pass: emit the statics/glue in declaration order, assign stable slot
739    // indices, and record each slot + its raw deps. `names` maps a dep-addressable ident
740    // to its slot index for dep resolution — keyed on the *raw* ident (not the runtime
741    // `name_string`). A `node` maps to its own slot; a `pool` maps to its floor member's
742    // slot (so `deps: [POOL]` = "after the pool is up"). Individual pool members are not
743    // separately name-addressable.
744    let mut defs: Vec<TokenStream2> = Vec::new();
745    let mut pool_entries: Vec<TokenStream2> = Vec::new();
746    let mut slots: Vec<Slot> = Vec::new();
747    let mut names: HashMap<String, usize> = HashMap::new();
748
749    // Pre-pass: collect the declared `executor NAME;` slots so a node's
750    // `executor:` reference can be validated regardless of declaration order.
751    let executor_names: Vec<String> = graph
752        .items
753        .iter()
754        .filter_map(|i| match i {
755            Item::Executor(x) => Some(x.ident.to_string()),
756            _ => None,
757        })
758        .collect();
759
760    for item in &graph.items {
761        match item {
762            Item::Node(n) => {
763                if let Some(ex) = &n.executor
764                    && !executor_names.contains(&ex.to_string())
765                {
766                    return Err(syn::Error::new_spanned(
767                        ex,
768                        format!(
769                            "unknown executor `{ex}`; declare it in the graph with \
770                             `executor {ex};` (declared: [{}])",
771                            executor_names.join(", ")
772                        ),
773                    ));
774                }
775                // The index is the slot's position, taken *before* the push.
776                // A redeclared name is a hard error here (not just the downstream
777                // `duplicate definition of static`): deps resolve through this map,
778                // so a silent overwrite would silently rewire earlier `deps:` edges.
779                if names.insert(n.ident.to_string(), slots.len()).is_some() {
780                    return Err(syn::Error::new_spanned(
781                        &n.ident,
782                        format!("duplicate node/pool name `{}`", n.ident),
783                    ));
784                }
785                let (def, slot) = emit_node(n, &cr, &spawn_fn)?;
786                defs.push(def);
787                slots.push(slot);
788            }
789            Item::Executor(x) => {
790                let (cfg, ident) = (&x.cfg, &x.ident);
791                // A runtime-filled SendSpawner slot: the app registers the
792                // executor's spawner before `Supervisor::start`; nodes declared
793                // `executor: NAME` spawn through it. Occupies no graph slot.
794                defs.push(quote! {
795                    #(#cfg)*
796                    /// Spawner slot for the graph's `executor:`-annotated nodes
797                    /// (generated by `supervisor_graph!`). Fill with
798                    /// `SpawnerSlot::set` before `Supervisor::start`.
799                    pub static #ident: #cr::SpawnerSlot = #cr::SpawnerSlot::new();
800                });
801            }
802            Item::Pool(p) => {
803                // Pools are only meaningful with the supervisor's `pool` feature (which
804                // forwards to this crate). Without it, `Graph` has no `pools` field and
805                // `ElasticPool` doesn't exist — so refuse a `pool` with a clear message
806                // rather than emitting dangling references.
807                if cfg!(feature = "pool") {
808                    if let Some(ex) = &p.executor
809                        && !executor_names.contains(&ex.to_string())
810                    {
811                        return Err(syn::Error::new_spanned(
812                            ex,
813                            format!(
814                                "unknown executor `{ex}`; declare it in the graph with \
815                                 `executor {ex};` (declared: [{}])",
816                                executor_names.join(", ")
817                            ),
818                        ));
819                    }
820                    let (pool_defs, pool_entry, pool_slots) = emit_pool(p, &cr, &spawn_fn)?;
821                    // A dep on the pool NAME resolves to the pool's floor member (member 0
822                    // — the `min`-kept, always-started member): `deps: [POOL]` means "after
823                    // the pool is up". `slots.len()` here is that member's slot index, taken
824                    // *before* the extend below (pool_slots[0] lands at exactly this index).
825                    // A redeclared name errors, same as the node arm.
826                    if names.insert(p.ident.to_string(), slots.len()).is_some() {
827                        return Err(syn::Error::new_spanned(
828                            &p.ident,
829                            format!("duplicate node/pool name `{}`", p.ident),
830                        ));
831                    }
832                    defs.extend(pool_defs);
833                    pool_entries.push(pool_entry);
834                    slots.extend(pool_slots);
835                } else {
836                    return Err(syn::Error::new_spanned(
837                        &p.ident,
838                        "a `pool` requires enabling embassy-supervisor's `pool` feature",
839                    ));
840                }
841            }
842        }
843    }
844
845    let m = slots.len();
846    // Every graph index (dep entries, `topo_sort_const`'s queue/order) is a `u8`, so
847    // more than 256 slots would silently truncate (`i as u8`) and corrupt the order.
848    // 256 slots means max index 255 and max per-node dep count 255 — both fit exactly.
849    if m > 256 {
850        return Err(syn::Error::new(
851            proc_macro2::Span::call_site(),
852            format!(
853                "supervisor_graph!: {m} node slots declared, but at most 256 are supported \
854                 (including pool members) — graph indices are `u8`"
855            ),
856        ));
857    }
858    let (all_entries, deps_entries) = slot_tables(&slots, &names)?;
859
860    // `Graph.pools` is `#[cfg(feature = "pool")]`; emit that field iff this macro was
861    // built with pool support (forwarded from the supervisor's `pool` feature).
862    let pools_field = if cfg!(feature = "pool") {
863        quote!( pools: &[ #(#pool_entries),* ], )
864    } else {
865        quote!()
866    };
867
868    // embassy-executor's trace hooks (declared `unsafe extern "Rust"` in the
869    // executor), defined once here at the graph declaration site — the supervisor
870    // crate is `forbid(unsafe_code)` and cannot carry `#[unsafe(no_mangle)]` items.
871    // They forward to the supervisor's `trace` recorders. `task_new` and
872    // `task_ready_begin` carry nothing the recorders need (the id→node mapping
873    // comes from the spawn glue above), so they are no-ops. Exactly one definition
874    // of each may exist per binary: enable `trace-hooks` OR write your own set.
875    // Requires an edition-2024 consumer (`#[unsafe(no_mangle)]` syntax).
876    let trace_hooks = if cfg!(feature = "trace-hooks") {
877        quote! {
878            #[unsafe(no_mangle)]
879            fn _embassy_trace_poll_start(executor_id: u32) {
880                #cr::trace::on_poll_start(executor_id);
881            }
882            #[unsafe(no_mangle)]
883            fn _embassy_trace_task_new(_executor_id: u32, _task_id: u32) {}
884            #[unsafe(no_mangle)]
885            fn _embassy_trace_task_end(executor_id: u32, task_id: u32) {
886                #cr::trace::on_task_end(executor_id, task_id);
887            }
888            #[unsafe(no_mangle)]
889            fn _embassy_trace_task_exec_begin(executor_id: u32, task_id: u32) {
890                #cr::trace::on_task_exec_begin(executor_id, task_id);
891            }
892            #[unsafe(no_mangle)]
893            fn _embassy_trace_task_exec_end(executor_id: u32, task_id: u32) {
894                #cr::trace::on_task_exec_end(executor_id, task_id);
895            }
896            #[unsafe(no_mangle)]
897            fn _embassy_trace_task_ready_begin(_executor_id: u32, _task_id: u32) {}
898            #[unsafe(no_mangle)]
899            fn _embassy_trace_executor_idle(executor_id: u32) {
900                #cr::trace::on_executor_idle(executor_id);
901            }
902        }
903    } else {
904        quote!()
905    };
906
907    Ok(quote! {
908        #(#defs)*
909
910        // Private backing tables — the application uses `GRAPH`. The topological order
911        // and pools are inlined into the `GRAPH` literal below; the node count is
912        // `GRAPH.nodes.len()`.
913        static NODES: [::core::option::Option<&'static #cr::TaskNode>; #m] = [ #(#all_entries),* ];
914        const DEPS: [&'static [u8]; #m] = [ #(#deps_entries),* ];
915
916        /// The compile-time task graph — node slots, dependency table, topological order,
917        /// and (with the `pool` feature) the elastic pools. Pass to `Supervisor::new`.
918        pub static GRAPH: #cr::Graph<#m> = #cr::Graph {
919            nodes: &NODES,
920            deps: &DEPS,
921            order: #cr::topo_sort_const(&DEPS),
922            #pools_field
923        };
924
925        #trace_hooks
926    })
927}
928
929/// Declare a supervised task graph; see the crate docs for the surface syntax.
930#[proc_macro]
931pub fn supervisor_graph(input: TokenStream) -> TokenStream {
932    let graph = syn::parse_macro_input!(input as GraphSpec);
933    expand(graph)
934        .unwrap_or_else(syn::Error::into_compile_error)
935        .into()
936}