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: [[#[cfg(..)]] RES: [local] [shared|consume] Type, ..]]
13//!     [, slot_timeout: MS][, disabled];
14//! node NAME = Mode, deps: [A];                 // neither => a parked node the app spawns
15//! executor EXEC;                               // runtime-filled SendSpawner slot
16//! pool NAME = [Mode, ..], deps: [A][, executor: EXEC], spawn: <fn> | task: <worker>,
17//!     [resources: [RES: [local] shared Type, ..],]
18//!     policy: [<Ty> =] <expr>, min: N, max: M[, slot_timeout: MS];
19//! ```
20//! `deps:` entries name a `node` or a `pool`; a `pool` dep resolves to that pool's floor
21//! member (member 0, the `min`-kept one), i.e. "start after the pool is up". A repeated
22//! dep or a redeclared node/pool name is a compile error.
23//!
24//! An `executor NAME;` slot may carry `#[cfg(...)]`, but validation does not model cfg
25//! predicates: a node referencing a slot that is cfg'd *out* while the node is cfg'd
26//! *in* surfaces as rustc's `cannot find value NAME`, not a macro error — don't gate an
27//! executor slot more restrictively than the nodes that reference it.
28//! `executor EXEC;` emits a `pub static EXEC: SpawnerSlot`; the app fills it with a
29//! `SendSpawner` (`InterruptExecutor::start`, `Spawner::make_send`) before
30//! `Supervisor::start`, and nodes carrying `executor: EXEC` spawn through it instead
31//! of the supervisor's own executor (their futures must be `Send`; an unfilled slot
32//! fails the spawn with `SpawnError::Busy`).
33//! A pool is emitted as `ElasticPool<P>`, so the macro needs the policy type `P`. By
34//! default it derives `P` from a `Ty::new(..)`-shaped `policy:` value (e.g.
35//! `DeferredShrink::new(..)` => `P = DeferredShrink`). Give `policy: <Ty> = <expr>` to
36//! state `P` explicitly when the value isn't that shape — a const, a free fn, a builder
37//! chain (`X::new(..).with(..)`), or a qualified path.
38//! `spawn:` takes a path or a partial call to a task fn taking the node **first**
39//! (`spawn: f` => `s.spawn(f(&NAME)?)`; `spawn: f(a)` => `s.spawn(f(&NAME, a)?)`), or,
40//! for a node, a closure / ready spawn fn emitted verbatim (for anything that doesn't
41//! fit that shape). A pool's `spawn:` is the same path/partial-call form with `&POOL[j]`
42//! injected first, via a generated `spawn_<pool>::<j>` glue fn; a pool has no closure
43//! form (members are instantiated per index).
44//!
45//! `task:` takes the same path/partial-call forms but names a **plain async worker
46//! fn** — possibly generic (turbofish or inferred) — instead of a hand-written
47//! `#[embassy_executor::task]`. The macro stamps a concrete shell task per
48//! declaration (embassy forbids generic tasks: one static `TaskPool` per concrete
49//! future type), sized by `pool_size:` on a node (default 1) or by the member count
50//! on a pool. Worker args are evaluated **inside the shell** — at the task's first
51//! poll, on the node's own executor — so cross-node data should go through awaited
52//! accessors, and a cross-core node builds its resources on its own core. `task:`
53//! and `spawn:` are mutually exclusive; `pool_size:` requires `task:`.
54//!
55//! **Prefer `task:`** — no attribute boilerplate, generic workers, auto-sized pool
56//! shells, and it is the only form supporting `resources:`; the shell inlines into
57//! the same poll and its `TaskPool` replaces the one the attribute would emit.
58//! `spawn:` remains for: a fn that already carries `#[embassy_executor::task]` and
59//! can't be de-attributed (another crate); a task also spawned outside the graph
60//! (sharing its one `TaskPool` instead of duplicating it as a shell); the verbatim
61//! closure form (custom spawn-time logic); and args that must be evaluated at
62//! spawn time on the supervisor's executor rather than at the shell's first poll.
63//! Worked examples: README "`spawn:` vs `task:` — which to use".
64//!
65//! Two `task:` footguns, spelled out because nothing warns about either:
66//!
67//! * a partial-call **extra that can be missing at first poll is a task-side
68//!   panic**, not a failed spawn — extras are for infallible accessors. A value
69//!   that might not exist yet belongs in `resources:` (a `shared` entry for a
70//!   fan-out handle), where the pre-spawn gate turns "missing" into a clean
71//!   `SpawnError::Busy`;
72//! * a **verbatim-closure `spawn:` node is invisible to the trace/name glue** —
73//!   the closure owns the `SpawnToken`, so `adopt`/`stamp_name` is YOUR job
74//!   inside it, and a stable proc-macro cannot emit a warning when you forget.
75//!
76//! `resources: [RES: [local] [shared|consume] Type, ..]` (requires `task:`)
77//! threads **owned resources from `main`** into the worker instead of re-acquiring
78//! them inside the task (`Peripherals::steal()`). Each entry emits a
79//! `pub static RES` slot at the declaration site; `main` moves the
80//! resource in with `RES.provide(..)` (consuming the `Peripherals` field — the
81//! compile-time exclusive-ownership guarantee), the generated glue `take()`s it just
82//! before the spawn (an unprovided slot fails `Supervisor::start` with
83//! `SpawnError::Busy` after a bounded wait — fail-closed, not a task-side panic),
84//! and the shell passes the worker `&mut Type` (after the node arg, in declared
85//! order, before any partial-call extras) and `restore()`s the value after the
86//! worker returns, so a Terminate respawn re-takes the *same instance*. Take-kind
87//! slot names are statics: unique across the graph. Entries may carry per-entry
88//! `#[cfg(...)]` (the slot, gate, glue, shell param, and worker-call argument all
89//! follow it — gate the worker fn's matching parameter with the same `#[cfg]`).
90//!
91//! Per-entry kind markers refine that default (order-free; `local` composes with
92//! either of the mutually-exclusive `consume`/`shared`):
93//!
94//! * `consume` — the worker receives the value **by value** and no restore is
95//!   emitted: the slot stays empty after the task exits, so the worker may *drop*
96//!   the resource at teardown (a driver whose `Drop` releases pins/DMA) and a
97//!   respawn fail-closes (`SpawnError::Busy`) until the application `provide()`s
98//!   a fresh value — the pattern for resources rebuilt each run (e.g. radio
99//!   driver objects that go stale across a power cycle).
100//! * `shared` — a fan-out slot for a `Copy` handle (an `embassy_net::Stack`, a
101//!   `&'static` shared-bus ref): the glue copies the value out non-destructively
102//!   (`get()` — `T: Copy` enforced by its bound), the worker receives it by
103//!   value, no restore, and the slot STAYS FILLED — so any number of nodes
104//!   (and whole `pool`s: the only `resources:` kind pools accept, `task:` pools
105//!   only) may declare the SAME slot name. The static is emitted once, with the
106//!   union of the declaring sites' cfg predicates; every re-declaration must
107//!   repeat the kinds + type verbatim.
108//! * `local` — **requires the non-default `local-resources` feature** (of the
109//!   supervisor crate, forwarded here): the slot is the graph-site
110//!   `__SvLocalResourceSlot` type instead of `ResourceSlot` — same protocol, no
111//!   `T: Send` bound, for `!Send` driver handles
112//!   (`RefCell`-/`NoopRawMutex`-based). Feature-gated because it is the one
113//!   graph form that emits `unsafe` code into the CONSUMER'S crate: an
114//!   `unsafe impl Sync` whose soundness is the **single-core contract** (all
115//!   `provide`/`take`/`restore` of the slot on one core). It cannot combine
116//!   with `executor:` (a `SendSpawner`-routed node needs a `Send` future —
117//!   macro error), and a consumer crate forbidding `unsafe_code` cannot use
118//!   `local` (the assertion lands in *its* code, like the `trace-hooks`
119//!   symbols).
120//!
121//! `slot_timeout: MS` (node and pool; milliseconds ≥ 1) overrides the node's
122//! pre-spawn wait bound for its `executor:` slot and `resources:` gates (default
123//! 100 ms — sized for "provided before `start()`"). Raise it for consumers of a
124//! **provider node** — a first-in-topo node whose worker *builds* the resources
125//! at runtime and `provide()`s them (the graph-native `hw_init`): size the
126//! timeout to the provider's async build time and the gate wait becomes a
127//! rendezvous instead of a `Busy`. See the README's provider-node recipe.
128//!
129//! A graph holds at most **256 node slots** (including pool members): all graph
130//! indices are `u8`, and the macro rejects a larger declaration at expansion.
131//!
132//! Nodes, pools, and individual deps may carry `#[cfg(...)]` attributes. A
133//! proc-macro can't evaluate `cfg`, so the node array is a fixed-length
134//! `[Option<&TaskNode>; M]` over all declared slots (each entry `Some`/`None` via a
135//! cfg-expression), the dep table is cfg-aware per-dep, and the order runs through
136//! `topo_sort_const` at const-eval (after cfg). Absent nodes are skipped at runtime.
137//!
138//! Generated items (at the call site): one `pub static` per `node`, a `[TaskNode; K]`
139//! array + `spawn_<pool>` glue fn + `<POOL>_POOL` `ElasticPool` + the structural
140//! `pub const`s `<POOL>_MIN` / `<POOL>_MAX` / `<POOL>_MEMBERS` (usize; for
141//! const-context sizing downstream — a `const` cannot read them off the member
142//! `static`) per `pool`, one slot `pub static` per `resources:` entry (shared
143//! entries: one per unique name; plus, iff any entry is `local`, the
144//! `__SvLocalResourceSlot` type), plus a
145//! single `pub static GRAPH: Graph<M>` bundling the node slots, the dependency table,
146//! the topological order, and (with the `pool` feature) the pools — pass `&GRAPH` to
147//! `Supervisor::new`. The backing tables are private; read them through `GRAPH.nodes`
148//! / `GRAPH.deps` / `GRAPH.order` / `GRAPH.pools` (node count is `GRAPH.nodes.len()`).
149//!
150//! With the supervisor's `trace` feature (forwarded here) the generated spawn glue
151//! also captures each `SpawnToken`'s task id into its node (`set_task_id`); with
152//! `metadata-names` it stamps the node name into the task Metadata. These are
153//! independent: `metadata-names` without `trace` emits a name-only spawn path
154//! (`stamp_name`, no id capture, no `_embassy_trace_*` dependency), so node names
155//! reach external tooling (rtos-trace/SystemView) without the trace recorders.
156//! With `trace-hooks` the macro additionally defines the seven `_embassy_trace_*`
157//! hook symbols at the declaration site (the supervisor crate is
158//! `forbid(unsafe_code)` and cannot), forwarding to the supervisor's `trace`
159//! recorders — requires an edition-2024 consumer, and exactly one graph declaration
160//! (or hook set) per binary.
161//!
162//! Types are referenced absolutely (`::embassy_supervisor::…`), so the consuming
163//! crate must depend on `embassy-supervisor` under its real name (not aliased).
164
165use proc_macro::TokenStream;
166use proc_macro2::TokenStream as TokenStream2;
167use quote::{format_ident, quote};
168use std::collections::{HashMap, HashSet};
169use syn::parse::{Parse, ParseStream};
170use syn::punctuated::Punctuated;
171use syn::{
172    Attribute, Expr, Ident, LitInt, Meta, Path, Result as SynResult, Token, Type, bracketed,
173};
174
175mod kw {
176    syn::custom_keyword!(node);
177    syn::custom_keyword!(pool);
178    syn::custom_keyword!(deps);
179    syn::custom_keyword!(spawn);
180    syn::custom_keyword!(task);
181    syn::custom_keyword!(pool_size);
182    syn::custom_keyword!(policy);
183    syn::custom_keyword!(min);
184    syn::custom_keyword!(max);
185    syn::custom_keyword!(disabled);
186    syn::custom_keyword!(executor);
187    syn::custom_keyword!(resources);
188    syn::custom_keyword!(slot_timeout);
189}
190
191/// The graph-site slot type emitted (once per graph, iff any `resources:` entry
192/// is `local`-marked) for `!Send` resources. A single shared name: `emit_node`
193/// types the slot statics with it and `expand` emits its definition. Like the
194/// fixed `GRAPH` static, at most one `supervisor_graph!` per module.
195const LOCAL_SLOT_TYPE: &str = "__SvLocalResourceSlot";
196
197/// A dependency reference: a node ident, optionally `#[cfg(...)]`-gated.
198#[derive(Clone)]
199struct Dep {
200    cfg: Vec<Attribute>,
201    ident: Ident,
202}
203
204/// `deps: [a, #[cfg(feature = "x")] b, …]`
205fn parse_dep_list(input: ParseStream) -> SynResult<Vec<Dep>> {
206    let content;
207    bracketed!(content in input);
208    let mut deps = Vec::new();
209    while !content.is_empty() {
210        let cfg = content.call(Attribute::parse_outer)?;
211        let ident: Ident = content.parse()?;
212        deps.push(Dep { cfg, ident });
213        if content.peek(Token![,]) {
214            content.parse::<Token![,]>()?;
215        }
216    }
217    Ok(deps)
218}
219
220/// `[Terminate, OnDemand, …]` — the bracketed mode list.
221fn parse_mode_list(input: ParseStream) -> SynResult<Vec<Ident>> {
222    let content;
223    bracketed!(content in input);
224    let punct = Punctuated::<Ident, Token![,]>::parse_terminated(&content)?;
225    Ok(punct.into_iter().collect())
226}
227
228/// How a node/pool member gets its task: `spawn:` names a hand-written
229/// `#[embassy_executor::task]` fn (path / partial call / verbatim closure), while
230/// `task:` names a **plain async fn** — possibly generic — for which the macro
231/// emits a concrete `#[embassy_executor::task]` shell (embassy forbids generic
232/// tasks: one static `TaskPool` per concrete future type, so per-type shells are
233/// the only way — the macro stamps them so the user doesn't).
234enum TaskSource {
235    /// `spawn: <expr>` — the expr *is* (or produces) the task fn.
236    Spawn(Expr),
237    /// `task: <path | partial call>` — wrap in a generated shell; args are
238    /// evaluated inside the shell (at the task's first poll, on its own executor).
239    Shell(Expr),
240}
241
242/// One `[#[cfg(..)]] NAME: [local] [shared|consume] Type` entry of a
243/// `resources:` clause. The macro emits a `pub static NAME` slot at the
244/// declaration site (`ResourceSlot<Type>`, or the graph-site local slot type
245/// for `local` entries); `main` moves the resource in with `NAME.provide(..)`
246/// (consuming the `Peripherals` field — the compile-time ownership guarantee),
247/// the generated spawn glue `take()`s (or, for `shared`, copies via `get()`) it
248/// before the spawn, and the generated shell `restore()`s it after the worker
249/// returns so a respawn re-takes the same instance (unless `consume`/`shared`).
250struct ResourceDecl {
251    /// Per-entry `#[cfg(...)]` attributes: the slot static, gate entry, glue
252    /// take/get, shell param, worker-call argument, and restore all carry them,
253    /// so a feature-varying resource set works within one node (the worker fn
254    /// must gate its matching parameter with the same `#[cfg]`).
255    cfg: Vec<Attribute>,
256    ident: Ident,
257    ty: Type,
258    /// `local` marker: the slot holds a `!Send`-capable value (`Rc`-, `RefCell`-,
259    /// `NoopRawMutex`-based driver handles). Kept as the marker `Ident` for
260    /// span-attached errors (`local` composes with neither `executor:` nor a
261    /// multi-core provider — see `parse_node`).
262    local: Option<Ident>,
263    /// `consume` marker: the worker receives the value **by value** and the shell
264    /// emits no restore — the slot is left empty when the worker exits, so a
265    /// respawn gates on an explicit re-`provide()`. For resources that must be
266    /// *dropped* at teardown (a driver whose `Drop` releases pins/DMA) or that go
267    /// stale across a power cycle and must be rebuilt each run.
268    consume: Option<Ident>,
269    /// `shared` marker: a fan-out slot for a `Copy` handle. The glue copies the
270    /// value out non-destructively (`get()`), the worker receives it **by
271    /// value**, no restore — so any number of nodes (and whole pools) may
272    /// declare the SAME slot name (the static is emitted once; re-declarations
273    /// must repeat kinds + type exactly). Mutually exclusive with `consume`.
274    shared: Option<Ident>,
275}
276
277impl ResourceDecl {
278    /// The kinds+type signature every re-declaration of a `shared` slot must
279    /// repeat verbatim (compared as token strings — same-name shared slots are
280    /// ONE static, so their declared shapes must agree).
281    fn shared_signature(&self) -> String {
282        let ty = &self.ty;
283        format!(
284            "{}shared {}",
285            if self.local.is_some() { "local " } else { "" },
286            quote!(#ty)
287        )
288    }
289}
290
291/// Peek whether the next token of a `resources:` entry is a kind *marker*
292/// (`local` / `consume` / `shared`) rather than the start of the resource
293/// `Type` itself. Contextual-keyword rule (no reserved words): the ident is a
294/// marker only when something else of the entry still follows it — i.e. it is
295/// NOT a marker when followed by `::` or `<` (it starts a path/generic type
296/// like `local::Foo` or `local<T>`) or by `,` / end-of-list (it IS the whole
297/// type, a type literally named `local`). Same fork-and-peek disambiguation as
298/// the pool `policy:` type annotation.
299fn peek_kind_marker(content: ParseStream) -> Option<Ident> {
300    if !content.peek(syn::Ident) {
301        return None;
302    }
303    let fork = content.fork();
304    let ident: Ident = fork.parse().ok()?;
305    if ident != "local" && ident != "consume" && ident != "shared" {
306        return None;
307    }
308    if fork.is_empty() || fork.peek(Token![,]) || fork.peek(Token![::]) || fork.peek(Token![<]) {
309        return None;
310    }
311    Some(ident)
312}
313
314/// `resources: [LED: Output<'static>, RUNNER: local consume Runner, …]`
315fn parse_resource_list(input: ParseStream) -> SynResult<Vec<ResourceDecl>> {
316    let content;
317    bracketed!(content in input);
318    let mut resources = Vec::new();
319    while !content.is_empty() {
320        let cfg = content.call(Attribute::parse_outer)?;
321        let ident: Ident = content.parse()?;
322        content.parse::<Token![:]>()?;
323        // Kind markers between the colon and the type, order-free: `local`
324        // plus at most one of `consume` / `shared`. A repeated marker is a
325        // declaration bug; `consume` (exclusive take, slot empty after exit)
326        // and `shared` (non-destructive fan-out copy) contradict each other.
327        let mut local: Option<Ident> = None;
328        let mut consume: Option<Ident> = None;
329        let mut shared: Option<Ident> = None;
330        while let Some(marker) = peek_kind_marker(&content) {
331            content.parse::<Ident>()?; // commit the peeked marker
332            // `local` is the one kind whose slot type carries an `unsafe impl
333            // Sync` — injecting unsafe code is an explicit opt-in, so the
334            // marker is rejected unless the (non-default) `local-resources`
335            // feature forwarded by the supervisor crate is enabled.
336            if marker == "local" && !cfg!(feature = "local-resources") {
337                return Err(syn::Error::new_spanned(
338                    &marker,
339                    "`local` resources emit an `unsafe impl Sync` — opt in by \
340                     enabling embassy-supervisor's `local-resources` feature",
341                ));
342            }
343            let slot = if marker == "local" {
344                &mut local
345            } else if marker == "consume" {
346                &mut consume
347            } else {
348                &mut shared
349            };
350            if slot.is_some() {
351                return Err(syn::Error::new_spanned(
352                    &marker,
353                    format!("duplicate `{marker}` marker"),
354                ));
355            }
356            *slot = Some(marker);
357        }
358        if let (Some(_), Some(s)) = (&consume, &shared) {
359            return Err(syn::Error::new_spanned(
360                s,
361                "`consume` and `shared` are mutually exclusive — `consume` takes \
362                 the single value out for one owner, `shared` copies it out to \
363                 any number of consumers",
364            ));
365        }
366        let ty: Type = content.parse()?;
367        resources.push(ResourceDecl {
368            cfg,
369            ident,
370            ty,
371            local,
372            consume,
373            shared,
374        });
375        if content.peek(Token![,]) {
376            content.parse::<Token![,]>()?;
377        }
378    }
379    Ok(resources)
380}
381
382struct NodeItem {
383    cfg: Vec<Attribute>,
384    ident: Ident,
385    mode: Ident,
386    deps: Vec<Dep>,
387    /// `None` = a parked node the app spawns itself (neither `spawn:` nor `task:`).
388    source: Option<TaskSource>,
389    /// `pool_size: N` on a `task:` node — sizes the generated shell's `TaskPool`
390    /// (headroom for a respawn while the previous instance is still draining).
391    pool_size: Option<LitInt>,
392    /// `resources: [NAME: Type, ..]` on a `task:` node — owned values threaded
393    /// from `main` through macro-emitted `ResourceSlot` statics into the
394    /// generated shell (which hands the worker `&mut Type` and restores the
395    /// value on exit). Empty for `spawn:`/parked nodes (enforced at parse).
396    resources: Vec<ResourceDecl>,
397    disabled: bool,
398    /// `executor: NAME` — spawn through the named [`SpawnerSlot`] (a
399    /// `SendSpawner` the app registers at runtime) instead of the supervisor's
400    /// own `Spawner`. `None` = the default executor.
401    executor: Option<Ident>,
402    /// `slot_timeout: N` (milliseconds) — overrides the node's pre-spawn
403    /// slot/gate wait bound (default 100 ms). Needed when the node's resources
404    /// are filled by a **provider node** at runtime: size it to the provider's
405    /// async build time.
406    slot_timeout: Option<LitInt>,
407}
408
409/// `executor NAME;` — declares a `pub static NAME: SpawnerSlot` the application
410/// fills with a `SendSpawner` before (or concurrently with) `Supervisor::start` (an
411/// InterruptExecutor tier, core1, ...). Nodes reference it with `executor: NAME`; the
412/// supervisor awaits the slot before spawning such a node (bounded by
413/// `SLOT_READY_TIMEOUT`, then `SpawnError::Busy`).
414struct ExecutorItem {
415    cfg: Vec<Attribute>,
416    ident: Ident,
417}
418
419struct PoolItem {
420    cfg: Vec<Attribute>,
421    ident: Ident,
422    modes: Vec<Ident>,
423    deps: Vec<Dep>,
424    /// The member task. Either a bare path (`http_task`) or a partial call carrying
425    /// extra args (`mcp_server_task(stack())`); the macro spawns member `j` as
426    /// `s.spawn(<fn>(&POOL[j] [, extra args])?)` — the node is always the first arg.
427    /// No closure form (members are instantiated per index), unlike a node's `spawn:`.
428    /// The `Shell` variant (`task:`) wraps a plain — possibly generic — async fn in
429    /// ONE generated `#[embassy_executor::task(pool_size = K)]` shell shared by all
430    /// members (they share one concrete future type, like a `spawn:` pool).
431    source: TaskSource,
432    /// The scaling policy value, emitted as the `policy:` field of the `ElasticPool`
433    /// static. The static is typed `ElasticPool<P>`, so the macro needs the policy
434    /// *type* `P`: when `policy_ty` is `None` it derives `P` from this expr via
435    /// `policy_type` (requires a `Type::new(..)` shape); when `policy_ty` is `Some`
436    /// the caller stated `P` explicitly and this expr can be any value of that type.
437    policy: Expr,
438    /// Optional explicit policy type from the `policy: <Type> = <expr>` form. `Some`
439    /// bypasses `policy_type` derivation, allowing a value the deriver can't handle
440    /// (a free fn, a const, a builder chain, a qualified path).
441    policy_ty: Option<Type>,
442    /// `executor: NAME` — spawn every member through the named [`SpawnerSlot`]
443    /// (e.g. a worker pool on the second core, scaled by this core's supervisor).
444    executor: Option<Ident>,
445    /// Pool `resources:` — **`shared` entries only** (enforced at parse): each
446    /// member's glue copies the same `Copy` handle out non-destructively, so
447    /// members don't contend (the reason non-shared kinds stay rejected).
448    resources: Vec<ResourceDecl>,
449    /// `slot_timeout: N` (milliseconds) — applied to every member (see the
450    /// node field of the same name).
451    slot_timeout: Option<LitInt>,
452    min: LitInt,
453    max: LitInt,
454}
455
456// Both variants embed a large `syn::Expr` (and `PoolItem` a bit more), so their sizes
457// are close but unequal — enough for `large_enum_variant` to flag the gap. This AST is
458// parsed once and lives only briefly in a `Vec` during expansion, so boxing a variant
459// to shave a few bytes per element buys nothing real; suppress the lint instead of
460// paying a heap allocation.
461#[allow(clippy::large_enum_variant)]
462enum Item {
463    Node(NodeItem),
464    Pool(PoolItem),
465    Executor(ExecutorItem),
466}
467
468/// The parsed macro input: the list of `node`/`pool` declarations, in source order.
469/// Named `GraphSpec` (not `Graph`) to stay distinct from the *emitted* public type
470/// [`embassy_supervisor::Graph`] that `expand` produces as the `GRAPH` static.
471struct GraphSpec {
472    items: Vec<Item>,
473}
474
475/// An item's `resources:` entries (nodes and pools both carry them; an
476/// `executor` slot has none) — for the graph-wide pre-passes in `expand`.
477fn item_resources(item: &Item) -> &[ResourceDecl] {
478    match item {
479        Item::Node(n) => &n.resources,
480        Item::Pool(p) => &p.resources,
481        Item::Executor(_) => &[],
482    }
483}
484
485/// An item's own name + cfg attributes (for shared-slot bookkeeping/docs).
486fn item_ident_cfg(item: &Item) -> Option<(&Ident, &[Attribute])> {
487    match item {
488        Item::Node(n) => Some((&n.ident, &n.cfg)),
489        Item::Pool(p) => Some((&p.ident, &p.cfg)),
490        Item::Executor(_) => None,
491    }
492}
493
494impl Parse for GraphSpec {
495    fn parse(input: ParseStream) -> SynResult<Self> {
496        let mut items = Vec::new();
497        while !input.is_empty() {
498            let cfg = input.call(Attribute::parse_outer)?;
499            if input.peek(kw::node) {
500                items.push(Item::Node(parse_node(input, cfg)?));
501            } else if input.peek(kw::pool) {
502                items.push(Item::Pool(parse_pool(input, cfg)?));
503            } else if input.peek(kw::executor) {
504                // `executor NAME;` — a runtime-filled SendSpawner slot; nodes
505                // carrying `executor: NAME` spawn through it (the supervisor awaits
506                // the slot before spawning them).
507                input.parse::<kw::executor>()?;
508                let ident: Ident = input.parse()?;
509                input.parse::<Token![;]>()?;
510                items.push(Item::Executor(ExecutorItem { cfg, ident }));
511            } else {
512                return Err(input.error(
513                    "expected `node`, `pool`, or `executor` (optionally `#[cfg(...)]`-prefixed)",
514                ));
515            }
516        }
517        Ok(GraphSpec { items })
518    }
519}
520
521// node IDENT = MODE, deps: [..] [, spawn: <expr>] [, disabled];
522fn parse_node(input: ParseStream, cfg: Vec<Attribute>) -> SynResult<NodeItem> {
523    input.parse::<kw::node>()?;
524    let ident: Ident = input.parse()?;
525    input.parse::<Token![=]>()?;
526    let mode: Ident = input.parse()?;
527    input.parse::<Token![,]>()?;
528    input.parse::<kw::deps>()?;
529    input.parse::<Token![:]>()?;
530    let deps = parse_dep_list(input)?;
531
532    let mut spawn = None;
533    let mut task: Option<(kw::task, Expr)> = None;
534    let mut pool_size = None;
535    let mut disabled = false;
536    let mut executor = None;
537    let mut resources: Option<(kw::resources, Vec<ResourceDecl>)> = None;
538    let mut slot_timeout = None;
539    while input.peek(Token![,]) {
540        input.parse::<Token![,]>()?;
541        if input.peek(kw::spawn) {
542            input.parse::<kw::spawn>()?;
543            input.parse::<Token![:]>()?;
544            spawn = Some(input.parse::<Expr>()?);
545        } else if input.peek(kw::task) {
546            let k = input.parse::<kw::task>()?;
547            input.parse::<Token![:]>()?;
548            task = Some((k, input.parse::<Expr>()?));
549        } else if input.peek(kw::pool_size) {
550            input.parse::<kw::pool_size>()?;
551            input.parse::<Token![:]>()?;
552            pool_size = Some(input.parse::<LitInt>()?);
553        } else if input.peek(kw::disabled) {
554            input.parse::<kw::disabled>()?;
555            disabled = true;
556        } else if input.peek(kw::executor) {
557            input.parse::<kw::executor>()?;
558            input.parse::<Token![:]>()?;
559            executor = Some(input.parse::<Ident>()?);
560        } else if input.peek(kw::resources) {
561            let k = input.parse::<kw::resources>()?;
562            input.parse::<Token![:]>()?;
563            resources = Some((k, parse_resource_list(input)?));
564        } else if input.peek(kw::slot_timeout) {
565            input.parse::<kw::slot_timeout>()?;
566            input.parse::<Token![:]>()?;
567            slot_timeout = Some(input.parse::<LitInt>()?);
568        } else {
569            return Err(input.error(
570                "expected `spawn:`, `task:`, `pool_size:`, `executor:`, `resources:`, \
571                 `slot_timeout:`, or `disabled`",
572            ));
573        }
574    }
575    input.parse::<Token![;]>()?;
576
577    // `slot_timeout: 0` would make every gated spawn fail instantly — reject it
578    // as the declaration bug it is (`base10_parse::<u64>` also rejects suffixed
579    // or oversized literals with a span-attached error).
580    if let Some(st) = &slot_timeout {
581        if st.base10_parse::<u64>()? == 0 {
582            return Err(syn::Error::new_spanned(
583                st,
584                "`slot_timeout:` must be at least 1 (milliseconds)",
585            ));
586        }
587    }
588
589    // Exactly one of `spawn:` / `task:` may pick the node's task.
590    if let (Some(_), Some((k, _))) = (&spawn, &task) {
591        return Err(syn::Error::new_spanned(
592            k,
593            "`task:` and `spawn:` are mutually exclusive — `spawn:` names a \
594             hand-written `#[embassy_executor::task]` fn, `task:` generates one",
595        ));
596    }
597    // `pool_size:` sizes the generated shell's TaskPool; without `task:` there is
598    // no generated shell to size (a `spawn:` task fn declares its own).
599    if let (Some(ps), None) = (&pool_size, &task) {
600        return Err(syn::Error::new_spanned(
601            ps,
602            "`pool_size:` requires `task:` — a `spawn:` task fn sets its own \
603             `#[embassy_executor::task(pool_size = ...)]`",
604        ));
605    }
606    // `resources:` only makes sense with `task:`: the generated shell is what
607    // takes the values out of their slots at spawn and restores them after the
608    // worker returns. A hand-written `spawn:` fn (or a parked node) manages its
609    // own arguments.
610    if let Some((k, decls)) = &resources {
611        if task.is_none() {
612            return Err(syn::Error::new_spanned(
613                k,
614                "`resources:` requires `task:` — resources are handed to the \
615                 generated shell as owned arguments and restored by it; a \
616                 `spawn:` task fn manages its own arguments",
617            ));
618        }
619        if decls.is_empty() {
620            return Err(syn::Error::new_spanned(
621                k,
622                "`resources:` must declare at least one `NAME: Type` entry",
623            ));
624        }
625        // Duplicate names within one node would emit two statics with the same
626        // ident; catch it here with a clearer message than rustc's E0428.
627        for (i, d) in decls.iter().enumerate() {
628            if decls[..i].iter().any(|prev| prev.ident == d.ident) {
629                return Err(syn::Error::new_spanned(
630                    &d.ident,
631                    format!("duplicate resource name `{}`", d.ident),
632                ));
633            }
634        }
635    }
636    if let Some(ps) = &pool_size {
637        if ps.base10_parse::<usize>()? == 0 {
638            return Err(syn::Error::new_spanned(
639                ps,
640                "`pool_size:` must be at least 1",
641            ));
642        }
643    }
644    // A `local` resource makes the shell future hold a `!Send`-capable value, and
645    // an `executor:`-routed node spawns through a `SendSpawner`, whose `spawn`
646    // requires a `Send` future. Reject here with the reason instead of letting
647    // rustc surface it as an opaque `F: Send` bound failure deep in the glue.
648    if let (Some((_, decls)), Some(ex)) = (&resources, &executor) {
649        if let Some(l) = decls.iter().find_map(|d| d.local.as_ref()) {
650            return Err(syn::Error::new_spanned(
651                l,
652                format!(
653                    "`local` resources cannot be combined with `executor: {ex}` — a \
654                     local slot exists to carry `!Send` values, and a node routed \
655                     through a `SpawnerSlot` (`SendSpawner`) must have a `Send` \
656                     future; run the node on the supervisor's own executor"
657                ),
658            ));
659        }
660    }
661    let source = match (spawn, task) {
662        (Some(e), _) => Some(TaskSource::Spawn(e)),
663        (None, Some((_, e))) => Some(TaskSource::Shell(e)),
664        (None, None) => None,
665    };
666
667    Ok(NodeItem {
668        cfg,
669        ident,
670        mode,
671        deps,
672        source,
673        pool_size,
674        disabled,
675        executor,
676        resources: resources.map(|(_, decls)| decls).unwrap_or_default(),
677        slot_timeout,
678    })
679}
680
681// pool IDENT = [MODES], deps: [..][, executor: EXEC], spawn: <fn>, policy: EXPR, min: N, max: M;
682fn parse_pool(input: ParseStream, cfg: Vec<Attribute>) -> SynResult<PoolItem> {
683    input.parse::<kw::pool>()?;
684    let ident: Ident = input.parse()?;
685    input.parse::<Token![=]>()?;
686    let modes = parse_mode_list(input)?;
687    input.parse::<Token![,]>()?;
688    input.parse::<kw::deps>()?;
689    input.parse::<Token![:]>()?;
690    let deps = parse_dep_list(input)?;
691    input.parse::<Token![,]>()?;
692    // Optional `executor: NAME,` — run the whole pool on the named SpawnerSlot's
693    // executor (e.g. a worker pool on the second core, scaled from this one).
694    let executor = if input.peek(kw::executor) {
695        input.parse::<kw::executor>()?;
696        input.parse::<Token![:]>()?;
697        let ex: Ident = input.parse()?;
698        input.parse::<Token![,]>()?;
699        Some(ex)
700    } else {
701        None
702    };
703    // The member task: a path, or a partial call supplying extra args (the macro
704    // injects `&POOL[j]` as the first argument in either case). `spawn:` names a
705    // hand-written `#[embassy_executor::task(pool_size = K)]` fn; `task:` names a
706    // plain (possibly generic) async fn the macro wraps in ONE generated shell
707    // task sized `pool_size = K`.
708    let source = if input.peek(kw::task) {
709        input.parse::<kw::task>()?;
710        input.parse::<Token![:]>()?;
711        TaskSource::Shell(input.parse()?)
712    } else {
713        input.parse::<kw::spawn>()?;
714        input.parse::<Token![:]>()?;
715        TaskSource::Spawn(input.parse()?)
716    };
717    input.parse::<Token![,]>()?;
718    // Pool `resources:` — `shared` entries only. A take-kind slot holds ONE
719    // value and pool members all run the same worker: they would contend for
720    // that single instance and every member past the first would fail its
721    // spawn. A `shared` entry is a non-destructive fan-out copy, so members
722    // don't contend — each glue `get()`s the same `Copy` handle.
723    let resources = if input.peek(kw::resources) {
724        input.parse::<kw::resources>()?;
725        input.parse::<Token![:]>()?;
726        let decls = parse_resource_list(input)?;
727        if let Some(bad) = decls.iter().find(|d| d.shared.is_none()) {
728            return Err(syn::Error::new_spanned(
729                &bad.ident,
730                "only `shared` resources are supported on `pool` — members \
731                 would contend for a take-kind slot's single instance; declare \
732                 take/consume resources per-node",
733            ));
734        }
735        input.parse::<Token![,]>()?;
736        decls
737    } else {
738        Vec::new()
739    };
740    input.parse::<kw::policy>()?;
741    input.parse::<Token![:]>()?;
742    // Optional explicit policy type: `policy: <Ty> = <expr>`. Fork to see if a `Type`
743    // is followed by `=`; if so it's an annotation (commit on the real stream + eat the
744    // `=`), otherwise rewind and treat the whole thing as the value expr (type derived
745    // from it in `emit_pool`). For the common `Ty::new(..)` value the fork parses only a
746    // partial type and then sees `(`, not `=`, so it correctly falls back to the derive
747    // path — this keeps the bare form working unchanged.
748    let policy_ty = {
749        let fork = input.fork();
750        if fork.parse::<Type>().is_ok() && fork.peek(Token![=]) {
751            let ty: Type = input.parse()?;
752            input.parse::<Token![=]>()?;
753            Some(ty)
754        } else {
755            None
756        }
757    };
758    let policy: Expr = input.parse()?;
759    input.parse::<Token![,]>()?;
760    input.parse::<kw::min>()?;
761    input.parse::<Token![:]>()?;
762    let min: LitInt = input.parse()?;
763    input.parse::<Token![,]>()?;
764    input.parse::<kw::max>()?;
765    input.parse::<Token![:]>()?;
766    let max: LitInt = input.parse()?;
767    // Optional trailing `, slot_timeout: N` (milliseconds, ≥ 1) — every member's
768    // pre-spawn slot/gate wait bound (see the node clause of the same name).
769    let slot_timeout = if input.peek(Token![,]) && input.peek2(kw::slot_timeout) {
770        input.parse::<Token![,]>()?;
771        input.parse::<kw::slot_timeout>()?;
772        input.parse::<Token![:]>()?;
773        let st: LitInt = input.parse()?;
774        if st.base10_parse::<u64>()? == 0 {
775            return Err(syn::Error::new_spanned(
776                &st,
777                "`slot_timeout:` must be at least 1 (milliseconds)",
778            ));
779        }
780        Some(st)
781    } else {
782        None
783    };
784    input.parse::<Token![;]>()?;
785    // Same `Send` reasoning as the node-side check: a `local` (i.e. `!Send`able)
786    // resource cannot ride members routed through a `SendSpawner`.
787    if let Some(ex) = &executor {
788        if let Some(l) = resources.iter().find_map(|d| d.local.as_ref()) {
789            return Err(syn::Error::new_spanned(
790                l,
791                format!(
792                    "`local` resources cannot be combined with `executor: {ex}` — a \
793                     local slot exists to carry `!Send` values, and a pool routed \
794                     through a `SpawnerSlot` (`SendSpawner`) must have `Send` \
795                     futures; run the pool on the supervisor's own executor"
796                ),
797            ));
798        }
799    }
800    Ok(PoolItem {
801        cfg,
802        ident,
803        modes,
804        deps,
805        source,
806        policy,
807        policy_ty,
808        executor,
809        resources,
810        slot_timeout,
811        min,
812        max,
813    })
814}
815
816/// The node/pool name string: ident lowercased with `_`→`-` (`WIFI_CTRL` → "wifi-ctrl").
817fn name_string(ident: &Ident) -> String {
818    ident.to_string().to_lowercase().replace('_', "-")
819}
820
821/// Build a task-call expression with leading arguments injected ahead of the
822/// user-supplied extras — the node ref (`&NODE` / `&POOL[i]`) first, then the
823/// item's threaded `resources:` values: a bare path `f` => `f(lead..)`; a
824/// partial call `f(a, b)` => `f(lead.., a, b)`.
825fn inject_call_with(task: &Expr, lead: &[TokenStream2]) -> SynResult<TokenStream2> {
826    match task {
827        Expr::Path(_) => Ok(quote!(#task(#(#lead),*))),
828        Expr::Call(c) => {
829            let f = &c.func;
830            let args = c.args.iter();
831            Ok(quote!(#f(#(#lead),* #(, #args)*)))
832        }
833        other => Err(syn::Error::new_spanned(
834            other,
835            "expected a task-fn path or a partial call like `f(extra_args)`",
836        )),
837    }
838}
839
840/// Combine an item's `#[cfg(...)]` attributes into one predicate (`all(..)` if
841/// several), used to gate its `GRAPH.nodes` slot to `Some`/`None`. `None` = always present.
842fn cfg_predicate(attrs: &[Attribute]) -> Option<TokenStream2> {
843    let preds: Vec<TokenStream2> = attrs
844        .iter()
845        .filter_map(|a| match &a.meta {
846            Meta::List(ml) if ml.path.is_ident("cfg") => Some(ml.tokens.clone()),
847            _ => None,
848        })
849        .collect();
850    match preds.len() {
851        0 => None,
852        1 => Some(preds[0].clone()),
853        _ => Some(quote!(all(#(#preds),*))),
854    }
855}
856
857/// Gate-array tokens for a `resources:` list: the element list (each entry
858/// `#[cfg]`-gated — cfg on array elements is stable, same as the deps table)
859/// and a matching length expression. A cfg'd-out element must also subtract
860/// from the fixed array length, so with any per-entry cfg the length becomes a
861/// sum of cfg-block 1/0 terms (the `GRAPH.nodes` Some/None trick, in const
862/// position); without, it stays the plain count.
863fn gate_tokens(resources: &[ResourceDecl]) -> (TokenStream2, Vec<TokenStream2>) {
864    let gate_refs: Vec<TokenStream2> = resources
865        .iter()
866        .map(|r| {
867            let cfg = &r.cfg;
868            let res = &r.ident;
869            quote!(#(#cfg)* &#res)
870        })
871        .collect();
872    let any_cfg = resources.iter().any(|r| cfg_predicate(&r.cfg).is_some());
873    let len = if any_cfg {
874        let terms: Vec<TokenStream2> = resources
875            .iter()
876            .map(|r| match cfg_predicate(&r.cfg) {
877                None => quote!(1usize),
878                Some(pred) => quote!({
879                    #[cfg(#pred)]
880                    {
881                        1usize
882                    }
883                    #[cfg(not(#pred))]
884                    {
885                        0usize
886                    }
887                }),
888            })
889            .collect();
890        quote!(0usize #(+ #terms)*)
891    } else {
892        let n = resources.len();
893        quote!(#n)
894    };
895    (len, gate_refs)
896}
897
898/// Extract the policy *type* from a `Type::new(..)` constructor expression. Only used
899/// on the derive path (no explicit `policy: <Ty> = ..` annotation); the type is the
900/// call's path minus its last segment (`DeferredShrink::new` -> `DeferredShrink`).
901fn policy_type(expr: &Expr) -> SynResult<Path> {
902    if let Expr::Call(call) = expr
903        && let Expr::Path(p) = &*call.func
904    {
905        let n = p.path.segments.len();
906        if n >= 2 {
907            let segs: Punctuated<_, Token![::]> =
908                p.path.segments.iter().take(n - 1).cloned().collect();
909            return Ok(Path {
910                leading_colon: p.path.leading_colon,
911                segments: segs,
912            });
913        }
914    }
915    Err(syn::Error::new_spanned(
916        expr,
917        "pool `policy:` must be a `Type::new(..)` constructor (e.g. `DeferredShrink::new(..)`), \
918         or give the type explicitly: `policy: <Type> = <expr>`",
919    ))
920}
921
922/// One emitted node slot, in final index order.
923struct Slot {
924    /// Presence predicate (`None` = unconditional), gates the node slot (`GRAPH.nodes`) entry.
925    cfg_pred: Option<TokenStream2>,
926    /// `&NODE` or `&POOL[j]`.
927    reference: TokenStream2,
928    /// Raw deps, resolved to indices in the second pass.
929    deps: Vec<Dep>,
930}
931
932/// The `Option<fn(..)>` spawn expression for a node. `None` (no `spawn:`) is a
933/// parked node the app spawns itself. A path or partial call is a task fn taking
934/// `&NODE` first (plus any given args); the macro wraps it as
935/// `|s| { s.spawn(<task>(&NODE, ..)?); Ok(()) }`. Anything else (a closure, or a
936/// ready spawn fn) is emitted verbatim. Every form is cast to `spawn_fn` so it
937/// coerces cleanly inside `Option::Some(..)`.
938fn node_spawn(
939    ident: &Ident,
940    spawn: &Option<Expr>,
941    executor: &Option<Ident>,
942    resources: &[ResourceDecl],
943    spawn_fn: &TokenStream2,
944) -> SynResult<TokenStream2> {
945    // `resources:` take-prelude + the taken values as extra shell arguments.
946    // Taking here — in the glue, BEFORE the spawn — is the point: an unprovided
947    // slot fails `Supervisor::start` with `SpawnError::Busy` (the supervisor
948    // logs the node name), instead of panicking inside an already-spawned task.
949    // The values ride into the task as ordinary `#[embassy_executor::task]`
950    // arguments (embassy stores them in the shell's TaskPool slot). A `shared`
951    // entry copies the value out non-destructively (`get()` — the slot stays
952    // filled for the other consumers) instead of `take()`ing it; `get`'s
953    // `T: Copy` bound is what enforces "shared handles must be Copy".
954    let take_prelude: Vec<TokenStream2> = resources
955        .iter()
956        .enumerate()
957        .map(|(i, r)| {
958            let cfg = &r.cfg;
959            let res = &r.ident;
960            let var = format_ident!("__r{}", i);
961            let getter = if r.shared.is_some() {
962                quote!(get)
963            } else {
964                quote!(take)
965            };
966            quote! {
967                #(#cfg)*
968                let #var = #res
969                    .#getter()
970                    .ok_or(::embassy_executor::SpawnError::Busy)?;
971            }
972        })
973        .collect();
974    // Per-entry `#[cfg]` rides on the call ARGUMENT too (stable in call
975    // position, like the cfg'd array elements in the deps table), so a
976    // cfg'd-out entry vanishes from the glue, the shell signature, and the
977    // worker call consistently.
978    let res_args: Vec<TokenStream2> = resources
979        .iter()
980        .enumerate()
981        .map(|(i, r)| {
982            let cfg = &r.cfg;
983            let var = format_ident!("__r{}", i);
984            quote!(#(#cfg)* #var)
985        })
986        .collect();
987    Ok(match (spawn, executor) {
988        (None, None) => quote!(::core::option::Option::None),
989        // `executor:` needs the macro to perform the spawn, so it composes only
990        // with the path / partial-call `spawn:` forms below.
991        (None, Some(ex)) => {
992            return Err(syn::Error::new_spanned(
993                ex,
994                "`executor:` requires a `spawn:` (a parked node is spawned by the \
995                 application, which picks its own spawner)",
996            ));
997        }
998        // A path or a partial call: a task fn taking `&NODE` first (plus any
999        // given args); generate `|s| { s.spawn(<task>(&NODE, ..)?); Ok(()) }`.
1000        // With `executor: NAME` the glue ignores the supervisor's `Spawner` and
1001        // spawns through the named `SpawnerSlot` (a `SendSpawner` the app
1002        // registers at runtime): an unfilled slot fails the spawn with
1003        // `SpawnError::Busy` — loud misconfiguration, not a missing task. The
1004        // task future must then be `Send` (enforced by `SendSpawner::spawn`).
1005        (Some(e @ (Expr::Path(_) | Expr::Call(_))), executor) => {
1006            let mut lead: Vec<TokenStream2> = vec![quote!(&#ident)];
1007            lead.extend(res_args.iter().cloned());
1008            let call = inject_call_with(e, &lead)?;
1009            match executor {
1010                None => {
1011                    let stmts = spawn_stmts(&call, &quote!(&#ident), &quote!(s));
1012                    quote!(::core::option::Option::Some(
1013                        (|s| {
1014                            #(#take_prelude)*
1015                            #stmts
1016                            ::core::result::Result::Ok(())
1017                        }) as #spawn_fn
1018                    ))
1019                }
1020                Some(ex) => {
1021                    let stmts = spawn_stmts(&call, &quote!(&#ident), &quote!(__sp));
1022                    quote!(::core::option::Option::Some(
1023                        (|_s| {
1024                            // The supervisor awaits this slot's `ready()` before
1025                            // invoking the glue (the node carries `.with_executor(&EX)`
1026                            // and the bring-up bounds the wait), so `get()` is already
1027                            // filled; `ok_or` is the belt-and-braces unfilled guard.
1028                            // Resources are taken AFTER the spawner guard, so an
1029                            // unfilled executor never consumes (and strands) them.
1030                            let __sp = #ex
1031                                .get()
1032                                .ok_or(::embassy_executor::SpawnError::Busy)?;
1033                            #(#take_prelude)*
1034                            #stmts
1035                            ::core::result::Result::Ok(())
1036                        }) as #spawn_fn
1037                    ))
1038                }
1039            }
1040        }
1041        (Some(_), Some(ex)) => {
1042            return Err(syn::Error::new_spanned(
1043                ex,
1044                "`executor:` cannot be combined with a verbatim spawn closure (the \
1045                 closure owns the spawn; use the named SpawnerSlot inside it instead)",
1046            ));
1047        }
1048        // Anything else (a closure, or a ready spawn fn) is emitted verbatim.
1049        // NOTE: with the `trace` feature such a node is not auto-mapped — the
1050        // closure owns the SpawnToken; call `adopt`/`set_task_id` in it yourself.
1051        (Some(e), None) => quote!(::core::option::Option::Some((#e) as #spawn_fn)),
1052    })
1053}
1054
1055/// The spawn statement(s) for the generated glue. Plain `s.spawn(<call>?)`
1056/// normally; with the `trace` feature the `SpawnToken` is bound first so its task
1057/// id can be captured into the node (`set_task_id`) — the id→node mapping the
1058/// supervisor's `trace` recorders resolve against (in embassy-executor 0.10 the
1059/// task-fn call returns `Result<SpawnToken, SpawnError>` and `Spawner::spawn`
1060/// itself is infallible, so the token is available between the two).
1061///
1062/// Three shapes, resolved at expansion by the macro crate's own features:
1063/// * `trace` on → bind the token and `adopt` it (`set_task_id` + name stamp under
1064///   `metadata-names`).
1065/// * `trace` off but `metadata-names` on → bind the token and `stamp_name` only:
1066///   the node name reaches the task Metadata (for rtos-trace/SystemView) with no id
1067///   capture and no dependency on the `_embassy_trace_*` hooks.
1068/// * neither → plain infallible spawn.
1069fn spawn_stmts(call: &TokenStream2, node_ref: &TokenStream2, sp: &TokenStream2) -> TokenStream2 {
1070    if cfg!(feature = "trace") {
1071        // `adopt` = set_task_id + (under metadata-names) Metadata name stamp.
1072        quote! {
1073            let __token = #call?;
1074            (#node_ref).adopt(&__token);
1075            #sp.spawn(__token);
1076        }
1077    } else if cfg!(feature = "metadata-names") {
1078        // Name-only path: stamp the node name into the task Metadata, nothing else.
1079        quote! {
1080            let __token = #call?;
1081            (#node_ref).stamp_name(&__token);
1082            #sp.spawn(__token);
1083        }
1084    } else {
1085        quote!(#sp.spawn(#call?);)
1086    }
1087}
1088
1089/// Emit the `#[embassy_executor::task]` shell for a `task:` clause: a concrete,
1090/// non-generic task fn that takes only the node and awaits the user's worker with
1091/// the node injected first. This is how a **generic** worker becomes spawnable —
1092/// embassy forbids generic tasks (one static `TaskPool` per concrete future type),
1093/// so a monomorphized shell is stamped per declaration. Worker args are evaluated
1094/// inside the shell — at the task's first poll, on the node's own executor — so
1095/// the DSL never needs the arg types and a cross-core node builds its resources on
1096/// the core that runs them.
1097///
1098/// Returns the shell item and a path `Expr` naming it, which feeds the ordinary
1099/// `spawn:` path-form glue (executor routing and trace `adopt` compose unchanged).
1100fn emit_shell(
1101    owner: &Ident,
1102    cfg: &[Attribute],
1103    worker: &Expr,
1104    pool_size: usize,
1105    resources: &[ResourceDecl],
1106    cr: &TokenStream2,
1107) -> SynResult<(TokenStream2, Expr)> {
1108    if !matches!(worker, Expr::Path(_) | Expr::Call(_)) {
1109        return Err(syn::Error::new_spanned(
1110            worker,
1111            "`task:` names an async worker fn — a path or a partial call like \
1112             `worker(args)`; for a closure or a ready spawn fn use `spawn:`",
1113        ));
1114    }
1115    let shell = format_ident!("__sv_task_{}", owner.to_string().to_lowercase());
1116    // `resources:` values arrive as owned task arguments (the spawn glue took
1117    // them out of their slots); the shell keeps ownership, lends the worker
1118    // `&mut`, and restores each value to its slot after the worker returns —
1119    // i.e. after the worker's clean shutdown ack — so a Terminate respawn
1120    // re-takes the SAME instance instead of re-acquiring hardware. A `Pause`
1121    // worker parks instead of returning, so it simply retains its resources
1122    // (the restore lines below are unreachable for it — correct, same as a
1123    // hand-written parked task holding its arguments).
1124    //
1125    // A `consume` entry is forwarded to the worker BY VALUE instead — the worker
1126    // owns it (it can drop it at teardown, e.g. a driver whose `Drop` releases
1127    // pins/DMA) and no restore is emitted: the slot stays empty until the app
1128    // re-`provide()`s, which the supervisor's pre-respawn gate wait turns into
1129    // fail-closed `SpawnError::Busy` rather than a stale-value reuse.
1130    //
1131    // A `shared` entry is also by value with no restore — but because the glue
1132    // COPIED it out (`get()`), the slot stays filled; the worker's value is its
1133    // own copy of the fan-out handle.
1134    //
1135    // Per-entry `#[cfg]` rides on params, worker-call arguments, and restore
1136    // statements alike, so a cfg'd-out entry disappears from the whole chain
1137    // (the worker fn must gate its matching parameter with the same `#[cfg]`).
1138    let by_value = |r: &ResourceDecl| r.consume.is_some() || r.shared.is_some();
1139    let res_params: Vec<TokenStream2> = resources
1140        .iter()
1141        .enumerate()
1142        .map(|(i, r)| {
1143            let cfg = &r.cfg;
1144            let var = format_ident!("__r{}", i);
1145            let ty = &r.ty;
1146            if by_value(r) {
1147                quote!(#(#cfg)* #var: #ty)
1148            } else {
1149                quote!(#(#cfg)* mut #var: #ty)
1150            }
1151        })
1152        .collect();
1153    let res_leases: Vec<TokenStream2> = resources
1154        .iter()
1155        .enumerate()
1156        .map(|(i, r)| {
1157            let cfg = &r.cfg;
1158            let var = format_ident!("__r{}", i);
1159            if by_value(r) {
1160                quote!(#(#cfg)* #var)
1161            } else {
1162                quote!(#(#cfg)* &mut #var)
1163            }
1164        })
1165        .collect();
1166    let restores: Vec<TokenStream2> = resources
1167        .iter()
1168        .enumerate()
1169        .filter(|(_, r)| !by_value(r))
1170        .map(|(i, r)| {
1171            let cfg = &r.cfg;
1172            let res = &r.ident;
1173            let var = format_ident!("__r{}", i);
1174            quote!(#(#cfg)* #res.restore(#var);)
1175        })
1176        .collect();
1177    let mut lead: Vec<TokenStream2> = vec![quote!(__node)];
1178    lead.extend(res_leases);
1179    let call = inject_call_with(worker, &lead)?;
1180    // Unsuffixed literal: `#[task]`'s own parser wants a plain integer.
1181    let ps = LitInt::new(&pool_size.to_string(), proc_macro2::Span::call_site());
1182    // A diverging (`-> !`) worker makes the restore statements unreachable —
1183    // legitimate (a detached/`Pause` worker retains its resources forever), so
1184    // silence rustc's `unreachable_code` lint on the generated body. Only
1185    // emitted when there ARE trailing statements to reach.
1186    let allow_unreachable = if restores.is_empty() {
1187        quote!()
1188    } else {
1189        quote!(#[allow(unreachable_code)])
1190    };
1191    let def = quote! {
1192        #(#cfg)*
1193        #[::embassy_executor::task(pool_size = #ps)]
1194        #allow_unreachable
1195        async fn #shell(__node: &'static #cr::TaskNode #(, #res_params)*) {
1196            #call.await;
1197            #(#restores)*
1198        }
1199    };
1200    let path: Expr = syn::parse_quote!(#shell);
1201    Ok((def, path))
1202}
1203
1204/// Emit a `node`: its `pub static #ident: TaskNode` definition and its `Slot`. The
1205/// caller assigns the slot index and records the name, so this touches neither.
1206/// A `task:` node additionally emits its generated shell ahead of the static.
1207fn emit_node(
1208    n: &NodeItem,
1209    cr: &TokenStream2,
1210    spawn_fn: &TokenStream2,
1211) -> SynResult<(TokenStream2, Slot)> {
1212    let ident = &n.ident;
1213    let cfg = &n.cfg;
1214    let mode = &n.mode;
1215    let name = name_string(&n.ident);
1216    let disabled = n.disabled;
1217    let (shell_def, spawn_expr) = match &n.source {
1218        Some(TaskSource::Shell(worker)) => {
1219            let ps = match &n.pool_size {
1220                Some(l) => l.base10_parse::<usize>()?,
1221                None => 1,
1222            };
1223            let (def, path) = emit_shell(ident, cfg, worker, ps, &n.resources, cr)?;
1224            (def, Some(path))
1225        }
1226        Some(TaskSource::Spawn(e)) => (quote!(), Some(e.clone())),
1227        None => (quote!(), None),
1228    };
1229    let spawn = node_spawn(ident, &spawn_expr, &n.executor, &n.resources, spawn_fn)?;
1230    // `executor: NAME` routes the node through that SpawnerSlot; the supervisor
1231    // awaits the slot before spawning (see `TaskNode::with_executor`).
1232    let with_exec = match &n.executor {
1233        Some(ex) => quote!( .with_executor(&#ex) ),
1234        None => quote!(),
1235    };
1236    // `resources: [NAME: Type, ..]` — one `pub static NAME: ResourceSlot<Type>`
1237    // per entry (main moves the resource in with `NAME.provide(..)`), plus a
1238    // type-erased gate array wired into the node so the supervisor can await
1239    // provisioning/restore before each (re)spawn (see `TaskNode::with_resources`).
1240    // The unsized coercion `&NAME` -> `&dyn ResourceGate` happens in the static
1241    // initializer, where it is allowed.
1242    let (res_defs, with_res) = if n.resources.is_empty() {
1243        (quote!(), quote!())
1244    } else {
1245        let gates_ident = format_ident!("__SV_GATES_{}", ident);
1246        // `shared` slots are emitted once per graph in `expand` (several items
1247        // may declare the same one); only this node's exclusive (take-kind)
1248        // slots are emitted here.
1249        let slot_defs = n.resources.iter().filter(|r| r.shared.is_none()).map(|r| {
1250            let ecfg = &r.cfg;
1251            let res = &r.ident;
1252            let ty = &r.ty;
1253            // `local` entries use the graph-site slot type (emitted once per
1254            // graph in `expand`): same provide/take protocol as `ResourceSlot`
1255            // but without its `T: Send` bound, for `!Send` driver handles on a
1256            // single-core system. `consume` changes only shell codegen (by-value
1257            // arg, no restore) — the slot type is the same either way.
1258            let slot_ty = if r.local.is_some() {
1259                let local = format_ident!("{LOCAL_SLOT_TYPE}");
1260                quote!(#local<#ty>)
1261            } else {
1262                quote!(#cr::ResourceSlot<#ty>)
1263            };
1264            let doc = if r.consume.is_some() {
1265                format!(
1266                    "Resource slot for node `{ident}` (generated by `supervisor_graph!`). \
1267                         Move the resource in with `.provide(..)` before `Supervisor::start`. \
1268                         `consume`: the worker owns (and may drop) the value, so the slot is \
1269                         empty after the task exits — re-`provide()` before any respawn."
1270                )
1271            } else {
1272                format!(
1273                    "Resource slot for node `{ident}` (generated by `supervisor_graph!`). \
1274                         Move the resource in with `.provide(..)` before `Supervisor::start`."
1275                )
1276            };
1277            quote! {
1278                #(#cfg)*
1279                #(#ecfg)*
1280                #[doc = #doc]
1281                pub static #res: #slot_ty = <#slot_ty>::new();
1282            }
1283        });
1284        let (gates_len, gate_refs) = gate_tokens(&n.resources);
1285        (
1286            quote! {
1287                #(#slot_defs)*
1288                #(#cfg)*
1289                static #gates_ident: [&'static dyn #cr::ResourceGate; #gates_len] =
1290                    [#(#gate_refs),*];
1291            },
1292            quote!( .with_resources(&#gates_ident) ),
1293        )
1294    };
1295    // `slot_timeout: N` — override the node's pre-spawn slot/gate wait bound
1296    // (see `TaskNode::with_slot_timeout`; sized to a provider node's build time).
1297    let with_timeout = match &n.slot_timeout {
1298        Some(ms) => quote!( .with_slot_timeout(#cr::_export::Duration::from_millis(#ms)) ),
1299        None => quote!(),
1300    };
1301    let def = quote! {
1302        #res_defs
1303        #shell_def
1304        #(#cfg)*
1305        pub static #ident: #cr::TaskNode =
1306            #cr::TaskNode::new(#name, #cr::Mode::#mode, #spawn, #disabled)
1307                #with_exec #with_res #with_timeout;
1308    };
1309    let slot = Slot {
1310        cfg_pred: cfg_predicate(cfg),
1311        reference: quote!(&#ident),
1312        deps: n.deps.clone(),
1313    };
1314    Ok((def, slot))
1315}
1316
1317/// Emit a `pool`: the member `[TaskNode; K]` array, the `spawn_<pool>` glue fn, and
1318/// the `ElasticPool` static (returned as `defs`, in that emission order), plus the
1319/// pool-registry entry (for `GRAPH.pools`) and one `Slot` per member (members occupy
1320/// slots but aren't name-addressable, so no name is recorded).
1321fn emit_pool(
1322    p: &PoolItem,
1323    cr: &TokenStream2,
1324    spawn_fn: &TokenStream2,
1325) -> SynResult<(Vec<TokenStream2>, TokenStream2, Vec<Slot>)> {
1326    let ident = &p.ident;
1327    let cfg = &p.cfg;
1328    let lname = name_string(&p.ident);
1329    let pool_static = format_ident!("{}_POOL", ident);
1330    let k = p.modes.len();
1331
1332    // Validate the scaling bounds at expansion time. `base10_parse::<u8>` also
1333    // rejects values > 255 with a span-attached error (the `ElasticPool` fields are
1334    // `u8`). `min > max` makes the policy contradict itself; `max > k` is a ceiling
1335    // the pool can never reach (there are only `k` member slots) — both are
1336    // declaration bugs, caught here rather than surfacing as odd runtime scaling.
1337    // `max < k` is allowed (spare declared members below the ceiling), as is
1338    // `min: 0` (the policy may scale the pool all the way down when idle).
1339    let min_v: u8 = p.min.base10_parse()?;
1340    let max_v: u8 = p.max.base10_parse()?;
1341    if min_v > max_v {
1342        return Err(syn::Error::new_spanned(
1343            &p.min,
1344            format!("pool `min:` ({min_v}) must not exceed `max:` ({max_v})"),
1345        ));
1346    }
1347    if usize::from(max_v) > k {
1348        return Err(syn::Error::new_spanned(
1349            &p.max,
1350            format!("pool `max:` ({max_v}) exceeds the declared member count ({k})"),
1351        ));
1352    }
1353
1354    // Pool `resources:` (all `shared`, enforced at parse) additionally require
1355    // `task:` — same rule as nodes: the generated shell is what receives the
1356    // values as arguments (a hand-written `spawn:` task fn manages its own).
1357    if !p.resources.is_empty() && matches!(p.source, TaskSource::Spawn(_)) {
1358        return Err(syn::Error::new_spanned(
1359            &p.resources[0].ident,
1360            "pool `resources:` requires `task:` — the shared values are handed \
1361             to the generated shell as arguments; a `spawn:` task fn manages \
1362             its own arguments",
1363        ));
1364    }
1365
1366    // Resolve the member task: `spawn:` uses the given expr directly; `task:`
1367    // first stamps ONE generated shell sized `pool_size = K` (all members share a
1368    // single concrete future type) and targets that. Shared resources become
1369    // by-value shell parameters, exactly like a node's.
1370    let (shell_def, member_expr) = match &p.source {
1371        TaskSource::Spawn(e) => (quote!(), e.clone()),
1372        TaskSource::Shell(worker) => emit_shell(ident, cfg, worker, k, &p.resources, cr)?,
1373    };
1374    // Build member `I`'s spawn call from the member task, injecting `&POOL[I]`
1375    // as the first argument, then the shared resource copies (see
1376    // `inject_call_with`).
1377    let res_args: Vec<TokenStream2> = p
1378        .resources
1379        .iter()
1380        .enumerate()
1381        .map(|(i, r)| {
1382            let ecfg = &r.cfg;
1383            let var = format_ident!("__r{}", i);
1384            quote!(#(#ecfg)* #var)
1385        })
1386        .collect();
1387    let mut lead: Vec<TokenStream2> = vec![quote!(&#ident[I])];
1388    lead.extend(res_args);
1389    let call = inject_call_with(&member_expr, &lead)?;
1390    // Per-member spawn fn: a generated `spawn_<pool>::<I>` wrapper. Same optional
1391    // trace capture as a node's closure, against member `I`'s slot. With
1392    // `executor: NAME` the wrapper ignores the supervisor's `Spawner` and spawns
1393    // through the named SpawnerSlot; each member node carries `.with_executor(&EX)`,
1394    // so the supervisor awaits the slot (bounded) before invoking the wrapper and
1395    // the wrapper's `get()` is already filled (`SpawnError::Busy` guards a never-
1396    // filled slot; member futures must be `Send`). A whole worker pool can thus live
1397    // on another executor — e.g. the second core — while this core scales it.
1398    let (param, prelude, sp_tokens) = match &p.executor {
1399        None => (quote!(s), quote!(), quote!(s)),
1400        Some(ex) => (
1401            quote!(_s),
1402            quote! {
1403                let __sp = #ex
1404                    .get()
1405                    .ok_or(::embassy_executor::SpawnError::Busy)?;
1406            },
1407            quote!(__sp),
1408        ),
1409    };
1410    // Shared-resource prelude: copy each fan-out handle out non-destructively
1411    // (the slot stays filled for the next member/consumer); an unprovided slot
1412    // fail-closes the member's spawn with `SpawnError::Busy`. After the
1413    // executor-slot guard, same ordering rationale as a node's glue.
1414    let get_prelude: Vec<TokenStream2> = p
1415        .resources
1416        .iter()
1417        .enumerate()
1418        .map(|(i, r)| {
1419            let ecfg = &r.cfg;
1420            let res = &r.ident;
1421            let var = format_ident!("__r{}", i);
1422            quote! {
1423                #(#ecfg)*
1424                let #var = #res
1425                    .get()
1426                    .ok_or(::embassy_executor::SpawnError::Busy)?;
1427            }
1428        })
1429        .collect();
1430    let pool_spawn_stmts = spawn_stmts(&call, &quote!(&#ident[I]), &sp_tokens);
1431    let wrapper = format_ident!("spawn_{}", lname);
1432    let mut defs: Vec<TokenStream2> = Vec::new();
1433    defs.push(shell_def);
1434    defs.push(quote! {
1435        #(#cfg)*
1436        fn #wrapper<const I: usize>(
1437            #param: ::embassy_executor::Spawner,
1438        ) -> ::core::result::Result<(), ::embassy_executor::SpawnError> {
1439            #prelude
1440            #(#get_prelude)*
1441            #pool_spawn_stmts
1442            ::core::result::Result::Ok(())
1443        }
1444    });
1445    let member_spawn: Vec<TokenStream2> = (0..k).map(|j| quote!(#wrapper::<#j>)).collect();
1446
1447    // `executor: NAME` on the pool routes every member through that SpawnerSlot; the
1448    // supervisor awaits it before spawning each member (see `TaskNode::with_executor`).
1449    let member_with_exec = match &p.executor {
1450        Some(ex) => quote!( .with_executor(&#ex) ),
1451        None => quote!(),
1452    };
1453    // Shared-resource gates: ONE array for the whole pool (every member awaits
1454    // the same fan-out slots), wired into each member via `.with_resources`.
1455    // The shared slot statics themselves are emitted once per graph in `expand`.
1456    let gates_ident = format_ident!("__SV_GATES_{}", ident);
1457    let member_with_res = if p.resources.is_empty() {
1458        quote!()
1459    } else {
1460        quote!( .with_resources(&#gates_ident) )
1461    };
1462    let gates_def = if p.resources.is_empty() {
1463        quote!()
1464    } else {
1465        let (gates_len, gate_refs) = gate_tokens(&p.resources);
1466        quote! {
1467            #(#cfg)*
1468            static #gates_ident: [&'static dyn #cr::ResourceGate; #gates_len] =
1469                [#(#gate_refs),*];
1470        }
1471    };
1472    defs.push(gates_def);
1473    // `slot_timeout: N` — every member's pre-spawn slot/gate wait bound.
1474    let member_with_timeout = match &p.slot_timeout {
1475        Some(ms) => quote!( .with_slot_timeout(#cr::_export::Duration::from_millis(#ms)) ),
1476        None => quote!(),
1477    };
1478    let members = p
1479        .modes
1480        .iter()
1481        .zip(&member_spawn)
1482        .enumerate()
1483        .map(|(j, (mode, sp))| {
1484            let nm = format!("{lname}{j}");
1485            quote! {
1486                #cr::TaskNode::new(
1487                    #nm, #cr::Mode::#mode,
1488                    ::core::option::Option::Some((#sp) as #spawn_fn), false,
1489                ) #member_with_exec #member_with_res #member_with_timeout
1490            }
1491        });
1492    defs.push(quote! {
1493        #(#cfg)*
1494        pub static #ident: [#cr::TaskNode; #k] = [ #(#members),* ];
1495    });
1496
1497    // Structural constants, for downstream compile-time sizing (e.g. a socket
1498    // budget: `const BUDGET: usize = HTTP_MAX + 1`). Emitted because user code
1499    // can't derive them from the member array — a `const` cannot refer to a
1500    // `static` (E0013), so `HTTP.len()` is unusable in const context and the
1501    // count would otherwise have to be duplicated by hand next to the DSL.
1502    let min_const = format_ident!("{}_MIN", ident);
1503    let max_const = format_ident!("{}_MAX", ident);
1504    let members_const = format_ident!("{}_MEMBERS", ident);
1505    let (min_u, max_u) = (usize::from(min_v), usize::from(max_v));
1506    defs.push(quote! {
1507        #(#cfg)*
1508        #[doc = concat!("Pool `", stringify!(#ident), "`'s `min:` floor (validated at expansion).")]
1509        pub const #min_const: usize = #min_u;
1510        #(#cfg)*
1511        #[doc = concat!("Pool `", stringify!(#ident), "`'s `max:` scaling ceiling — the most members ever running concurrently.")]
1512        pub const #max_const: usize = #max_u;
1513        #(#cfg)*
1514        #[doc = concat!("Pool `", stringify!(#ident), "`'s declared member count (the `[TaskNode; K]` array length).")]
1515        pub const #members_const: usize = #k;
1516    });
1517
1518    let member_refs = (0..k).map(|j| quote!(&#ident[#j]));
1519    let policy = &p.policy;
1520    // The `ElasticPool<P>` type argument: honor an explicit `policy: <Ty> = ..`
1521    // annotation, else derive `P` from the constructor expr (`Ty::new(..)` shape).
1522    let policy_ty = match &p.policy_ty {
1523        Some(ty) => quote!(#ty),
1524        None => {
1525            let path = policy_type(policy)?;
1526            quote!(#path)
1527        }
1528    };
1529    // Emit the *validated* u8 values (`min_v`/`max_v`), not the raw literals: a
1530    // suffixed literal like `min: 3usize` parses as u8 above but would emit a
1531    // mismatched-type rustc error into the u8 field.
1532    defs.push(quote! {
1533        #(#cfg)*
1534        pub static #pool_static: #cr::ElasticPool<#policy_ty> = #cr::ElasticPool {
1535            nodes: &[ #(#member_refs),* ],
1536            min: #min_v,
1537            max: #max_v,
1538            policy: #policy,
1539        };
1540    });
1541
1542    let pool_entry = quote!( #(#cfg)* &#pool_static );
1543
1544    let pred = cfg_predicate(cfg);
1545    let slots = (0..k)
1546        .map(|j| Slot {
1547            cfg_pred: pred.clone(),
1548            reference: quote!(&#ident[#j]),
1549            deps: p.deps.clone(),
1550        })
1551        .collect();
1552
1553    Ok((defs, pool_entry, slots))
1554}
1555
1556/// Second pass: build the node-slot entries for `GRAPH.nodes` (`Option`, cfg-gated) and
1557/// the cfg-aware dep-index entries for `GRAPH.deps`. Runs after every slot + name is
1558/// known, since a dep may forward-reference a node declared later. An unknown dep name
1559/// is a compile error.
1560fn slot_tables(
1561    slots: &[Slot],
1562    names: &HashMap<String, usize>,
1563) -> SynResult<(Vec<TokenStream2>, Vec<TokenStream2>)> {
1564    let mut all_entries: Vec<TokenStream2> = Vec::new();
1565    let mut deps_entries: Vec<TokenStream2> = Vec::new();
1566    for slot in slots {
1567        let reference = &slot.reference;
1568        all_entries.push(match &slot.cfg_pred {
1569            None => quote!(::core::option::Option::Some(#reference)),
1570            Some(pred) => quote!({
1571                #[cfg(#pred)]
1572                { ::core::option::Option::Some(#reference) }
1573                #[cfg(not(#pred))]
1574                { ::core::option::Option::None }
1575            }),
1576        });
1577
1578        let mut dep_toks: Vec<TokenStream2> = Vec::new();
1579        // Duplicate deps are a compile error: `deps: [A, A]` would emit a doubled
1580        // index, which `topo_sort_const` counts twice in the in-degree but decrements
1581        // once — misreported as a dependency cycle. Compared by *resolved* slot index
1582        // (so a repeated pool name trips it too); two cfg-gated variants of the same
1583        // dep are allowed only when their cfg predicates differ.
1584        let mut seen: Vec<(u8, String)> = Vec::new();
1585        for d in &slot.deps {
1586            let idx = match names.get(&d.ident.to_string()) {
1587                Some(&i) => i as u8,
1588                None => {
1589                    return Err(syn::Error::new_spanned(
1590                        &d.ident,
1591                        format!(
1592                            "unknown dependency `{}` — not a declared node or pool",
1593                            d.ident
1594                        ),
1595                    ));
1596                }
1597            };
1598            let cfg = &d.cfg;
1599            let cfg_key = quote!( #(#cfg)* ).to_string();
1600            if seen.iter().any(|(i, k)| *i == idx && *k == cfg_key) {
1601                return Err(syn::Error::new_spanned(
1602                    &d.ident,
1603                    format!("duplicate dependency `{}`", d.ident),
1604                ));
1605            }
1606            seen.push((idx, cfg_key));
1607            dep_toks.push(quote!( #(#cfg)* #idx ));
1608        }
1609        deps_entries.push(quote!( &[ #(#dep_toks),* ] ));
1610    }
1611    Ok((all_entries, deps_entries))
1612}
1613
1614fn expand(graph: GraphSpec) -> SynResult<TokenStream2> {
1615    let cr = quote!(::embassy_supervisor);
1616    // The node spawn fn-pointer type. Spawn exprs (closures / const-generic fns) are
1617    // cast to this so they coerce cleanly inside `Option::Some(..)`.
1618    let spawn_fn = quote!(
1619        fn(
1620            ::embassy_executor::Spawner,
1621        ) -> ::core::result::Result<(), ::embassy_executor::SpawnError>
1622    );
1623
1624    // First pass: emit the statics/glue in declaration order, assign stable slot
1625    // indices, and record each slot + its raw deps. `names` maps a dep-addressable ident
1626    // to its slot index for dep resolution — keyed on the *raw* ident (not the runtime
1627    // `name_string`). A `node` maps to its own slot; a `pool` maps to its floor member's
1628    // slot (so `deps: [POOL]` = "after the pool is up"). Individual pool members are not
1629    // separately name-addressable.
1630    let mut defs: Vec<TokenStream2> = Vec::new();
1631    let mut pool_entries: Vec<TokenStream2> = Vec::new();
1632    let mut slots: Vec<Slot> = Vec::new();
1633    let mut names: HashMap<String, usize> = HashMap::new();
1634
1635    // Iff any `resources:` entry is `local`-marked, emit the local slot TYPE once
1636    // per graph (the per-entry statics in `emit_node` reference it by name). It
1637    // mirrors `embassy_supervisor::ResourceSlot` — same provide/take/restore
1638    // protocol, same critical-section interior, same `ResourceGate` view — but
1639    // WITHOUT the `T: Send` bound, so it can carry the `!Send` driver handles
1640    // (`RefCell`-/`NoopRawMutex`-based: `embassy_net::Stack` runners,
1641    // `cyw43::Control`, …) that a single-core system hands between its own tasks.
1642    // That requires asserting `Sync` for a `!Send` payload, so like the
1643    // `trace-hooks` symbols it is emitted here, at the graph declaration site,
1644    // where the application owns the soundness contract (see the SAFETY note).
1645    let any_local = graph
1646        .items
1647        .iter()
1648        .any(|item| item_resources(item).iter().any(|r| r.local.is_some()));
1649    if any_local {
1650        let local = format_ident!("{LOCAL_SLOT_TYPE}");
1651        // `Cell<Option<T>>` spelled through absolute paths (macro output must not
1652        // rely on the caller's prelude/imports); the mutex/signal types come from
1653        // the supervisor's `_export` shim so the consumer needs no direct
1654        // `embassy-sync` dependency.
1655        let cell = quote!(::core::cell::Cell<::core::option::Option<T>>);
1656        let raw = quote!(#cr::_export::CriticalSectionRawMutex);
1657        let signal = quote!(#cr::_export::Signal<#raw, ()>);
1658        defs.push(quote! {
1659            /// One-value handoff cell for a `local`-marked `resources:` entry
1660            /// (generated by `supervisor_graph!`). Protocol and fail-closed
1661            /// semantics of `embassy_supervisor::ResourceSlot`, minus its
1662            /// `T: Send` bound — for `!Send` driver handles on a single core.
1663            ///
1664            /// Contract (see the `unsafe impl Sync` below): every `provide` /
1665            /// `take` / `restore` of a given slot must happen on the SAME core.
1666            // `dead_code`/`missing_docs` in the consumer: the type is emitted
1667            // whenever a `local` entry is *declared*, even if every declaring
1668            // node is `#[cfg]`-compiled out of this build.
1669            #[allow(dead_code)]
1670            pub struct #local<T> {
1671                slot: #cr::_export::BlockingMutex<#raw, #cell>,
1672                filled: #signal,
1673            }
1674            // SAFETY: the payload is intentionally NOT `Send` — this assertion is
1675            // exactly the single-core contract: the value only ever moves between
1676            // executors/tasks of one core (interrupt-safe via the critical-section
1677            // mutex around every access), never across cores. The macro rejects
1678            // `local` + `executor:` so a slot cannot feed a `SendSpawner`-routed
1679            // node, and a multi-core application must not `provide`/`take` a given
1680            // slot from different cores.
1681            unsafe impl<T> ::core::marker::Sync for #local<T> {}
1682            #[allow(dead_code)]
1683            impl<T> #local<T> {
1684                /// An empty slot (`const` — it lives in the generated `static`s).
1685                pub const fn new() -> Self {
1686                    Self {
1687                        slot: #cr::_export::BlockingMutex::new(
1688                            ::core::cell::Cell::new(::core::option::Option::None),
1689                        ),
1690                        filled: #cr::_export::Signal::new(),
1691                    }
1692                }
1693                /// Move the resource in and wake the supervisor's pre-spawn wait.
1694                pub fn provide(&self, value: T) {
1695                    self.slot.lock(|c| c.set(::core::option::Option::Some(value)));
1696                    self.filled.signal(());
1697                }
1698                /// Take the resource out, leaving the slot empty (spawn glue).
1699                pub fn take(&self) -> ::core::option::Option<T> {
1700                    self.slot.lock(::core::cell::Cell::take)
1701                }
1702                /// Put the resource back for the next spawn (generated shell;
1703                /// not emitted for `consume` entries).
1704                pub fn restore(&self, value: T) {
1705                    self.provide(value);
1706                }
1707            }
1708            #[allow(dead_code)]
1709            impl<T: ::core::marker::Copy> #local<T> {
1710                /// Copy the value out WITHOUT emptying the slot — the `shared`
1711                /// kind's fan-out read (any number of consumers, slot stays
1712                /// filled). `T: Copy` only.
1713                pub fn get(&self) -> ::core::option::Option<T> {
1714                    self.slot.lock(|c| {
1715                        let v = c.take();
1716                        c.set(v);
1717                        v
1718                    })
1719                }
1720            }
1721            impl<T> ::core::default::Default for #local<T> {
1722                fn default() -> Self {
1723                    Self::new()
1724                }
1725            }
1726            impl<T> #cr::ResourceGate for #local<T> {
1727                fn is_filled(&self) -> bool {
1728                    // Peek without consuming: `Cell` has no `&T` access, so
1729                    // take-and-put-back under the same critical section.
1730                    self.slot.lock(|c| {
1731                        let v = c.take();
1732                        let filled = v.is_some();
1733                        c.set(v);
1734                        filled
1735                    })
1736                }
1737                fn filled_signal(&self) -> &#signal {
1738                    &self.filled
1739                }
1740            }
1741        });
1742    }
1743
1744    // Pre-pass: collect the declared `executor NAME;` slots so a node's
1745    // `executor:` reference can be validated regardless of declaration order.
1746    let executor_names: Vec<String> = graph
1747        .items
1748        .iter()
1749        .filter_map(|i| match i {
1750            Item::Executor(x) => Some(x.ident.to_string()),
1751            _ => None,
1752        })
1753        .collect();
1754
1755    // Pre-pass: `resources:` slot names become `pub static`s at the declaration
1756    // site, so take-kind names must be unique across the whole graph — and no
1757    // resource may shadow an `executor NAME;` static. `shared` entries are the
1758    // deliberate exception: the SAME name on several items is one fan-out slot,
1759    // emitted once (below, with the union of the declaring sites' cfg
1760    // predicates so it exists whenever any consumer does) — provided every
1761    // re-declaration repeats the kinds + type verbatim. Caught here with
1762    // targeted messages instead of rustc's downstream duplicate-static E0428.
1763    struct SharedPlan<'a> {
1764        /// First declaration — supplies the emitted static's ident (span), type,
1765        /// and `local` flag.
1766        decl: &'a ResourceDecl,
1767        /// Kinds+type token string every re-declaration must match.
1768        sig: String,
1769        /// One entry per declaring site: `None` = unconditional (the slot is
1770        /// then unconditional too), `Some(pred)` = that site's combined
1771        /// item-level + entry-level cfg predicate.
1772        preds: Vec<Option<TokenStream2>>,
1773        /// Declaring node/pool names, for the generated doc comment.
1774        owners: Vec<String>,
1775    }
1776    let mut shared_plans: Vec<(String, SharedPlan)> = Vec::new();
1777    {
1778        let mut taken: HashSet<String> = HashSet::new();
1779        for item in &graph.items {
1780            let Some((owner, item_cfg)) = item_ident_cfg(item) else {
1781                continue;
1782            };
1783            let item_pred = cfg_predicate(item_cfg);
1784            for r in item_resources(item) {
1785                let key = r.ident.to_string();
1786                if executor_names.contains(&key) {
1787                    return Err(syn::Error::new_spanned(
1788                        &r.ident,
1789                        format!(
1790                            "resource name `{}` shadows an `executor {};` slot — \
1791                             both are statics at the declaration site",
1792                            r.ident, r.ident
1793                        ),
1794                    ));
1795                }
1796                // A site's presence predicate: the item's cfg AND the entry's.
1797                let pred = match (item_pred.clone(), cfg_predicate(&r.cfg)) {
1798                    (None, None) => None,
1799                    (Some(p), None) | (None, Some(p)) => Some(p),
1800                    (Some(a), Some(b)) => Some(quote!(all(#a, #b))),
1801                };
1802                if r.shared.is_some() {
1803                    if taken.contains(&key) {
1804                        return Err(syn::Error::new_spanned(
1805                            &r.ident,
1806                            format!(
1807                                "`{}` is already a take-kind resource elsewhere in \
1808                                 the graph — a name is either one exclusive slot or \
1809                                 one `shared` slot, not both",
1810                                r.ident
1811                            ),
1812                        ));
1813                    }
1814                    let sig = r.shared_signature();
1815                    match shared_plans.iter_mut().find(|(k, _)| *k == key) {
1816                        Some((_, plan)) => {
1817                            if plan.sig != sig {
1818                                return Err(syn::Error::new_spanned(
1819                                    &r.ident,
1820                                    format!(
1821                                        "shared resource `{}` re-declared with a \
1822                                         different shape: `{}` here vs `{}` on \
1823                                         `{}` — every declaration of a shared slot \
1824                                         must repeat the same kind markers and type",
1825                                        r.ident, sig, plan.sig, plan.owners[0]
1826                                    ),
1827                                ));
1828                            }
1829                            plan.preds.push(pred);
1830                            plan.owners.push(owner.to_string());
1831                        }
1832                        None => shared_plans.push((
1833                            key,
1834                            SharedPlan {
1835                                decl: r,
1836                                sig,
1837                                preds: vec![pred],
1838                                owners: vec![owner.to_string()],
1839                            },
1840                        )),
1841                    }
1842                } else {
1843                    if !taken.insert(key.clone()) || shared_plans.iter().any(|(k, _)| *k == key) {
1844                        return Err(syn::Error::new_spanned(
1845                            &r.ident,
1846                            format!(
1847                                "duplicate resource name `{}` — resource slots are \
1848                                 statics and must be unique across the graph (only \
1849                                 `shared` entries may repeat a name)",
1850                                r.ident
1851                            ),
1852                        ));
1853                    }
1854                }
1855            }
1856        }
1857    }
1858    // Emit each shared slot once. Presence: unconditional if ANY declaring site
1859    // is, else `#[cfg(any(<site preds>))]` — the slot exists whenever at least
1860    // one consumer does.
1861    for (_, plan) in &shared_plans {
1862        let res = &plan.decl.ident;
1863        let ty = &plan.decl.ty;
1864        let slot_ty = if plan.decl.local.is_some() {
1865            let local = format_ident!("{LOCAL_SLOT_TYPE}");
1866            quote!(#local<#ty>)
1867        } else {
1868            quote!(#cr::ResourceSlot<#ty>)
1869        };
1870        let cfg_attr = if plan.preds.iter().any(|p| p.is_none()) {
1871            quote!()
1872        } else {
1873            let preds = plan.preds.iter().flatten();
1874            quote!(#[cfg(any(#(#preds),*))])
1875        };
1876        let doc = format!(
1877            "Shared (fan-out) resource slot declared by `{}` (generated by \
1878             `supervisor_graph!`). `provide()` the `Copy` handle before \
1879             `Supervisor::start`; every consumer's glue copies it out with \
1880             `get()`, so the slot STAYS FILLED — re-`provide()` only to replace \
1881             the handle (e.g. after rebuilding the underlying driver).",
1882            plan.owners.join("`, `"),
1883        );
1884        defs.push(quote! {
1885            #cfg_attr
1886            #[doc = #doc]
1887            pub static #res: #slot_ty = <#slot_ty>::new();
1888        });
1889    }
1890
1891    for item in &graph.items {
1892        match item {
1893            Item::Node(n) => {
1894                if let Some(ex) = &n.executor
1895                    && !executor_names.contains(&ex.to_string())
1896                {
1897                    return Err(syn::Error::new_spanned(
1898                        ex,
1899                        format!(
1900                            "unknown executor `{ex}`; declare it in the graph with \
1901                             `executor {ex};` (declared: [{}])",
1902                            executor_names.join(", ")
1903                        ),
1904                    ));
1905                }
1906                // The index is the slot's position, taken *before* the push.
1907                // A redeclared name is a hard error here (not just the downstream
1908                // `duplicate definition of static`): deps resolve through this map,
1909                // so a silent overwrite would silently rewire earlier `deps:` edges.
1910                if names.insert(n.ident.to_string(), slots.len()).is_some() {
1911                    return Err(syn::Error::new_spanned(
1912                        &n.ident,
1913                        format!("duplicate node/pool name `{}`", n.ident),
1914                    ));
1915                }
1916                let (def, slot) = emit_node(n, &cr, &spawn_fn)?;
1917                defs.push(def);
1918                slots.push(slot);
1919            }
1920            Item::Executor(x) => {
1921                let (cfg, ident) = (&x.cfg, &x.ident);
1922                // A runtime-filled SendSpawner slot: the app registers the
1923                // executor's spawner before `Supervisor::start`; nodes declared
1924                // `executor: NAME` spawn through it. Occupies no graph slot.
1925                defs.push(quote! {
1926                    #(#cfg)*
1927                    /// Spawner slot for the graph's `executor:`-annotated nodes
1928                    /// (generated by `supervisor_graph!`). Fill with
1929                    /// `SpawnerSlot::set` before `Supervisor::start`.
1930                    pub static #ident: #cr::SpawnerSlot = #cr::SpawnerSlot::new();
1931                });
1932            }
1933            Item::Pool(p) => {
1934                // Pools are only meaningful with the supervisor's `pool` feature (which
1935                // forwards to this crate). Without it, `Graph` has no `pools` field and
1936                // `ElasticPool` doesn't exist — so refuse a `pool` with a clear message
1937                // rather than emitting dangling references.
1938                if cfg!(feature = "pool") {
1939                    if let Some(ex) = &p.executor
1940                        && !executor_names.contains(&ex.to_string())
1941                    {
1942                        return Err(syn::Error::new_spanned(
1943                            ex,
1944                            format!(
1945                                "unknown executor `{ex}`; declare it in the graph with \
1946                                 `executor {ex};` (declared: [{}])",
1947                                executor_names.join(", ")
1948                            ),
1949                        ));
1950                    }
1951                    let (pool_defs, pool_entry, pool_slots) = emit_pool(p, &cr, &spawn_fn)?;
1952                    // A dep on the pool NAME resolves to the pool's floor member (member 0
1953                    // — the `min`-kept, always-started member): `deps: [POOL]` means "after
1954                    // the pool is up". `slots.len()` here is that member's slot index, taken
1955                    // *before* the extend below (pool_slots[0] lands at exactly this index).
1956                    // A redeclared name errors, same as the node arm.
1957                    if names.insert(p.ident.to_string(), slots.len()).is_some() {
1958                        return Err(syn::Error::new_spanned(
1959                            &p.ident,
1960                            format!("duplicate node/pool name `{}`", p.ident),
1961                        ));
1962                    }
1963                    defs.extend(pool_defs);
1964                    pool_entries.push(pool_entry);
1965                    slots.extend(pool_slots);
1966                } else {
1967                    return Err(syn::Error::new_spanned(
1968                        &p.ident,
1969                        "a `pool` requires enabling embassy-supervisor's `pool` feature",
1970                    ));
1971                }
1972            }
1973        }
1974    }
1975
1976    let m = slots.len();
1977    // Every graph index (dep entries, `topo_sort_const`'s queue/order) is a `u8`, so
1978    // more than 256 slots would silently truncate (`i as u8`) and corrupt the order.
1979    // 256 slots means max index 255 and max per-node dep count 255 — both fit exactly.
1980    if m > 256 {
1981        return Err(syn::Error::new(
1982            proc_macro2::Span::call_site(),
1983            format!(
1984                "supervisor_graph!: {m} node slots declared, but at most 256 are supported \
1985                 (including pool members) — graph indices are `u8`"
1986            ),
1987        ));
1988    }
1989    let (all_entries, deps_entries) = slot_tables(&slots, &names)?;
1990
1991    // `Graph.pools` is `#[cfg(feature = "pool")]`; emit that field iff this macro was
1992    // built with pool support (forwarded from the supervisor's `pool` feature).
1993    let pools_field = if cfg!(feature = "pool") {
1994        quote!( pools: &[ #(#pool_entries),* ], )
1995    } else {
1996        quote!()
1997    };
1998
1999    // embassy-executor's trace hooks (declared `unsafe extern "Rust"` in the
2000    // executor), defined once here at the graph declaration site — the supervisor
2001    // crate is `forbid(unsafe_code)` and cannot carry `#[unsafe(no_mangle)]` items.
2002    // They forward to the supervisor's `trace` recorders. `task_new` and
2003    // `task_ready_begin` carry nothing the recorders need (the id→node mapping
2004    // comes from the spawn glue above), so they are no-ops. Exactly one definition
2005    // of each may exist per binary: enable `trace-hooks` OR write your own set.
2006    // Requires an edition-2024 consumer (`#[unsafe(no_mangle)]` syntax).
2007    let trace_hooks = if cfg!(feature = "trace-hooks") {
2008        quote! {
2009            #[unsafe(no_mangle)]
2010            fn _embassy_trace_poll_start(executor_id: u32) {
2011                #cr::trace::on_poll_start(executor_id);
2012            }
2013            #[unsafe(no_mangle)]
2014            fn _embassy_trace_task_new(_executor_id: u32, _task_id: u32) {}
2015            #[unsafe(no_mangle)]
2016            fn _embassy_trace_task_end(executor_id: u32, task_id: u32) {
2017                #cr::trace::on_task_end(executor_id, task_id);
2018            }
2019            #[unsafe(no_mangle)]
2020            fn _embassy_trace_task_exec_begin(executor_id: u32, task_id: u32) {
2021                #cr::trace::on_task_exec_begin(executor_id, task_id);
2022            }
2023            #[unsafe(no_mangle)]
2024            fn _embassy_trace_task_exec_end(executor_id: u32, task_id: u32) {
2025                #cr::trace::on_task_exec_end(executor_id, task_id);
2026            }
2027            #[unsafe(no_mangle)]
2028            fn _embassy_trace_task_ready_begin(_executor_id: u32, _task_id: u32) {}
2029            #[unsafe(no_mangle)]
2030            fn _embassy_trace_executor_idle(executor_id: u32) {
2031                #cr::trace::on_executor_idle(executor_id);
2032            }
2033        }
2034    } else {
2035        quote!()
2036    };
2037
2038    Ok(quote! {
2039        #(#defs)*
2040
2041        // Private backing tables — the application uses `GRAPH`. The topological order
2042        // and pools are inlined into the `GRAPH` literal below; the node count is
2043        // `GRAPH.nodes.len()`.
2044        static NODES: [::core::option::Option<&'static #cr::TaskNode>; #m] = [ #(#all_entries),* ];
2045        const DEPS: [&'static [u8]; #m] = [ #(#deps_entries),* ];
2046
2047        /// The compile-time task graph — node slots, dependency table, topological order,
2048        /// and (with the `pool` feature) the elastic pools. Pass to `Supervisor::new`.
2049        pub static GRAPH: #cr::Graph<#m> = #cr::Graph {
2050            nodes: &NODES,
2051            deps: &DEPS,
2052            order: #cr::topo_sort_const(&DEPS),
2053            #pools_field
2054        };
2055
2056        #trace_hooks
2057    })
2058}
2059
2060/// Declare a supervised task graph; see the crate docs for the surface syntax.
2061#[proc_macro]
2062pub fn supervisor_graph(input: TokenStream) -> TokenStream {
2063    let graph = syn::parse_macro_input!(input as GraphSpec);
2064    expand(graph)
2065        .unwrap_or_else(syn::Error::into_compile_error)
2066        .into()
2067}