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, B], task: <worker>[, pool_size: N][, executor: EXEC]
12//!     [, resources: [RES: Type, ..]][, disabled];
13//! node NAME = Mode, deps: [A];                 // neither => a parked node the app spawns
14//! executor EXEC;                               // runtime-filled SendSpawner slot
15//! pool NAME = [Mode, ..], deps: [A][, executor: EXEC], spawn: <fn> | task: <worker>,
16//!     policy: [<Ty> =] <expr>, min: N, max: M;
17//! ```
18//! `deps:` entries name a `node` or a `pool`; a `pool` dep resolves to that pool's floor
19//! member (member 0, the `min`-kept one), i.e. "start after the pool is up". A repeated
20//! dep or a redeclared node/pool name is a compile error.
21//!
22//! An `executor NAME;` slot may carry `#[cfg(...)]`, but validation does not model cfg
23//! predicates: a node referencing a slot that is cfg'd *out* while the node is cfg'd
24//! *in* surfaces as rustc's `cannot find value NAME`, not a macro error — don't gate an
25//! executor slot more restrictively than the nodes that reference it.
26//! `executor EXEC;` emits a `pub static EXEC: SpawnerSlot`; the app fills it with a
27//! `SendSpawner` (`InterruptExecutor::start`, `Spawner::make_send`) before
28//! `Supervisor::start`, and nodes carrying `executor: EXEC` spawn through it instead
29//! of the supervisor's own executor (their futures must be `Send`; an unfilled slot
30//! fails the spawn with `SpawnError::Busy`).
31//! A pool is emitted as `ElasticPool<P>`, so the macro needs the policy type `P`. By
32//! default it derives `P` from a `Ty::new(..)`-shaped `policy:` value (e.g.
33//! `DeferredShrink::new(..)` => `P = DeferredShrink`). Give `policy: <Ty> = <expr>` to
34//! state `P` explicitly when the value isn't that shape — a const, a free fn, a builder
35//! chain (`X::new(..).with(..)`), or a qualified path.
36//! `spawn:` takes a path or a partial call to a task fn taking the node **first**
37//! (`spawn: f` => `s.spawn(f(&NAME)?)`; `spawn: f(a)` => `s.spawn(f(&NAME, a)?)`), or,
38//! for a node, a closure / ready spawn fn emitted verbatim (for anything that doesn't
39//! fit that shape). A pool's `spawn:` is the same path/partial-call form with `&POOL[j]`
40//! injected first, via a generated `spawn_<pool>::<j>` glue fn; a pool has no closure
41//! form (members are instantiated per index).
42//!
43//! `task:` takes the same path/partial-call forms but names a **plain async worker
44//! fn** — possibly generic (turbofish or inferred) — instead of a hand-written
45//! `#[embassy_executor::task]`. The macro stamps a concrete shell task per
46//! declaration (embassy forbids generic tasks: one static `TaskPool` per concrete
47//! future type), sized by `pool_size:` on a node (default 1) or by the member count
48//! on a pool. Worker args are evaluated **inside the shell** — at the task's first
49//! poll, on the node's own executor — so cross-node data should go through awaited
50//! accessors, and a cross-core node builds its resources on its own core. `task:`
51//! and `spawn:` are mutually exclusive; `pool_size:` requires `task:`.
52//!
53//! **Prefer `task:`** — no attribute boilerplate, generic workers, auto-sized pool
54//! shells, and it is the only form supporting `resources:`; the shell inlines into
55//! the same poll and its `TaskPool` replaces the one the attribute would emit.
56//! `spawn:` remains for: a fn that already carries `#[embassy_executor::task]` and
57//! can't be de-attributed (another crate); a task also spawned outside the graph
58//! (sharing its one `TaskPool` instead of duplicating it as a shell); the verbatim
59//! closure form (custom spawn-time logic); and args that must be evaluated at
60//! spawn time on the supervisor's executor rather than at the shell's first poll.
61//! Worked examples: README "`spawn:` vs `task:` — which to use".
62//!
63//! `resources: [RES: Type, ..]` (requires `task:`; node-only) threads **owned
64//! resources from `main`** into the worker instead of re-acquiring them inside the
65//! task (`Peripherals::steal()`). Each entry emits a
66//! `pub static RES: ResourceSlot<Type>` at the declaration site; `main` moves the
67//! resource in with `RES.provide(..)` (consuming the `Peripherals` field — the
68//! compile-time exclusive-ownership guarantee), the generated glue `take()`s it just
69//! before the spawn (an unprovided slot fails `Supervisor::start` with
70//! `SpawnError::Busy` after a bounded wait — fail-closed, not a task-side panic),
71//! and the shell passes the worker `&mut Type` (after the node arg, in declared
72//! order, before any partial-call extras) and `restore()`s the value after the
73//! worker returns, so a Terminate respawn re-takes the *same instance*. Slot names
74//! are statics: unique across the graph. Pools reject `resources:` (members would
75//! contend for one instance).
76//!
77//! A graph holds at most **256 node slots** (including pool members): all graph
78//! indices are `u8`, and the macro rejects a larger declaration at expansion.
79//!
80//! Nodes, pools, and individual deps may carry `#[cfg(...)]` attributes. A
81//! proc-macro can't evaluate `cfg`, so the node array is a fixed-length
82//! `[Option<&TaskNode>; M]` over all declared slots (each entry `Some`/`None` via a
83//! cfg-expression), the dep table is cfg-aware per-dep, and the order runs through
84//! `topo_sort_const` at const-eval (after cfg). Absent nodes are skipped at runtime.
85//!
86//! Generated items (at the call site): one `pub static` per `node`, a `[TaskNode; K]`
87//! array + `spawn_<pool>` glue fn + `<POOL>_POOL` `ElasticPool` + the structural
88//! `pub const`s `<POOL>_MIN` / `<POOL>_MAX` / `<POOL>_MEMBERS` (usize; for
89//! const-context sizing downstream — a `const` cannot read them off the member
90//! `static`) per `pool`, plus a
91//! single `pub static GRAPH: Graph<M>` bundling the node slots, the dependency table,
92//! the topological order, and (with the `pool` feature) the pools — pass `&GRAPH` to
93//! `Supervisor::new`. The backing tables are private; read them through `GRAPH.nodes`
94//! / `GRAPH.deps` / `GRAPH.order` / `GRAPH.pools` (node count is `GRAPH.nodes.len()`).
95//!
96//! With the supervisor's `trace` feature (forwarded here) the generated spawn glue
97//! also captures each `SpawnToken`'s task id into its node (`set_task_id`); with
98//! `metadata-names` it stamps the node name into the task Metadata. These are
99//! independent: `metadata-names` without `trace` emits a name-only spawn path
100//! (`stamp_name`, no id capture, no `_embassy_trace_*` dependency), so node names
101//! reach external tooling (rtos-trace/SystemView) without the trace recorders.
102//! With `trace-hooks` the macro additionally defines the seven `_embassy_trace_*`
103//! hook symbols at the declaration site (the supervisor crate is
104//! `forbid(unsafe_code)` and cannot), forwarding to the supervisor's `trace`
105//! recorders — requires an edition-2024 consumer, and exactly one graph declaration
106//! (or hook set) per binary.
107//!
108//! Types are referenced absolutely (`::embassy_supervisor::…`), so the consuming
109//! crate must depend on `embassy-supervisor` under its real name (not aliased).
110
111use proc_macro::TokenStream;
112use proc_macro2::TokenStream as TokenStream2;
113use quote::{format_ident, quote};
114use std::collections::{HashMap, HashSet};
115use syn::parse::{Parse, ParseStream};
116use syn::punctuated::Punctuated;
117use syn::{
118    Attribute, Expr, Ident, LitInt, Meta, Path, Result as SynResult, Token, Type, bracketed,
119};
120
121mod kw {
122    syn::custom_keyword!(node);
123    syn::custom_keyword!(pool);
124    syn::custom_keyword!(deps);
125    syn::custom_keyword!(spawn);
126    syn::custom_keyword!(task);
127    syn::custom_keyword!(pool_size);
128    syn::custom_keyword!(policy);
129    syn::custom_keyword!(min);
130    syn::custom_keyword!(max);
131    syn::custom_keyword!(disabled);
132    syn::custom_keyword!(executor);
133    syn::custom_keyword!(resources);
134}
135
136/// A dependency reference: a node ident, optionally `#[cfg(...)]`-gated.
137#[derive(Clone)]
138struct Dep {
139    cfg: Vec<Attribute>,
140    ident: Ident,
141}
142
143/// `deps: [a, #[cfg(feature = "x")] b, …]`
144fn parse_dep_list(input: ParseStream) -> SynResult<Vec<Dep>> {
145    let content;
146    bracketed!(content in input);
147    let mut deps = Vec::new();
148    while !content.is_empty() {
149        let cfg = content.call(Attribute::parse_outer)?;
150        let ident: Ident = content.parse()?;
151        deps.push(Dep { cfg, ident });
152        if content.peek(Token![,]) {
153            content.parse::<Token![,]>()?;
154        }
155    }
156    Ok(deps)
157}
158
159/// `[Terminate, OnDemand, …]` — the bracketed mode list.
160fn parse_mode_list(input: ParseStream) -> SynResult<Vec<Ident>> {
161    let content;
162    bracketed!(content in input);
163    let punct = Punctuated::<Ident, Token![,]>::parse_terminated(&content)?;
164    Ok(punct.into_iter().collect())
165}
166
167/// How a node/pool member gets its task: `spawn:` names a hand-written
168/// `#[embassy_executor::task]` fn (path / partial call / verbatim closure), while
169/// `task:` names a **plain async fn** — possibly generic — for which the macro
170/// emits a concrete `#[embassy_executor::task]` shell (embassy forbids generic
171/// tasks: one static `TaskPool` per concrete future type, so per-type shells are
172/// the only way — the macro stamps them so the user doesn't).
173enum TaskSource {
174    /// `spawn: <expr>` — the expr *is* (or produces) the task fn.
175    Spawn(Expr),
176    /// `task: <path | partial call>` — wrap in a generated shell; args are
177    /// evaluated inside the shell (at the task's first poll, on its own executor).
178    Shell(Expr),
179}
180
181/// One `NAME: Type` entry of a `resources:` clause. The macro emits a
182/// `pub static NAME: ResourceSlot<Type>` at the declaration site; `main` moves
183/// the resource in with `NAME.provide(..)` (consuming the `Peripherals` field —
184/// the compile-time ownership guarantee), the generated spawn glue `take()`s it
185/// before the spawn, and the generated shell `restore()`s it after the worker
186/// returns so a respawn re-takes the same instance.
187struct ResourceDecl {
188    ident: Ident,
189    ty: Type,
190}
191
192/// `resources: [LED: Output<'static>, …]`
193fn parse_resource_list(input: ParseStream) -> SynResult<Vec<ResourceDecl>> {
194    let content;
195    bracketed!(content in input);
196    let mut resources = Vec::new();
197    while !content.is_empty() {
198        let ident: Ident = content.parse()?;
199        content.parse::<Token![:]>()?;
200        let ty: Type = content.parse()?;
201        resources.push(ResourceDecl { ident, ty });
202        if content.peek(Token![,]) {
203            content.parse::<Token![,]>()?;
204        }
205    }
206    Ok(resources)
207}
208
209struct NodeItem {
210    cfg: Vec<Attribute>,
211    ident: Ident,
212    mode: Ident,
213    deps: Vec<Dep>,
214    /// `None` = a parked node the app spawns itself (neither `spawn:` nor `task:`).
215    source: Option<TaskSource>,
216    /// `pool_size: N` on a `task:` node — sizes the generated shell's `TaskPool`
217    /// (headroom for a respawn while the previous instance is still draining).
218    pool_size: Option<LitInt>,
219    /// `resources: [NAME: Type, ..]` on a `task:` node — owned values threaded
220    /// from `main` through macro-emitted `ResourceSlot` statics into the
221    /// generated shell (which hands the worker `&mut Type` and restores the
222    /// value on exit). Empty for `spawn:`/parked nodes (enforced at parse).
223    resources: Vec<ResourceDecl>,
224    disabled: bool,
225    /// `executor: NAME` — spawn through the named [`SpawnerSlot`] (a
226    /// `SendSpawner` the app registers at runtime) instead of the supervisor's
227    /// own `Spawner`. `None` = the default executor.
228    executor: Option<Ident>,
229}
230
231/// `executor NAME;` — declares a `pub static NAME: SpawnerSlot` the application
232/// fills with a `SendSpawner` before (or concurrently with) `Supervisor::start` (an
233/// InterruptExecutor tier, core1, ...). Nodes reference it with `executor: NAME`; the
234/// supervisor awaits the slot before spawning such a node (bounded by
235/// `SLOT_READY_TIMEOUT`, then `SpawnError::Busy`).
236struct ExecutorItem {
237    cfg: Vec<Attribute>,
238    ident: Ident,
239}
240
241struct PoolItem {
242    cfg: Vec<Attribute>,
243    ident: Ident,
244    modes: Vec<Ident>,
245    deps: Vec<Dep>,
246    /// The member task. Either a bare path (`http_task`) or a partial call carrying
247    /// extra args (`mcp_server_task(stack())`); the macro spawns member `j` as
248    /// `s.spawn(<fn>(&POOL[j] [, extra args])?)` — the node is always the first arg.
249    /// No closure form (members are instantiated per index), unlike a node's `spawn:`.
250    /// The `Shell` variant (`task:`) wraps a plain — possibly generic — async fn in
251    /// ONE generated `#[embassy_executor::task(pool_size = K)]` shell shared by all
252    /// members (they share one concrete future type, like a `spawn:` pool).
253    source: TaskSource,
254    /// The scaling policy value, emitted as the `policy:` field of the `ElasticPool`
255    /// static. The static is typed `ElasticPool<P>`, so the macro needs the policy
256    /// *type* `P`: when `policy_ty` is `None` it derives `P` from this expr via
257    /// `policy_type` (requires a `Type::new(..)` shape); when `policy_ty` is `Some`
258    /// the caller stated `P` explicitly and this expr can be any value of that type.
259    policy: Expr,
260    /// Optional explicit policy type from the `policy: <Type> = <expr>` form. `Some`
261    /// bypasses `policy_type` derivation, allowing a value the deriver can't handle
262    /// (a free fn, a const, a builder chain, a qualified path).
263    policy_ty: Option<Type>,
264    /// `executor: NAME` — spawn every member through the named [`SpawnerSlot`]
265    /// (e.g. a worker pool on the second core, scaled by this core's supervisor).
266    executor: Option<Ident>,
267    min: LitInt,
268    max: LitInt,
269}
270
271// Both variants embed a large `syn::Expr` (and `PoolItem` a bit more), so their sizes
272// are close but unequal — enough for `large_enum_variant` to flag the gap. This AST is
273// parsed once and lives only briefly in a `Vec` during expansion, so boxing a variant
274// to shave a few bytes per element buys nothing real; suppress the lint instead of
275// paying a heap allocation.
276#[allow(clippy::large_enum_variant)]
277enum Item {
278    Node(NodeItem),
279    Pool(PoolItem),
280    Executor(ExecutorItem),
281}
282
283/// The parsed macro input: the list of `node`/`pool` declarations, in source order.
284/// Named `GraphSpec` (not `Graph`) to stay distinct from the *emitted* public type
285/// [`embassy_supervisor::Graph`] that `expand` produces as the `GRAPH` static.
286struct GraphSpec {
287    items: Vec<Item>,
288}
289
290impl Parse for GraphSpec {
291    fn parse(input: ParseStream) -> SynResult<Self> {
292        let mut items = Vec::new();
293        while !input.is_empty() {
294            let cfg = input.call(Attribute::parse_outer)?;
295            if input.peek(kw::node) {
296                items.push(Item::Node(parse_node(input, cfg)?));
297            } else if input.peek(kw::pool) {
298                items.push(Item::Pool(parse_pool(input, cfg)?));
299            } else if input.peek(kw::executor) {
300                // `executor NAME;` — a runtime-filled SendSpawner slot; nodes
301                // carrying `executor: NAME` spawn through it (the supervisor awaits
302                // the slot before spawning them).
303                input.parse::<kw::executor>()?;
304                let ident: Ident = input.parse()?;
305                input.parse::<Token![;]>()?;
306                items.push(Item::Executor(ExecutorItem { cfg, ident }));
307            } else {
308                return Err(input.error(
309                    "expected `node`, `pool`, or `executor` (optionally `#[cfg(...)]`-prefixed)",
310                ));
311            }
312        }
313        Ok(GraphSpec { items })
314    }
315}
316
317// node IDENT = MODE, deps: [..] [, spawn: <expr>] [, disabled];
318fn parse_node(input: ParseStream, cfg: Vec<Attribute>) -> SynResult<NodeItem> {
319    input.parse::<kw::node>()?;
320    let ident: Ident = input.parse()?;
321    input.parse::<Token![=]>()?;
322    let mode: Ident = input.parse()?;
323    input.parse::<Token![,]>()?;
324    input.parse::<kw::deps>()?;
325    input.parse::<Token![:]>()?;
326    let deps = parse_dep_list(input)?;
327
328    let mut spawn = None;
329    let mut task: Option<(kw::task, Expr)> = None;
330    let mut pool_size = None;
331    let mut disabled = false;
332    let mut executor = None;
333    let mut resources: Option<(kw::resources, Vec<ResourceDecl>)> = None;
334    while input.peek(Token![,]) {
335        input.parse::<Token![,]>()?;
336        if input.peek(kw::spawn) {
337            input.parse::<kw::spawn>()?;
338            input.parse::<Token![:]>()?;
339            spawn = Some(input.parse::<Expr>()?);
340        } else if input.peek(kw::task) {
341            let k = input.parse::<kw::task>()?;
342            input.parse::<Token![:]>()?;
343            task = Some((k, input.parse::<Expr>()?));
344        } else if input.peek(kw::pool_size) {
345            input.parse::<kw::pool_size>()?;
346            input.parse::<Token![:]>()?;
347            pool_size = Some(input.parse::<LitInt>()?);
348        } else if input.peek(kw::disabled) {
349            input.parse::<kw::disabled>()?;
350            disabled = true;
351        } else if input.peek(kw::executor) {
352            input.parse::<kw::executor>()?;
353            input.parse::<Token![:]>()?;
354            executor = Some(input.parse::<Ident>()?);
355        } else if input.peek(kw::resources) {
356            let k = input.parse::<kw::resources>()?;
357            input.parse::<Token![:]>()?;
358            resources = Some((k, parse_resource_list(input)?));
359        } else {
360            return Err(input.error(
361                "expected `spawn:`, `task:`, `pool_size:`, `executor:`, `resources:`, or `disabled`",
362            ));
363        }
364    }
365    input.parse::<Token![;]>()?;
366
367    // Exactly one of `spawn:` / `task:` may pick the node's task.
368    if let (Some(_), Some((k, _))) = (&spawn, &task) {
369        return Err(syn::Error::new_spanned(
370            k,
371            "`task:` and `spawn:` are mutually exclusive — `spawn:` names a \
372             hand-written `#[embassy_executor::task]` fn, `task:` generates one",
373        ));
374    }
375    // `pool_size:` sizes the generated shell's TaskPool; without `task:` there is
376    // no generated shell to size (a `spawn:` task fn declares its own).
377    if let (Some(ps), None) = (&pool_size, &task) {
378        return Err(syn::Error::new_spanned(
379            ps,
380            "`pool_size:` requires `task:` — a `spawn:` task fn sets its own \
381             `#[embassy_executor::task(pool_size = ...)]`",
382        ));
383    }
384    // `resources:` only makes sense with `task:`: the generated shell is what
385    // takes the values out of their slots at spawn and restores them after the
386    // worker returns. A hand-written `spawn:` fn (or a parked node) manages its
387    // own arguments.
388    if let Some((k, decls)) = &resources {
389        if task.is_none() {
390            return Err(syn::Error::new_spanned(
391                k,
392                "`resources:` requires `task:` — resources are handed to the \
393                 generated shell as owned arguments and restored by it; a \
394                 `spawn:` task fn manages its own arguments",
395            ));
396        }
397        if decls.is_empty() {
398            return Err(syn::Error::new_spanned(
399                k,
400                "`resources:` must declare at least one `NAME: Type` entry",
401            ));
402        }
403        // Duplicate names within one node would emit two statics with the same
404        // ident; catch it here with a clearer message than rustc's E0428.
405        for (i, d) in decls.iter().enumerate() {
406            if decls[..i].iter().any(|prev| prev.ident == d.ident) {
407                return Err(syn::Error::new_spanned(
408                    &d.ident,
409                    format!("duplicate resource name `{}`", d.ident),
410                ));
411            }
412        }
413    }
414    if let Some(ps) = &pool_size {
415        if ps.base10_parse::<usize>()? == 0 {
416            return Err(syn::Error::new_spanned(
417                ps,
418                "`pool_size:` must be at least 1",
419            ));
420        }
421    }
422    let source = match (spawn, task) {
423        (Some(e), _) => Some(TaskSource::Spawn(e)),
424        (None, Some((_, e))) => Some(TaskSource::Shell(e)),
425        (None, None) => None,
426    };
427
428    Ok(NodeItem {
429        cfg,
430        ident,
431        mode,
432        deps,
433        source,
434        pool_size,
435        disabled,
436        executor,
437        resources: resources.map(|(_, decls)| decls).unwrap_or_default(),
438    })
439}
440
441// pool IDENT = [MODES], deps: [..][, executor: EXEC], spawn: <fn>, policy: EXPR, min: N, max: M;
442fn parse_pool(input: ParseStream, cfg: Vec<Attribute>) -> SynResult<PoolItem> {
443    input.parse::<kw::pool>()?;
444    let ident: Ident = input.parse()?;
445    input.parse::<Token![=]>()?;
446    let modes = parse_mode_list(input)?;
447    input.parse::<Token![,]>()?;
448    input.parse::<kw::deps>()?;
449    input.parse::<Token![:]>()?;
450    let deps = parse_dep_list(input)?;
451    input.parse::<Token![,]>()?;
452    // Optional `executor: NAME,` — run the whole pool on the named SpawnerSlot's
453    // executor (e.g. a worker pool on the second core, scaled from this one).
454    let executor = if input.peek(kw::executor) {
455        input.parse::<kw::executor>()?;
456        input.parse::<Token![:]>()?;
457        let ex: Ident = input.parse()?;
458        input.parse::<Token![,]>()?;
459        Some(ex)
460    } else {
461        None
462    };
463    // The member task: a path, or a partial call supplying extra args (the macro
464    // injects `&POOL[j]` as the first argument in either case). `spawn:` names a
465    // hand-written `#[embassy_executor::task(pool_size = K)]` fn; `task:` names a
466    // plain (possibly generic) async fn the macro wraps in ONE generated shell
467    // task sized `pool_size = K`.
468    let source = if input.peek(kw::task) {
469        input.parse::<kw::task>()?;
470        input.parse::<Token![:]>()?;
471        TaskSource::Shell(input.parse()?)
472    } else {
473        input.parse::<kw::spawn>()?;
474        input.parse::<Token![:]>()?;
475        TaskSource::Spawn(input.parse()?)
476    };
477    input.parse::<Token![,]>()?;
478    // `resources:` is node-only: a ResourceSlot holds ONE value, and pool
479    // members all run the same worker — they would contend for that single
480    // instance and every member past the first would fail its spawn. Reject
481    // with a targeted message instead of the generic "expected `policy`".
482    if input.peek(kw::resources) {
483        let k = input.parse::<kw::resources>()?;
484        return Err(syn::Error::new_spanned(
485            k,
486            "`resources:` is not supported on `pool` — members would contend \
487             for a single instance; declare per-node",
488        ));
489    }
490    input.parse::<kw::policy>()?;
491    input.parse::<Token![:]>()?;
492    // Optional explicit policy type: `policy: <Ty> = <expr>`. Fork to see if a `Type`
493    // is followed by `=`; if so it's an annotation (commit on the real stream + eat the
494    // `=`), otherwise rewind and treat the whole thing as the value expr (type derived
495    // from it in `emit_pool`). For the common `Ty::new(..)` value the fork parses only a
496    // partial type and then sees `(`, not `=`, so it correctly falls back to the derive
497    // path — this keeps the bare form working unchanged.
498    let policy_ty = {
499        let fork = input.fork();
500        if fork.parse::<Type>().is_ok() && fork.peek(Token![=]) {
501            let ty: Type = input.parse()?;
502            input.parse::<Token![=]>()?;
503            Some(ty)
504        } else {
505            None
506        }
507    };
508    let policy: Expr = input.parse()?;
509    input.parse::<Token![,]>()?;
510    input.parse::<kw::min>()?;
511    input.parse::<Token![:]>()?;
512    let min: LitInt = input.parse()?;
513    input.parse::<Token![,]>()?;
514    input.parse::<kw::max>()?;
515    input.parse::<Token![:]>()?;
516    let max: LitInt = input.parse()?;
517    input.parse::<Token![;]>()?;
518    Ok(PoolItem {
519        cfg,
520        ident,
521        modes,
522        deps,
523        source,
524        policy,
525        policy_ty,
526        executor,
527        min,
528        max,
529    })
530}
531
532/// The node/pool name string: ident lowercased with `_`→`-` (`WIFI_CTRL` → "wifi-ctrl").
533fn name_string(ident: &Ident) -> String {
534    ident.to_string().to_lowercase().replace('_', "-")
535}
536
537/// Build a task-call expression with `node_ref` injected as the **first** argument:
538/// a bare path `f` => `f(node_ref)`; a partial call `f(a, b)` => `f(node_ref, a, b)`.
539/// The task fn is thus expected to take the node/`&POOL[i]` first, then any extra args.
540fn inject_node_call(task: &Expr, node_ref: &TokenStream2) -> SynResult<TokenStream2> {
541    inject_call_with(task, core::slice::from_ref(node_ref))
542}
543
544/// Like [`inject_node_call`] but injecting several leading arguments (the node
545/// ref, then a node's threaded `resources:` values) ahead of the user-supplied
546/// extras. Split out so the `resources:` paths don't disturb the single-arg
547/// callers.
548fn inject_call_with(task: &Expr, lead: &[TokenStream2]) -> SynResult<TokenStream2> {
549    match task {
550        Expr::Path(_) => Ok(quote!(#task(#(#lead),*))),
551        Expr::Call(c) => {
552            let f = &c.func;
553            let args = c.args.iter();
554            Ok(quote!(#f(#(#lead),* #(, #args)*)))
555        }
556        other => Err(syn::Error::new_spanned(
557            other,
558            "expected a task-fn path or a partial call like `f(extra_args)`",
559        )),
560    }
561}
562
563/// Combine an item's `#[cfg(...)]` attributes into one predicate (`all(..)` if
564/// several), used to gate its `GRAPH.nodes` slot to `Some`/`None`. `None` = always present.
565fn cfg_predicate(attrs: &[Attribute]) -> Option<TokenStream2> {
566    let preds: Vec<TokenStream2> = attrs
567        .iter()
568        .filter_map(|a| match &a.meta {
569            Meta::List(ml) if ml.path.is_ident("cfg") => Some(ml.tokens.clone()),
570            _ => None,
571        })
572        .collect();
573    match preds.len() {
574        0 => None,
575        1 => Some(preds[0].clone()),
576        _ => Some(quote!(all(#(#preds),*))),
577    }
578}
579
580/// Extract the policy *type* from a `Type::new(..)` constructor expression. Only used
581/// on the derive path (no explicit `policy: <Ty> = ..` annotation); the type is the
582/// call's path minus its last segment (`DeferredShrink::new` -> `DeferredShrink`).
583fn policy_type(expr: &Expr) -> SynResult<Path> {
584    if let Expr::Call(call) = expr
585        && let Expr::Path(p) = &*call.func
586    {
587        let n = p.path.segments.len();
588        if n >= 2 {
589            let segs: Punctuated<_, Token![::]> =
590                p.path.segments.iter().take(n - 1).cloned().collect();
591            return Ok(Path {
592                leading_colon: p.path.leading_colon,
593                segments: segs,
594            });
595        }
596    }
597    Err(syn::Error::new_spanned(
598        expr,
599        "pool `policy:` must be a `Type::new(..)` constructor (e.g. `DeferredShrink::new(..)`), \
600         or give the type explicitly: `policy: <Type> = <expr>`",
601    ))
602}
603
604/// One emitted node slot, in final index order.
605struct Slot {
606    /// Presence predicate (`None` = unconditional), gates the node slot (`GRAPH.nodes`) entry.
607    cfg_pred: Option<TokenStream2>,
608    /// `&NODE` or `&POOL[j]`.
609    reference: TokenStream2,
610    /// Raw deps, resolved to indices in the second pass.
611    deps: Vec<Dep>,
612}
613
614/// The `Option<fn(..)>` spawn expression for a node. `None` (no `spawn:`) is a
615/// parked node the app spawns itself. A path or partial call is a task fn taking
616/// `&NODE` first (plus any given args); the macro wraps it as
617/// `|s| { s.spawn(<task>(&NODE, ..)?); Ok(()) }`. Anything else (a closure, or a
618/// ready spawn fn) is emitted verbatim. Every form is cast to `spawn_fn` so it
619/// coerces cleanly inside `Option::Some(..)`.
620fn node_spawn(
621    ident: &Ident,
622    spawn: &Option<Expr>,
623    executor: &Option<Ident>,
624    resources: &[ResourceDecl],
625    spawn_fn: &TokenStream2,
626) -> SynResult<TokenStream2> {
627    // `resources:` take-prelude + the taken values as extra shell arguments.
628    // Taking here — in the glue, BEFORE the spawn — is the point: an unprovided
629    // slot fails `Supervisor::start` with `SpawnError::Busy` (the supervisor
630    // logs the node name), instead of panicking inside an already-spawned task.
631    // The values ride into the task as ordinary `#[embassy_executor::task]`
632    // arguments (embassy stores them in the shell's TaskPool slot).
633    let take_prelude: Vec<TokenStream2> = resources
634        .iter()
635        .enumerate()
636        .map(|(i, r)| {
637            let res = &r.ident;
638            let var = format_ident!("__r{}", i);
639            quote! {
640                let #var = #res
641                    .take()
642                    .ok_or(::embassy_executor::SpawnError::Busy)?;
643            }
644        })
645        .collect();
646    let res_args: Vec<TokenStream2> = (0..resources.len())
647        .map(|i| {
648            let var = format_ident!("__r{}", i);
649            quote!(#var)
650        })
651        .collect();
652    Ok(match (spawn, executor) {
653        (None, None) => quote!(::core::option::Option::None),
654        // `executor:` needs the macro to perform the spawn, so it composes only
655        // with the path / partial-call `spawn:` forms below.
656        (None, Some(ex)) => {
657            return Err(syn::Error::new_spanned(
658                ex,
659                "`executor:` requires a `spawn:` (a parked node is spawned by the \
660                 application, which picks its own spawner)",
661            ));
662        }
663        // A path or a partial call: a task fn taking `&NODE` first (plus any
664        // given args); generate `|s| { s.spawn(<task>(&NODE, ..)?); Ok(()) }`.
665        // With `executor: NAME` the glue ignores the supervisor's `Spawner` and
666        // spawns through the named `SpawnerSlot` (a `SendSpawner` the app
667        // registers at runtime): an unfilled slot fails the spawn with
668        // `SpawnError::Busy` — loud misconfiguration, not a missing task. The
669        // task future must then be `Send` (enforced by `SendSpawner::spawn`).
670        (Some(e @ (Expr::Path(_) | Expr::Call(_))), executor) => {
671            let mut lead: Vec<TokenStream2> = vec![quote!(&#ident)];
672            lead.extend(res_args.iter().cloned());
673            let call = inject_call_with(e, &lead)?;
674            match executor {
675                None => {
676                    let stmts = spawn_stmts(&call, &quote!(&#ident), &quote!(s));
677                    quote!(::core::option::Option::Some(
678                        (|s| {
679                            #(#take_prelude)*
680                            #stmts
681                            ::core::result::Result::Ok(())
682                        }) as #spawn_fn
683                    ))
684                }
685                Some(ex) => {
686                    let stmts = spawn_stmts(&call, &quote!(&#ident), &quote!(__sp));
687                    quote!(::core::option::Option::Some(
688                        (|_s| {
689                            // The supervisor awaits this slot's `ready()` before
690                            // invoking the glue (the node carries `.with_executor(&EX)`
691                            // and the bring-up bounds the wait), so `get()` is already
692                            // filled; `ok_or` is the belt-and-braces unfilled guard.
693                            // Resources are taken AFTER the spawner guard, so an
694                            // unfilled executor never consumes (and strands) them.
695                            let __sp = #ex
696                                .get()
697                                .ok_or(::embassy_executor::SpawnError::Busy)?;
698                            #(#take_prelude)*
699                            #stmts
700                            ::core::result::Result::Ok(())
701                        }) as #spawn_fn
702                    ))
703                }
704            }
705        }
706        (Some(_), Some(ex)) => {
707            return Err(syn::Error::new_spanned(
708                ex,
709                "`executor:` cannot be combined with a verbatim spawn closure (the \
710                 closure owns the spawn; use the named SpawnerSlot inside it instead)",
711            ));
712        }
713        // Anything else (a closure, or a ready spawn fn) is emitted verbatim.
714        // NOTE: with the `trace` feature such a node is not auto-mapped — the
715        // closure owns the SpawnToken; call `adopt`/`set_task_id` in it yourself.
716        (Some(e), None) => quote!(::core::option::Option::Some((#e) as #spawn_fn)),
717    })
718}
719
720/// The spawn statement(s) for the generated glue. Plain `s.spawn(<call>?)`
721/// normally; with the `trace` feature the `SpawnToken` is bound first so its task
722/// id can be captured into the node (`set_task_id`) — the id→node mapping the
723/// supervisor's `trace` recorders resolve against (in embassy-executor 0.10 the
724/// task-fn call returns `Result<SpawnToken, SpawnError>` and `Spawner::spawn`
725/// itself is infallible, so the token is available between the two).
726///
727/// Three shapes, resolved at expansion by the macro crate's own features:
728/// * `trace` on → bind the token and `adopt` it (`set_task_id` + name stamp under
729///   `metadata-names`).
730/// * `trace` off but `metadata-names` on → bind the token and `stamp_name` only:
731///   the node name reaches the task Metadata (for rtos-trace/SystemView) with no id
732///   capture and no dependency on the `_embassy_trace_*` hooks.
733/// * neither → plain infallible spawn.
734fn spawn_stmts(call: &TokenStream2, node_ref: &TokenStream2, sp: &TokenStream2) -> TokenStream2 {
735    if cfg!(feature = "trace") {
736        // `adopt` = set_task_id + (under metadata-names) Metadata name stamp.
737        quote! {
738            let __token = #call?;
739            (#node_ref).adopt(&__token);
740            #sp.spawn(__token);
741        }
742    } else if cfg!(feature = "metadata-names") {
743        // Name-only path: stamp the node name into the task Metadata, nothing else.
744        quote! {
745            let __token = #call?;
746            (#node_ref).stamp_name(&__token);
747            #sp.spawn(__token);
748        }
749    } else {
750        quote!(#sp.spawn(#call?);)
751    }
752}
753
754/// Emit the `#[embassy_executor::task]` shell for a `task:` clause: a concrete,
755/// non-generic task fn that takes only the node and awaits the user's worker with
756/// the node injected first. This is how a **generic** worker becomes spawnable —
757/// embassy forbids generic tasks (one static `TaskPool` per concrete future type),
758/// so a monomorphized shell is stamped per declaration. Worker args are evaluated
759/// inside the shell — at the task's first poll, on the node's own executor — so
760/// the DSL never needs the arg types and a cross-core node builds its resources on
761/// the core that runs them.
762///
763/// Returns the shell item and a path `Expr` naming it, which feeds the ordinary
764/// `spawn:` path-form glue (executor routing and trace `adopt` compose unchanged).
765fn emit_shell(
766    owner: &Ident,
767    cfg: &[Attribute],
768    worker: &Expr,
769    pool_size: usize,
770    resources: &[ResourceDecl],
771    cr: &TokenStream2,
772) -> SynResult<(TokenStream2, Expr)> {
773    if !matches!(worker, Expr::Path(_) | Expr::Call(_)) {
774        return Err(syn::Error::new_spanned(
775            worker,
776            "`task:` names an async worker fn — a path or a partial call like \
777             `worker(args)`; for a closure or a ready spawn fn use `spawn:`",
778        ));
779    }
780    let shell = format_ident!("__sv_task_{}", owner.to_string().to_lowercase());
781    // `resources:` values arrive as owned task arguments (the spawn glue took
782    // them out of their slots); the shell keeps ownership, lends the worker
783    // `&mut`, and restores each value to its slot after the worker returns —
784    // i.e. after the worker's clean shutdown ack — so a Terminate respawn
785    // re-takes the SAME instance instead of re-acquiring hardware. A `Pause`
786    // worker parks instead of returning, so it simply retains its resources
787    // (the restore lines below are unreachable for it — correct, same as a
788    // hand-written parked task holding its arguments).
789    let res_params: Vec<TokenStream2> = resources
790        .iter()
791        .enumerate()
792        .map(|(i, r)| {
793            let var = format_ident!("__r{}", i);
794            let ty = &r.ty;
795            quote!(mut #var: #ty)
796        })
797        .collect();
798    let res_leases: Vec<TokenStream2> = (0..resources.len())
799        .map(|i| {
800            let var = format_ident!("__r{}", i);
801            quote!(&mut #var)
802        })
803        .collect();
804    let restores: Vec<TokenStream2> = resources
805        .iter()
806        .enumerate()
807        .map(|(i, r)| {
808            let res = &r.ident;
809            let var = format_ident!("__r{}", i);
810            quote!(#res.restore(#var);)
811        })
812        .collect();
813    let mut lead: Vec<TokenStream2> = vec![quote!(__node)];
814    lead.extend(res_leases);
815    let call = inject_call_with(worker, &lead)?;
816    // Unsuffixed literal: `#[task]`'s own parser wants a plain integer.
817    let ps = LitInt::new(&pool_size.to_string(), proc_macro2::Span::call_site());
818    let def = quote! {
819        #(#cfg)*
820        #[::embassy_executor::task(pool_size = #ps)]
821        async fn #shell(__node: &'static #cr::TaskNode #(, #res_params)*) {
822            #call.await;
823            #(#restores)*
824        }
825    };
826    let path: Expr = syn::parse_quote!(#shell);
827    Ok((def, path))
828}
829
830/// Emit a `node`: its `pub static #ident: TaskNode` definition and its `Slot`. The
831/// caller assigns the slot index and records the name, so this touches neither.
832/// A `task:` node additionally emits its generated shell ahead of the static.
833fn emit_node(
834    n: &NodeItem,
835    cr: &TokenStream2,
836    spawn_fn: &TokenStream2,
837) -> SynResult<(TokenStream2, Slot)> {
838    let ident = &n.ident;
839    let cfg = &n.cfg;
840    let mode = &n.mode;
841    let name = name_string(&n.ident);
842    let disabled = n.disabled;
843    let (shell_def, spawn_expr) = match &n.source {
844        Some(TaskSource::Shell(worker)) => {
845            let ps = match &n.pool_size {
846                Some(l) => l.base10_parse::<usize>()?,
847                None => 1,
848            };
849            let (def, path) = emit_shell(ident, cfg, worker, ps, &n.resources, cr)?;
850            (def, Some(path))
851        }
852        Some(TaskSource::Spawn(e)) => (quote!(), Some(e.clone())),
853        None => (quote!(), None),
854    };
855    let spawn = node_spawn(ident, &spawn_expr, &n.executor, &n.resources, spawn_fn)?;
856    // `executor: NAME` routes the node through that SpawnerSlot; the supervisor
857    // awaits the slot before spawning (see `TaskNode::with_executor`).
858    let with_exec = match &n.executor {
859        Some(ex) => quote!( .with_executor(&#ex) ),
860        None => quote!(),
861    };
862    // `resources: [NAME: Type, ..]` — one `pub static NAME: ResourceSlot<Type>`
863    // per entry (main moves the resource in with `NAME.provide(..)`), plus a
864    // type-erased gate array wired into the node so the supervisor can await
865    // provisioning/restore before each (re)spawn (see `TaskNode::with_resources`).
866    // The unsized coercion `&NAME` -> `&dyn ResourceGate` happens in the static
867    // initializer, where it is allowed.
868    let (res_defs, with_res) = if n.resources.is_empty() {
869        (quote!(), quote!())
870    } else {
871        let gates_ident = format_ident!("__SV_GATES_{}", ident);
872        let slot_defs = n.resources.iter().map(|r| {
873            let res = &r.ident;
874            let ty = &r.ty;
875            let doc = format!(
876                "Resource slot for node `{ident}` (generated by `supervisor_graph!`). \
877                 Move the resource in with `.provide(..)` before `Supervisor::start`."
878            );
879            quote! {
880                #(#cfg)*
881                #[doc = #doc]
882                pub static #res: #cr::ResourceSlot<#ty> = #cr::ResourceSlot::new();
883            }
884        });
885        let gate_refs = n.resources.iter().map(|r| {
886            let res = &r.ident;
887            quote!(&#res)
888        });
889        let gate_count = n.resources.len();
890        (
891            quote! {
892                #(#slot_defs)*
893                #(#cfg)*
894                static #gates_ident: [&'static dyn #cr::ResourceGate; #gate_count] =
895                    [#(#gate_refs),*];
896            },
897            quote!( .with_resources(&#gates_ident) ),
898        )
899    };
900    let def = quote! {
901        #res_defs
902        #shell_def
903        #(#cfg)*
904        pub static #ident: #cr::TaskNode =
905            #cr::TaskNode::new(#name, #cr::Mode::#mode, #spawn, #disabled) #with_exec #with_res;
906    };
907    let slot = Slot {
908        cfg_pred: cfg_predicate(cfg),
909        reference: quote!(&#ident),
910        deps: n.deps.clone(),
911    };
912    Ok((def, slot))
913}
914
915/// Emit a `pool`: the member `[TaskNode; K]` array, the `spawn_<pool>` glue fn, and
916/// the `ElasticPool` static (returned as `defs`, in that emission order), plus the
917/// pool-registry entry (for `GRAPH.pools`) and one `Slot` per member (members occupy
918/// slots but aren't name-addressable, so no name is recorded).
919fn emit_pool(
920    p: &PoolItem,
921    cr: &TokenStream2,
922    spawn_fn: &TokenStream2,
923) -> SynResult<(Vec<TokenStream2>, TokenStream2, Vec<Slot>)> {
924    let ident = &p.ident;
925    let cfg = &p.cfg;
926    let lname = name_string(&p.ident);
927    let pool_static = format_ident!("{}_POOL", ident);
928    let k = p.modes.len();
929
930    // Validate the scaling bounds at expansion time. `base10_parse::<u8>` also
931    // rejects values > 255 with a span-attached error (the `ElasticPool` fields are
932    // `u8`). `min > max` makes the policy contradict itself; `max > k` is a ceiling
933    // the pool can never reach (there are only `k` member slots) — both are
934    // declaration bugs, caught here rather than surfacing as odd runtime scaling.
935    // `max < k` is allowed (spare declared members below the ceiling), as is
936    // `min: 0` (the policy may scale the pool all the way down when idle).
937    let min_v: u8 = p.min.base10_parse()?;
938    let max_v: u8 = p.max.base10_parse()?;
939    if min_v > max_v {
940        return Err(syn::Error::new_spanned(
941            &p.min,
942            format!("pool `min:` ({min_v}) must not exceed `max:` ({max_v})"),
943        ));
944    }
945    if usize::from(max_v) > k {
946        return Err(syn::Error::new_spanned(
947            &p.max,
948            format!("pool `max:` ({max_v}) exceeds the declared member count ({k})"),
949        ));
950    }
951
952    // Resolve the member task: `spawn:` uses the given expr directly; `task:`
953    // first stamps ONE generated shell sized `pool_size = K` (all members share a
954    // single concrete future type) and targets that.
955    let (shell_def, member_expr) = match &p.source {
956        TaskSource::Spawn(e) => (quote!(), e.clone()),
957        TaskSource::Shell(worker) => emit_shell(ident, cfg, worker, k, &[], cr)?,
958    };
959    // Build member `I`'s spawn call from the member task, injecting
960    // `&POOL[I]` as the first argument (see `inject_node_call`).
961    let call = inject_node_call(&member_expr, &quote!(&#ident[I]))?;
962    // Per-member spawn fn: a generated `spawn_<pool>::<I>` wrapper. Same optional
963    // trace capture as a node's closure, against member `I`'s slot. With
964    // `executor: NAME` the wrapper ignores the supervisor's `Spawner` and spawns
965    // through the named SpawnerSlot; each member node carries `.with_executor(&EX)`,
966    // so the supervisor awaits the slot (bounded) before invoking the wrapper and
967    // the wrapper's `get()` is already filled (`SpawnError::Busy` guards a never-
968    // filled slot; member futures must be `Send`). A whole worker pool can thus live
969    // on another executor — e.g. the second core — while this core scales it.
970    let (param, prelude, sp_tokens) = match &p.executor {
971        None => (quote!(s), quote!(), quote!(s)),
972        Some(ex) => (
973            quote!(_s),
974            quote! {
975                let __sp = #ex
976                    .get()
977                    .ok_or(::embassy_executor::SpawnError::Busy)?;
978            },
979            quote!(__sp),
980        ),
981    };
982    let pool_spawn_stmts = spawn_stmts(&call, &quote!(&#ident[I]), &sp_tokens);
983    let wrapper = format_ident!("spawn_{}", lname);
984    let mut defs: Vec<TokenStream2> = Vec::new();
985    defs.push(shell_def);
986    defs.push(quote! {
987        #(#cfg)*
988        fn #wrapper<const I: usize>(
989            #param: ::embassy_executor::Spawner,
990        ) -> ::core::result::Result<(), ::embassy_executor::SpawnError> {
991            #prelude
992            #pool_spawn_stmts
993            ::core::result::Result::Ok(())
994        }
995    });
996    let member_spawn: Vec<TokenStream2> = (0..k).map(|j| quote!(#wrapper::<#j>)).collect();
997
998    // `executor: NAME` on the pool routes every member through that SpawnerSlot; the
999    // supervisor awaits it before spawning each member (see `TaskNode::with_executor`).
1000    let member_with_exec = match &p.executor {
1001        Some(ex) => quote!( .with_executor(&#ex) ),
1002        None => quote!(),
1003    };
1004    let members = p
1005        .modes
1006        .iter()
1007        .zip(&member_spawn)
1008        .enumerate()
1009        .map(|(j, (mode, sp))| {
1010            let nm = format!("{lname}{j}");
1011            quote! {
1012                #cr::TaskNode::new(
1013                    #nm, #cr::Mode::#mode,
1014                    ::core::option::Option::Some((#sp) as #spawn_fn), false,
1015                ) #member_with_exec
1016            }
1017        });
1018    defs.push(quote! {
1019        #(#cfg)*
1020        pub static #ident: [#cr::TaskNode; #k] = [ #(#members),* ];
1021    });
1022
1023    // Structural constants, for downstream compile-time sizing (e.g. a socket
1024    // budget: `const BUDGET: usize = HTTP_MAX + 1`). Emitted because user code
1025    // can't derive them from the member array — a `const` cannot refer to a
1026    // `static` (E0013), so `HTTP.len()` is unusable in const context and the
1027    // count would otherwise have to be duplicated by hand next to the DSL.
1028    let min_const = format_ident!("{}_MIN", ident);
1029    let max_const = format_ident!("{}_MAX", ident);
1030    let members_const = format_ident!("{}_MEMBERS", ident);
1031    let (min_u, max_u) = (usize::from(min_v), usize::from(max_v));
1032    defs.push(quote! {
1033        #(#cfg)*
1034        #[doc = concat!("Pool `", stringify!(#ident), "`'s `min:` floor (validated at expansion).")]
1035        pub const #min_const: usize = #min_u;
1036        #(#cfg)*
1037        #[doc = concat!("Pool `", stringify!(#ident), "`'s `max:` scaling ceiling — the most members ever running concurrently.")]
1038        pub const #max_const: usize = #max_u;
1039        #(#cfg)*
1040        #[doc = concat!("Pool `", stringify!(#ident), "`'s declared member count (the `[TaskNode; K]` array length).")]
1041        pub const #members_const: usize = #k;
1042    });
1043
1044    let member_refs = (0..k).map(|j| quote!(&#ident[#j]));
1045    let policy = &p.policy;
1046    // The `ElasticPool<P>` type argument: honor an explicit `policy: <Ty> = ..`
1047    // annotation, else derive `P` from the constructor expr (`Ty::new(..)` shape).
1048    let policy_ty = match &p.policy_ty {
1049        Some(ty) => quote!(#ty),
1050        None => {
1051            let path = policy_type(policy)?;
1052            quote!(#path)
1053        }
1054    };
1055    // Emit the *validated* u8 values (`min_v`/`max_v`), not the raw literals: a
1056    // suffixed literal like `min: 3usize` parses as u8 above but would emit a
1057    // mismatched-type rustc error into the u8 field.
1058    defs.push(quote! {
1059        #(#cfg)*
1060        pub static #pool_static: #cr::ElasticPool<#policy_ty> = #cr::ElasticPool {
1061            nodes: &[ #(#member_refs),* ],
1062            min: #min_v,
1063            max: #max_v,
1064            policy: #policy,
1065        };
1066    });
1067
1068    let pool_entry = quote!( #(#cfg)* &#pool_static );
1069
1070    let pred = cfg_predicate(cfg);
1071    let slots = (0..k)
1072        .map(|j| Slot {
1073            cfg_pred: pred.clone(),
1074            reference: quote!(&#ident[#j]),
1075            deps: p.deps.clone(),
1076        })
1077        .collect();
1078
1079    Ok((defs, pool_entry, slots))
1080}
1081
1082/// Second pass: build the node-slot entries for `GRAPH.nodes` (`Option`, cfg-gated) and
1083/// the cfg-aware dep-index entries for `GRAPH.deps`. Runs after every slot + name is
1084/// known, since a dep may forward-reference a node declared later. An unknown dep name
1085/// is a compile error.
1086fn slot_tables(
1087    slots: &[Slot],
1088    names: &HashMap<String, usize>,
1089) -> SynResult<(Vec<TokenStream2>, Vec<TokenStream2>)> {
1090    let mut all_entries: Vec<TokenStream2> = Vec::new();
1091    let mut deps_entries: Vec<TokenStream2> = Vec::new();
1092    for slot in slots {
1093        let reference = &slot.reference;
1094        all_entries.push(match &slot.cfg_pred {
1095            None => quote!(::core::option::Option::Some(#reference)),
1096            Some(pred) => quote!({
1097                #[cfg(#pred)]
1098                { ::core::option::Option::Some(#reference) }
1099                #[cfg(not(#pred))]
1100                { ::core::option::Option::None }
1101            }),
1102        });
1103
1104        let mut dep_toks: Vec<TokenStream2> = Vec::new();
1105        // Duplicate deps are a compile error: `deps: [A, A]` would emit a doubled
1106        // index, which `topo_sort_const` counts twice in the in-degree but decrements
1107        // once — misreported as a dependency cycle. Compared by *resolved* slot index
1108        // (so a repeated pool name trips it too); two cfg-gated variants of the same
1109        // dep are allowed only when their cfg predicates differ.
1110        let mut seen: Vec<(u8, String)> = Vec::new();
1111        for d in &slot.deps {
1112            let idx = match names.get(&d.ident.to_string()) {
1113                Some(&i) => i as u8,
1114                None => {
1115                    return Err(syn::Error::new_spanned(
1116                        &d.ident,
1117                        format!(
1118                            "unknown dependency `{}` — not a declared node or pool",
1119                            d.ident
1120                        ),
1121                    ));
1122                }
1123            };
1124            let cfg = &d.cfg;
1125            let cfg_key = quote!( #(#cfg)* ).to_string();
1126            if seen.iter().any(|(i, k)| *i == idx && *k == cfg_key) {
1127                return Err(syn::Error::new_spanned(
1128                    &d.ident,
1129                    format!("duplicate dependency `{}`", d.ident),
1130                ));
1131            }
1132            seen.push((idx, cfg_key));
1133            dep_toks.push(quote!( #(#cfg)* #idx ));
1134        }
1135        deps_entries.push(quote!( &[ #(#dep_toks),* ] ));
1136    }
1137    Ok((all_entries, deps_entries))
1138}
1139
1140fn expand(graph: GraphSpec) -> SynResult<TokenStream2> {
1141    let cr = quote!(::embassy_supervisor);
1142    // The node spawn fn-pointer type. Spawn exprs (closures / const-generic fns) are
1143    // cast to this so they coerce cleanly inside `Option::Some(..)`.
1144    let spawn_fn = quote!(
1145        fn(
1146            ::embassy_executor::Spawner,
1147        ) -> ::core::result::Result<(), ::embassy_executor::SpawnError>
1148    );
1149
1150    // First pass: emit the statics/glue in declaration order, assign stable slot
1151    // indices, and record each slot + its raw deps. `names` maps a dep-addressable ident
1152    // to its slot index for dep resolution — keyed on the *raw* ident (not the runtime
1153    // `name_string`). A `node` maps to its own slot; a `pool` maps to its floor member's
1154    // slot (so `deps: [POOL]` = "after the pool is up"). Individual pool members are not
1155    // separately name-addressable.
1156    let mut defs: Vec<TokenStream2> = Vec::new();
1157    let mut pool_entries: Vec<TokenStream2> = Vec::new();
1158    let mut slots: Vec<Slot> = Vec::new();
1159    let mut names: HashMap<String, usize> = HashMap::new();
1160
1161    // Pre-pass: collect the declared `executor NAME;` slots so a node's
1162    // `executor:` reference can be validated regardless of declaration order.
1163    let executor_names: Vec<String> = graph
1164        .items
1165        .iter()
1166        .filter_map(|i| match i {
1167            Item::Executor(x) => Some(x.ident.to_string()),
1168            _ => None,
1169        })
1170        .collect();
1171
1172    // Pre-pass: `resources:` slot names become `pub static`s at the declaration
1173    // site, so they must be unique across the whole graph — and must not shadow
1174    // an `executor NAME;` static. Caught here with a targeted message instead of
1175    // rustc's downstream duplicate-static E0428 on generated code.
1176    {
1177        let mut seen: HashSet<String> = HashSet::new();
1178        for item in &graph.items {
1179            if let Item::Node(n) = item {
1180                for r in &n.resources {
1181                    let key = r.ident.to_string();
1182                    if !seen.insert(key.clone()) || executor_names.contains(&key) {
1183                        return Err(syn::Error::new_spanned(
1184                            &r.ident,
1185                            format!(
1186                                "duplicate resource name `{}` — resource slots are \
1187                                 statics and must be unique across the graph",
1188                                r.ident
1189                            ),
1190                        ));
1191                    }
1192                }
1193            }
1194        }
1195    }
1196
1197    for item in &graph.items {
1198        match item {
1199            Item::Node(n) => {
1200                if let Some(ex) = &n.executor
1201                    && !executor_names.contains(&ex.to_string())
1202                {
1203                    return Err(syn::Error::new_spanned(
1204                        ex,
1205                        format!(
1206                            "unknown executor `{ex}`; declare it in the graph with \
1207                             `executor {ex};` (declared: [{}])",
1208                            executor_names.join(", ")
1209                        ),
1210                    ));
1211                }
1212                // The index is the slot's position, taken *before* the push.
1213                // A redeclared name is a hard error here (not just the downstream
1214                // `duplicate definition of static`): deps resolve through this map,
1215                // so a silent overwrite would silently rewire earlier `deps:` edges.
1216                if names.insert(n.ident.to_string(), slots.len()).is_some() {
1217                    return Err(syn::Error::new_spanned(
1218                        &n.ident,
1219                        format!("duplicate node/pool name `{}`", n.ident),
1220                    ));
1221                }
1222                let (def, slot) = emit_node(n, &cr, &spawn_fn)?;
1223                defs.push(def);
1224                slots.push(slot);
1225            }
1226            Item::Executor(x) => {
1227                let (cfg, ident) = (&x.cfg, &x.ident);
1228                // A runtime-filled SendSpawner slot: the app registers the
1229                // executor's spawner before `Supervisor::start`; nodes declared
1230                // `executor: NAME` spawn through it. Occupies no graph slot.
1231                defs.push(quote! {
1232                    #(#cfg)*
1233                    /// Spawner slot for the graph's `executor:`-annotated nodes
1234                    /// (generated by `supervisor_graph!`). Fill with
1235                    /// `SpawnerSlot::set` before `Supervisor::start`.
1236                    pub static #ident: #cr::SpawnerSlot = #cr::SpawnerSlot::new();
1237                });
1238            }
1239            Item::Pool(p) => {
1240                // Pools are only meaningful with the supervisor's `pool` feature (which
1241                // forwards to this crate). Without it, `Graph` has no `pools` field and
1242                // `ElasticPool` doesn't exist — so refuse a `pool` with a clear message
1243                // rather than emitting dangling references.
1244                if cfg!(feature = "pool") {
1245                    if let Some(ex) = &p.executor
1246                        && !executor_names.contains(&ex.to_string())
1247                    {
1248                        return Err(syn::Error::new_spanned(
1249                            ex,
1250                            format!(
1251                                "unknown executor `{ex}`; declare it in the graph with \
1252                                 `executor {ex};` (declared: [{}])",
1253                                executor_names.join(", ")
1254                            ),
1255                        ));
1256                    }
1257                    let (pool_defs, pool_entry, pool_slots) = emit_pool(p, &cr, &spawn_fn)?;
1258                    // A dep on the pool NAME resolves to the pool's floor member (member 0
1259                    // — the `min`-kept, always-started member): `deps: [POOL]` means "after
1260                    // the pool is up". `slots.len()` here is that member's slot index, taken
1261                    // *before* the extend below (pool_slots[0] lands at exactly this index).
1262                    // A redeclared name errors, same as the node arm.
1263                    if names.insert(p.ident.to_string(), slots.len()).is_some() {
1264                        return Err(syn::Error::new_spanned(
1265                            &p.ident,
1266                            format!("duplicate node/pool name `{}`", p.ident),
1267                        ));
1268                    }
1269                    defs.extend(pool_defs);
1270                    pool_entries.push(pool_entry);
1271                    slots.extend(pool_slots);
1272                } else {
1273                    return Err(syn::Error::new_spanned(
1274                        &p.ident,
1275                        "a `pool` requires enabling embassy-supervisor's `pool` feature",
1276                    ));
1277                }
1278            }
1279        }
1280    }
1281
1282    let m = slots.len();
1283    // Every graph index (dep entries, `topo_sort_const`'s queue/order) is a `u8`, so
1284    // more than 256 slots would silently truncate (`i as u8`) and corrupt the order.
1285    // 256 slots means max index 255 and max per-node dep count 255 — both fit exactly.
1286    if m > 256 {
1287        return Err(syn::Error::new(
1288            proc_macro2::Span::call_site(),
1289            format!(
1290                "supervisor_graph!: {m} node slots declared, but at most 256 are supported \
1291                 (including pool members) — graph indices are `u8`"
1292            ),
1293        ));
1294    }
1295    let (all_entries, deps_entries) = slot_tables(&slots, &names)?;
1296
1297    // `Graph.pools` is `#[cfg(feature = "pool")]`; emit that field iff this macro was
1298    // built with pool support (forwarded from the supervisor's `pool` feature).
1299    let pools_field = if cfg!(feature = "pool") {
1300        quote!( pools: &[ #(#pool_entries),* ], )
1301    } else {
1302        quote!()
1303    };
1304
1305    // embassy-executor's trace hooks (declared `unsafe extern "Rust"` in the
1306    // executor), defined once here at the graph declaration site — the supervisor
1307    // crate is `forbid(unsafe_code)` and cannot carry `#[unsafe(no_mangle)]` items.
1308    // They forward to the supervisor's `trace` recorders. `task_new` and
1309    // `task_ready_begin` carry nothing the recorders need (the id→node mapping
1310    // comes from the spawn glue above), so they are no-ops. Exactly one definition
1311    // of each may exist per binary: enable `trace-hooks` OR write your own set.
1312    // Requires an edition-2024 consumer (`#[unsafe(no_mangle)]` syntax).
1313    let trace_hooks = if cfg!(feature = "trace-hooks") {
1314        quote! {
1315            #[unsafe(no_mangle)]
1316            fn _embassy_trace_poll_start(executor_id: u32) {
1317                #cr::trace::on_poll_start(executor_id);
1318            }
1319            #[unsafe(no_mangle)]
1320            fn _embassy_trace_task_new(_executor_id: u32, _task_id: u32) {}
1321            #[unsafe(no_mangle)]
1322            fn _embassy_trace_task_end(executor_id: u32, task_id: u32) {
1323                #cr::trace::on_task_end(executor_id, task_id);
1324            }
1325            #[unsafe(no_mangle)]
1326            fn _embassy_trace_task_exec_begin(executor_id: u32, task_id: u32) {
1327                #cr::trace::on_task_exec_begin(executor_id, task_id);
1328            }
1329            #[unsafe(no_mangle)]
1330            fn _embassy_trace_task_exec_end(executor_id: u32, task_id: u32) {
1331                #cr::trace::on_task_exec_end(executor_id, task_id);
1332            }
1333            #[unsafe(no_mangle)]
1334            fn _embassy_trace_task_ready_begin(_executor_id: u32, _task_id: u32) {}
1335            #[unsafe(no_mangle)]
1336            fn _embassy_trace_executor_idle(executor_id: u32) {
1337                #cr::trace::on_executor_idle(executor_id);
1338            }
1339        }
1340    } else {
1341        quote!()
1342    };
1343
1344    Ok(quote! {
1345        #(#defs)*
1346
1347        // Private backing tables — the application uses `GRAPH`. The topological order
1348        // and pools are inlined into the `GRAPH` literal below; the node count is
1349        // `GRAPH.nodes.len()`.
1350        static NODES: [::core::option::Option<&'static #cr::TaskNode>; #m] = [ #(#all_entries),* ];
1351        const DEPS: [&'static [u8]; #m] = [ #(#deps_entries),* ];
1352
1353        /// The compile-time task graph — node slots, dependency table, topological order,
1354        /// and (with the `pool` feature) the elastic pools. Pass to `Supervisor::new`.
1355        pub static GRAPH: #cr::Graph<#m> = #cr::Graph {
1356            nodes: &NODES,
1357            deps: &DEPS,
1358            order: #cr::topo_sort_const(&DEPS),
1359            #pools_field
1360        };
1361
1362        #trace_hooks
1363    })
1364}
1365
1366/// Declare a supervised task graph; see the crate docs for the surface syntax.
1367#[proc_macro]
1368pub fn supervisor_graph(input: TokenStream) -> TokenStream {
1369    let graph = syn::parse_macro_input!(input as GraphSpec);
1370    expand(graph)
1371        .unwrap_or_else(syn::Error::into_compile_error)
1372        .into()
1373}