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][, cancel][, 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][, cancel];
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//! `cancel` (a bare flag on `task:` items) makes the shell own the shutdown
56//! race: the worker is driven under `TaskNode::run_cancellable` and does NOT
57//! receive the node (resources become the first arguments), so a plain
58//! supervisor-unaware `async fn` — even a diverging one — binds directly. On
59//! stop/teardown its future is dropped in place and the shell still runs its
60//! full tail (state drop, resource restores, exit record). With `exit:` the
61//! value is provided only on a real completion — an aborted worker leaves the
62//! exit slot empty. Rejected on `spawn:` (that fn owns its body) and on
63//! `Mode::Pause` (a Pause worker must survive the stop and park on
64//! `wait_resume()`; `cancel` would record an exit nothing resumes). On a `pool`
65//! it is the trailing flag (after `max:`/`slot_timeout:`) and applies to the one
66//! shared shell, i.e. to every member: a shrink drops that member's future in
67//! place and its per-member resources are restored to its own slot index.
68//!
69//! **Prefer `task:`** — no attribute boilerplate, generic workers, auto-sized pool
70//! shells, and it is the only form supporting `resources:`; the shell inlines into
71//! the same poll and its `TaskPool` replaces the one the attribute would emit.
72//! `spawn:` remains for: a fn that already carries `#[embassy_executor::task]` and
73//! can't be de-attributed (another crate); a task also spawned outside the graph
74//! (sharing its one `TaskPool` instead of duplicating it as a shell); the verbatim
75//! closure form (custom spawn-time logic); and args that must be evaluated at
76//! spawn time on the supervisor's executor rather than at the shell's first poll.
77//! Worked examples: README "`spawn:` vs `task:` — which to use".
78//!
79//! Two `task:` footguns, spelled out because nothing warns about either:
80//!
81//! * a partial-call **extra that can be missing at first poll is a task-side
82//!   panic**, not a failed spawn — extras are for infallible accessors. A value
83//!   that might not exist yet belongs in `resources:` (a `shared` entry for a
84//!   fan-out handle), where the pre-spawn gate turns "missing" into a clean
85//!   `SpawnError::Busy`;
86//! * a **verbatim-closure `spawn:` node is invisible to the trace/name glue** —
87//!   the closure owns the `SpawnToken`, so `adopt`/`stamp_name` is YOUR job
88//!   inside it, and a stable proc-macro cannot emit a warning when you forget.
89//!
90//! `resources: [RES: [local] [shared|consume] Type, ..]` (requires `task:`)
91//! threads **owned resources from `main`** into the worker instead of re-acquiring
92//! them inside the task (`Peripherals::steal()`). Each entry emits a
93//! `pub static RES` slot at the declaration site; `main` moves the
94//! resource in with `RES.provide(..)` (consuming the `Peripherals` field — the
95//! compile-time exclusive-ownership guarantee), the generated glue `take()`s it just
96//! before the spawn (an unprovided slot fails `Supervisor::start` with
97//! `SpawnError::Busy` after a bounded wait — fail-closed, not a task-side panic),
98//! and the shell passes the worker `&mut Type` (after the node arg, in declared
99//! order, before any partial-call extras) and `restore()`s the value after the
100//! worker returns, so a Terminate respawn re-takes the *same instance*. Take-kind
101//! slot names are statics: unique across the graph. Entries may carry per-entry
102//! `#[cfg(...)]` (the slot, gate, glue, shell param, and worker-call argument all
103//! follow it — gate the worker fn's matching parameter with the same `#[cfg]`).
104//!
105//! Per-entry kind markers refine that default (order-free; `local` composes with
106//! either of the mutually-exclusive `consume`/`shared`):
107//!
108//! * `consume` — the worker receives the value **by value** and no restore is
109//!   emitted: the slot stays empty after the task exits, so the worker may *drop*
110//!   the resource at teardown (a driver whose `Drop` releases pins/DMA) and a
111//!   respawn fail-closes (`SpawnError::Busy`) until the application `provide()`s
112//!   a fresh value — the pattern for resources rebuilt each run (e.g. radio
113//!   driver objects that go stale across a power cycle).
114//! * `shared` — a fan-out slot for a `Copy` handle (an `embassy_net::Stack`, a
115//!   `&'static` shared-bus ref): the glue copies the value out non-destructively
116//!   (`get()` — `T: Copy` enforced by its bound), the worker receives it by
117//!   value, no restore, and the slot STAYS FILLED — so any number of nodes
118//!   (and whole `task:` pools, where it stays ONE pool-wide slot — take kinds
119//!   become per-member arrays there instead) may declare the SAME slot name.
120//!   The static is emitted once, with the union of the declaring sites' cfg
121//!   predicates; every re-declaration must repeat the kinds + type verbatim.
122//! * `local` — **requires the non-default `local-resources` feature** (of the
123//!   supervisor crate, forwarded here): the slot is the graph-site
124//!   `__SvLocalResourceSlot` type instead of `ResourceSlot` — same protocol, no
125//!   `T: Send` bound, for `!Send` driver handles
126//!   (`RefCell`-/`NoopRawMutex`-based). Feature-gated because it is the one
127//!   graph form that emits `unsafe` code into the CONSUMER'S crate: an
128//!   `unsafe impl Sync` whose soundness is the **single-core contract** (all
129//!   `provide`/`take`/`restore` of the slot on one core). It cannot combine
130//!   with `executor:` (a `SendSpawner`-routed node needs a `Send` future —
131//!   macro error), and a consumer crate forbidding `unsafe_code` cannot use
132//!   `local` (the assertion lands in *its* code, like the `trace-hooks`
133//!   symbols).
134//!
135//! `slot_timeout: MS` (node and pool; milliseconds ≥ 1) overrides the node's
136//! pre-spawn wait bound for its `executor:` slot and `resources:` gates (default
137//! 100 ms — sized for "provided before `start()`"). Raise it for consumers of a
138//! **provider node** — a first-in-topo node whose worker *builds* the resources
139//! at runtime and `provide()`s them (the graph-native `hw_init`): size the
140//! timeout to the provider's async build time and the gate wait becomes a
141//! rendezvous instead of a `Busy`. See the README's provider-node recipe.
142//!
143//! A graph holds at most **256 node slots** (including pool members): all graph
144//! indices are `u8`, and the macro rejects a larger declaration at expansion.
145//!
146//! Nodes, pools, and individual deps may carry `#[cfg(...)]` attributes. A
147//! proc-macro can't evaluate `cfg`, so the node array is a fixed-length
148//! `[Option<&TaskNode>; M]` over all declared slots (each entry `Some`/`None` via a
149//! cfg-expression), the dep table is cfg-aware per-dep, and the order runs through
150//! `topo_sort_const` at const-eval (after cfg). Absent nodes are skipped at runtime.
151//!
152//! Generated items (at the call site): one `pub static` per `node`, a `[TaskNode; K]`
153//! array + `spawn_<pool>` glue fn + `<POOL>_POOL` `ElasticPool` + the structural
154//! `pub const`s `<POOL>_MIN` / `<POOL>_MAX` / `<POOL>_MEMBERS` (usize; for
155//! const-context sizing downstream — a `const` cannot read them off the member
156//! `static`) per `pool`, one slot `pub static` per `resources:` entry (shared
157//! entries: one per unique name; plus, iff any entry is `local`, the
158//! `__SvLocalResourceSlot` type), plus a
159//! single `pub static GRAPH: Graph<M>` bundling the node slots, the dependency table,
160//! the topological order, and (with the `pool` feature) the pools — pass `&GRAPH` to
161//! `Supervisor::new`. The backing tables are private; read them through `GRAPH.nodes`
162//! / `GRAPH.deps` / `GRAPH.order` / `GRAPH.pools` (node count is `GRAPH.nodes.len()`).
163//!
164//! With the supervisor's `trace` feature (forwarded here) the generated spawn glue
165//! also captures each `SpawnToken`'s task id into its node (`set_task_id`); with
166//! `metadata-names` it stamps the node name into the task Metadata. These are
167//! independent: `metadata-names` without `trace` emits a name-only spawn path
168//! (`stamp_name`, no id capture, no `_embassy_trace_*` dependency), so node names
169//! reach external tooling (rtos-trace/SystemView) without the trace recorders.
170//! With `trace-hooks` the macro additionally defines the seven `_embassy_trace_*`
171//! hook symbols at the declaration site (the supervisor crate is
172//! `forbid(unsafe_code)` and cannot), forwarding to the supervisor's `trace`
173//! recorders — requires an edition-2024 consumer, and exactly one graph declaration
174//! (or hook set) per binary.
175//!
176//! Types are referenced absolutely (`::embassy_supervisor::…`), so the consuming
177//! crate must depend on `embassy-supervisor` under its real name (not aliased).
178
179use proc_macro::TokenStream;
180use proc_macro2::TokenStream as TokenStream2;
181use quote::{format_ident, quote};
182use std::collections::{HashMap, HashSet};
183use syn::parse::{Parse, ParseStream};
184use syn::punctuated::Punctuated;
185use syn::spanned::Spanned;
186use syn::{
187    Attribute, Expr, Ident, LitInt, Meta, Path, Result as SynResult, Token, Type, bracketed,
188};
189
190mod kw {
191    syn::custom_keyword!(node);
192    syn::custom_keyword!(pool);
193    syn::custom_keyword!(deps);
194    syn::custom_keyword!(spawn);
195    syn::custom_keyword!(task);
196    syn::custom_keyword!(pool_size);
197    syn::custom_keyword!(policy);
198    syn::custom_keyword!(min);
199    syn::custom_keyword!(max);
200    syn::custom_keyword!(disabled);
201    syn::custom_keyword!(executor);
202    syn::custom_keyword!(resources);
203    syn::custom_keyword!(slot_timeout);
204    syn::custom_keyword!(exit);
205    syn::custom_keyword!(name);
206    syn::custom_keyword!(state);
207    syn::custom_keyword!(cancel);
208    syn::custom_keyword!(fragment);
209    syn::custom_keyword!(endfragment);
210}
211
212/// The graph-site slot type emitted (once per graph, iff any `resources:` entry
213/// is `local`-marked) for `!Send` resources. A single shared name: `emit_node`
214/// types the slot statics with it and `expand` emits its definition. Like the
215/// fixed `GRAPH` static, at most one `supervisor_graph!` per module.
216const LOCAL_SLOT_TYPE: &str = "__SvLocalResourceSlot";
217
218/// Per-graph idents for the generated helper items. UNNAMED graphs keep the
219/// historical fixed names; a `name: X;` graph suffixes them so several graphs
220/// coexist, even in one module.
221struct HelperIdents {
222    /// The graph-site `!Send` slot type (`local` kind).
223    local_slot: Ident,
224    /// The fallible-boxing fn (`state:` clauses).
225    try_box: Ident,
226    /// The `extern crate alloc as …` alias the try-box helper and shells use.
227    alloc_alias: Ident,
228}
229
230impl HelperIdents {
231    fn new(graph_name: Option<&Ident>) -> Self {
232        match graph_name {
233            None => Self {
234                local_slot: format_ident!("{LOCAL_SLOT_TYPE}"),
235                try_box: format_ident!("__sv_try_box"),
236                alloc_alias: format_ident!("__sv_alloc"),
237            },
238            Some(n) => {
239                let lower = n.to_string().to_lowercase();
240                Self {
241                    local_slot: format_ident!("{LOCAL_SLOT_TYPE}{}", n),
242                    try_box: format_ident!("__sv_try_box_{lower}"),
243                    alloc_alias: format_ident!("__sv_alloc_{lower}"),
244                }
245            }
246        }
247    }
248}
249
250/// A dependency reference: a node ident, optionally `#[cfg(...)]`-gated.
251#[derive(Clone)]
252struct Dep {
253    cfg: Vec<Attribute>,
254    ident: Ident,
255    /// `deps: [NET ready]` — bring-up additionally awaits the dep's
256    /// task-asserted readiness (`set_ready`), bounded by the dependent's
257    /// `slot_timeout`. The `Ident` is kept for its span (feature errors).
258    ready: Option<Ident>,
259}
260
261/// `deps: [a, #[cfg(feature = "x")] b, c ready, …]`
262fn parse_dep_list(input: ParseStream) -> SynResult<Vec<Dep>> {
263    let content;
264    bracketed!(content in input);
265    let mut deps = Vec::new();
266    while !content.is_empty() {
267        let cfg = content.call(Attribute::parse_outer)?;
268        let ident: Ident = content.parse()?;
269        // Optional contextual `ready` marker: a dep entry is otherwise a lone
270        // ident, so a following ident can only be the marker.
271        let ready = if content.peek(Ident) {
272            let marker: Ident = content.parse()?;
273            if marker != "ready" {
274                return Err(syn::Error::new_spanned(
275                    &marker,
276                    format!("expected `,`, `]`, or the `ready` marker, found `{marker}`"),
277                ));
278            }
279            if !cfg!(feature = "readiness") {
280                return Err(syn::Error::new_spanned(
281                    &marker,
282                    "the `ready` dep marker requires the `readiness` feature \
283                     (embassy-supervisor feature `readiness`) — bring-up then \
284                     awaits the dep's set_ready() before spawning this node",
285                ));
286            }
287            Some(marker)
288        } else {
289            None
290        };
291        deps.push(Dep { cfg, ident, ready });
292        if content.peek(Token![,]) {
293            content.parse::<Token![,]>()?;
294        }
295    }
296    Ok(deps)
297}
298
299/// `[Terminate, OnDemand, …]` — the bracketed mode list.
300fn parse_mode_list(input: ParseStream) -> SynResult<Vec<Ident>> {
301    let content;
302    bracketed!(content in input);
303    let punct = Punctuated::<Ident, Token![,]>::parse_terminated(&content)?;
304    Ok(punct.into_iter().collect())
305}
306
307/// How a node/pool member gets its task: `spawn:` names a hand-written
308/// `#[embassy_executor::task]` fn (path / partial call / verbatim closure), while
309/// `task:` names a **plain async fn** — possibly generic — for which the macro
310/// emits a concrete `#[embassy_executor::task]` shell (embassy forbids generic
311/// tasks: one static `TaskPool` per concrete future type, so per-type shells are
312/// the only way — the macro stamps them so the user doesn't).
313enum TaskSource {
314    /// `spawn: <expr>` — the expr *is* (or produces) the task fn.
315    Spawn(Expr),
316    /// `task: <path | partial call>` — wrap in a generated shell; args are
317    /// evaluated inside the shell (at the task's first poll, on its own executor).
318    Shell(Expr),
319}
320
321/// One `[#[cfg(..)]] NAME: [local] [shared|consume] Type` entry of a
322/// `resources:` clause. The macro emits a `pub static NAME` slot at the
323/// declaration site (`ResourceSlot<Type>`, or the graph-site local slot type
324/// for `local` entries); `main` moves the resource in with `NAME.provide(..)`
325/// (consuming the `Peripherals` field — the compile-time ownership guarantee),
326/// the generated spawn glue `take()`s (or, for `shared`, copies via `get()`) it
327/// before the spawn, and the generated shell `restore()`s it after the worker
328/// returns so a respawn re-takes the same instance (unless `consume`/`shared`).
329struct ResourceDecl {
330    /// Per-entry `#[cfg(...)]` attributes: the slot static, gate entry, glue
331    /// take/get, shell param, worker-call argument, and restore all carry them,
332    /// so a feature-varying resource set works within one node (the worker fn
333    /// must gate its matching parameter with the same `#[cfg]`).
334    cfg: Vec<Attribute>,
335    ident: Ident,
336    ty: Type,
337    /// `local` marker: the slot holds a `!Send`-capable value (`Rc`-, `RefCell`-,
338    /// `NoopRawMutex`-based driver handles). Kept as the marker `Ident` for
339    /// span-attached errors (`local` composes with neither `executor:` nor a
340    /// multi-core provider — see `parse_node`).
341    local: Option<Ident>,
342    /// `consume` marker: the worker receives the value **by value** and the shell
343    /// emits no restore — the slot is left empty when the worker exits, so a
344    /// respawn gates on an explicit re-`provide()`. For resources that must be
345    /// *dropped* at teardown (a driver whose `Drop` releases pins/DMA) or that go
346    /// stale across a power cycle and must be rebuilt each run.
347    consume: Option<Ident>,
348    /// `shared` marker: a fan-out slot for a `Copy` handle. The glue copies the
349    /// value out non-destructively (`get()`), the worker receives it **by
350    /// value**, no restore — so any number of nodes (and whole pools) may
351    /// declare the SAME slot name (the static is emitted once; re-declarations
352    /// must repeat kinds + type exactly). Mutually exclusive with `consume`.
353    shared: Option<Ident>,
354}
355
356impl ResourceDecl {
357    /// The kinds+type signature every re-declaration of a `shared` slot must
358    /// repeat verbatim (compared as token strings — same-name shared slots are
359    /// ONE static, so their declared shapes must agree).
360    fn shared_signature(&self) -> String {
361        let ty = &self.ty;
362        format!(
363            "{}shared {}",
364            if self.local.is_some() { "local " } else { "" },
365            quote!(#ty)
366        )
367    }
368}
369
370/// Peek whether the next token of a `resources:` entry is a kind *marker*
371/// (`local` / `consume` / `shared`) rather than the start of the resource
372/// `Type` itself. Contextual-keyword rule (no reserved words): the ident is a
373/// marker only when something else of the entry still follows it — i.e. it is
374/// NOT a marker when followed by `::` or `<` (it starts a path/generic type
375/// like `local::Foo` or `local<T>`) or by `,` / end-of-list (it IS the whole
376/// type, a type literally named `local`). Same fork-and-peek disambiguation as
377/// the pool `policy:` type annotation.
378fn peek_kind_marker(content: ParseStream) -> Option<Ident> {
379    if !content.peek(syn::Ident) {
380        return None;
381    }
382    let fork = content.fork();
383    let ident: Ident = fork.parse().ok()?;
384    if ident != "local" && ident != "consume" && ident != "shared" {
385        return None;
386    }
387    if fork.is_empty() || fork.peek(Token![,]) || fork.peek(Token![::]) || fork.peek(Token![<]) {
388        return None;
389    }
390    Some(ident)
391}
392
393/// `resources: [LED: Output<'static>, RUNNER: local consume Runner, …]`
394fn parse_resource_list(input: ParseStream) -> SynResult<Vec<ResourceDecl>> {
395    let content;
396    bracketed!(content in input);
397    let mut resources = Vec::new();
398    while !content.is_empty() {
399        let cfg = content.call(Attribute::parse_outer)?;
400        let ident: Ident = content.parse()?;
401        content.parse::<Token![:]>()?;
402        // Kind markers between the colon and the type, order-free: `local`
403        // plus at most one of `consume` / `shared`. A repeated marker is a
404        // declaration bug; `consume` (exclusive take, slot empty after exit)
405        // and `shared` (non-destructive fan-out copy) contradict each other.
406        let mut local: Option<Ident> = None;
407        let mut consume: Option<Ident> = None;
408        let mut shared: Option<Ident> = None;
409        while let Some(marker) = peek_kind_marker(&content) {
410            content.parse::<Ident>()?; // commit the peeked marker
411            // `local` is the one kind whose slot type carries an `unsafe impl
412            // Sync` — injecting unsafe code is an explicit opt-in, so the
413            // marker is rejected unless the (non-default) `local-resources`
414            // feature forwarded by the supervisor crate is enabled.
415            if marker == "local" && !cfg!(feature = "local-resources") {
416                return Err(syn::Error::new_spanned(
417                    &marker,
418                    "`local` resources emit an `unsafe impl Sync` — opt in by \
419                     enabling embassy-supervisor's `local-resources` feature",
420                ));
421            }
422            let slot = if marker == "local" {
423                &mut local
424            } else if marker == "consume" {
425                &mut consume
426            } else {
427                &mut shared
428            };
429            if slot.is_some() {
430                return Err(syn::Error::new_spanned(
431                    &marker,
432                    format!("duplicate `{marker}` marker"),
433                ));
434            }
435            *slot = Some(marker);
436        }
437        if let (Some(_), Some(s)) = (&consume, &shared) {
438            return Err(syn::Error::new_spanned(
439                s,
440                "`consume` and `shared` are mutually exclusive — `consume` takes \
441                 the single value out for one owner, `shared` copies it out to \
442                 any number of consumers",
443            ));
444        }
445        let ty: Type = content.parse()?;
446        resources.push(ResourceDecl {
447            cfg,
448            ident,
449            ty,
450            local,
451            consume,
452            shared,
453        });
454        if content.peek(Token![,]) {
455            content.parse::<Token![,]>()?;
456        }
457    }
458    Ok(resources)
459}
460
461struct NodeItem {
462    cfg: Vec<Attribute>,
463    ident: Ident,
464    mode: Ident,
465    deps: Vec<Dep>,
466    /// `None` = a parked node the app spawns itself (neither `spawn:` nor `task:`).
467    source: Option<TaskSource>,
468    /// `pool_size: N` on a `task:` node — sizes the generated shell's `TaskPool`
469    /// (headroom for a respawn while the previous instance is still draining).
470    pool_size: Option<LitInt>,
471    /// `resources: [NAME: Type, ..]` on a `task:` node — owned values threaded
472    /// from `main` through macro-emitted `ResourceSlot` statics into the
473    /// generated shell (which hands the worker `&mut Type` and restores the
474    /// value on exit). Empty for `spawn:`/parked nodes (enforced at parse).
475    resources: Vec<ResourceDecl>,
476    disabled: bool,
477    /// `executor: NAME` — spawn through the named [`SpawnerSlot`] (a
478    /// `SendSpawner` the app registers at runtime) instead of the supervisor's
479    /// own `Spawner`. `None` = the default executor.
480    executor: Option<Ident>,
481    /// `slot_timeout: N` (milliseconds) — overrides the node's pre-spawn
482    /// slot/gate wait bound (default 100 ms). Needed when the node's resources
483    /// are filled by a **provider node** at runtime: size it to the provider's
484    /// async build time.
485    slot_timeout: Option<LitInt>,
486    /// `exit: Type` on a `task:` node — the worker's return value is
487    /// `provide()`d into a generated `pub static <NODE>_EXIT: ResourceSlot<Type>`
488    /// just before the shell records the exit, so `<NODE>_EXIT.wait_take()`
489    /// observes the completion value (idiomatically `Result<R, Aborted>` out of
490    /// `run_cancellable` for completed-vs-cancelled). A worker that can never
491    /// return rejects the clause: the provide is dead code, and the shell denies
492    /// `unreachable_code` on it (spanned here) rather than emit a slot nothing
493    /// could ever fill.
494    exit: Option<syn::Type>,
495    /// `state: Type = init_expr` (feature `heap-state`) — per-activation heap
496    /// state: the glue fallibly boxes `init_expr` (alloc failure =
497    /// `SpawnError::Busy`, retryable), the shell lends the worker `&mut Type`,
498    /// and the Box drops on task exit — allocated fresh each activation,
499    /// reclaimed on every exit.
500    state: Option<(syn::Type, Expr)>,
501    /// `cancel` on a `task:` node — the shell drives the worker under
502    /// [`TaskNode::run_cancellable`] instead of awaiting it directly, and does
503    /// NOT pass the node to it. For the common shape the supervisor otherwise
504    /// can't take: a plain `async fn` that loops forever and knows nothing about
505    /// any supervisor. On shutdown its future is dropped in place and the shell
506    /// runs its usual restore/exit-provide/`mark_exited` tail.
507    cancel: bool,
508    /// The `supervisor_fragment!` this item was forwarded from (via the
509    /// `@fragment NAME;` marker), for error attribution across the relay.
510    fragment: Option<String>,
511}
512
513/// `executor NAME;` — declares a `pub static NAME: SpawnerSlot` the application
514/// fills with a `SendSpawner` before (or concurrently with) `Supervisor::start` (an
515/// InterruptExecutor tier, core1, ...). Nodes reference it with `executor: NAME`; the
516/// supervisor awaits the slot before spawning such a node (bounded by
517/// `SLOT_READY_TIMEOUT`, then `SpawnError::Busy`).
518struct ExecutorItem {
519    cfg: Vec<Attribute>,
520    ident: Ident,
521}
522
523struct PoolItem {
524    cfg: Vec<Attribute>,
525    ident: Ident,
526    modes: Vec<Ident>,
527    deps: Vec<Dep>,
528    /// The member task. Either a bare path (`http_task`) or a partial call carrying
529    /// extra args (`mcp_server_task(stack())`); the macro spawns member `j` as
530    /// `s.spawn(<fn>(&POOL[j] [, extra args])?)` — the node is always the first arg.
531    /// No closure form (members are instantiated per index), unlike a node's `spawn:`.
532    /// The `Shell` variant (`task:`) wraps a plain — possibly generic — async fn in
533    /// ONE generated `#[embassy_executor::task(pool_size = K)]` shell shared by all
534    /// members (they share one concrete future type, like a `spawn:` pool).
535    source: TaskSource,
536    /// The scaling policy value, emitted as the `policy:` field of the `ElasticPool`
537    /// static. The static is typed `ElasticPool<P>`, so the macro needs the policy
538    /// *type* `P`: when `policy_ty` is `None` it derives `P` from this expr via
539    /// `policy_type` (requires a `Type::new(..)` shape); when `policy_ty` is `Some`
540    /// the caller stated `P` explicitly and this expr can be any value of that type.
541    policy: Expr,
542    /// Optional explicit policy type from the `policy: <Type> = <expr>` form. `Some`
543    /// bypasses `policy_type` derivation, allowing a value the deriver can't handle
544    /// (a free fn, a const, a builder chain, a qualified path).
545    policy_ty: Option<Type>,
546    /// `executor: NAME` — spawn every member through the named [`SpawnerSlot`]
547    /// (e.g. a worker pool on the second core, scaled by this core's supervisor).
548    executor: Option<Ident>,
549    /// Pool `resources:` — **`shared` entries only** (enforced at parse): each
550    /// member's glue copies the same `Copy` handle out non-destructively, so
551    /// members don't contend (the reason non-shared kinds stay rejected).
552    resources: Vec<ResourceDecl>,
553    /// `slot_timeout: N` (milliseconds) — applied to every member (see the
554    /// node field of the same name).
555    slot_timeout: Option<LitInt>,
556    /// `min:`/`max:` — the scaling floor/ceiling. Any const-evaluable `usize`
557    /// expression: integer literals validate at parse time (best spans); other
558    /// exprs become the emitted `<POOL>_MIN`/`<POOL>_MAX` consts guarded by
559    /// const asserts (min <= max <= member count <= 255). The member count
560    /// itself (the mode list) stays structural — a proc macro cannot
561    /// const-evaluate, and the count drives how many nodes/shells/names are
562    /// emitted, so it cannot come from a const.
563    min: Expr,
564    max: Expr,
565    /// `state: Type = init_expr` (feature `heap-state`) — per-activation heap
566    /// state, one fresh Box per member per activation (see the node field).
567    state: Option<(syn::Type, Expr)>,
568    /// `cancel` on a `task:` pool — the one shared shell drives each member's
569    /// worker under [`TaskNode::run_cancellable`] and does not lead its arguments
570    /// with `&POOL[I]`, so a plain worker is shrunk (and torn down) by having its
571    /// future dropped in place. See the node field of the same name.
572    cancel: bool,
573    /// The `supervisor_fragment!` this item was forwarded from (via the
574    /// `@fragment NAME;` marker), for error attribution across the relay.
575    fragment: Option<String>,
576}
577
578// Both variants embed a large `syn::Expr` (and `PoolItem` a bit more), so their sizes
579// are close but unequal — enough for `large_enum_variant` to flag the gap. This AST is
580// parsed once and lives only briefly in a `Vec` during expansion, so boxing a variant
581// to shave a few bytes per element buys nothing real; suppress the lint instead of
582// paying a heap allocation.
583#[allow(clippy::large_enum_variant)]
584enum Item {
585    Node(NodeItem),
586    Pool(PoolItem),
587    Executor(ExecutorItem),
588}
589
590/// The parsed macro input: the list of `node`/`pool` declarations, in source order.
591/// Named `GraphSpec` (not `Graph`) to stay distinct from the *emitted* public type
592/// [`embassy_supervisor::Graph`] that `expand` produces as the `GRAPH` static.
593struct GraphSpec {
594    /// `name: IDENT;` as the FIRST item — the emitted graph static's ident
595    /// (default `GRAPH`). Named graphs suffix every generated helper ident so
596    /// several graphs coexist (even in one module); only the UNNAMED graph may
597    /// emit the once-per-binary `_embassy_trace_*` hook symbols.
598    name: Option<Ident>,
599    items: Vec<Item>,
600}
601
602/// An item's `resources:` entries (nodes and pools both carry them; an
603/// `executor` slot has none) — for the graph-wide pre-passes in `expand`.
604fn item_resources(item: &Item) -> &[ResourceDecl] {
605    match item {
606        Item::Node(n) => &n.resources,
607        Item::Pool(p) => &p.resources,
608        Item::Executor(_) => &[],
609    }
610}
611
612/// An item's own name + cfg attributes (for shared-slot bookkeeping/docs).
613fn item_ident_cfg(item: &Item) -> Option<(&Ident, &[Attribute])> {
614    match item {
615        Item::Node(n) => Some((&n.ident, &n.cfg)),
616        Item::Pool(p) => Some((&p.ident, &p.cfg)),
617        Item::Executor(_) => None,
618    }
619}
620
621impl Parse for GraphSpec {
622    fn parse(input: ParseStream) -> SynResult<Self> {
623        // Optional `name: IDENT;` first (same shape as a fragment's header).
624        let name = if input.peek(kw::name) && input.peek2(Token![:]) {
625            input.parse::<kw::name>()?;
626            input.parse::<Token![:]>()?;
627            let n: Ident = input.parse()?;
628            input.parse::<Token![;]>()?;
629            Some(n)
630        } else {
631            None
632        };
633        let mut items = Vec::new();
634        // Set while parsing items forwarded through a `supervisor_fragment!`
635        // relay: `@fragment NAME;` opens the span, `@endfragment;` closes it.
636        // Purely for error attribution — the items themselves are ordinary.
637        let mut current_fragment: Option<String> = None;
638        while !input.is_empty() {
639            if input.peek(Token![@]) {
640                input.parse::<Token![@]>()?;
641                if input.peek(kw::fragment) {
642                    input.parse::<kw::fragment>()?;
643                    current_fragment = Some(input.parse::<Ident>()?.to_string());
644                } else if input.peek(kw::endfragment) {
645                    input.parse::<kw::endfragment>()?;
646                    current_fragment = None;
647                } else {
648                    return Err(input.error("expected `@fragment NAME;` or `@endfragment;`"));
649                }
650                input.parse::<Token![;]>()?;
651                continue;
652            }
653            let cfg = input.call(Attribute::parse_outer)?;
654            if input.peek(kw::node) {
655                let mut n = parse_node(input, cfg)?;
656                n.fragment = current_fragment.clone();
657                items.push(Item::Node(n));
658            } else if input.peek(kw::pool) {
659                let mut p = parse_pool(input, cfg)?;
660                p.fragment = current_fragment.clone();
661                items.push(Item::Pool(p));
662            } else if input.peek(kw::executor) {
663                // `executor NAME;` — a runtime-filled SendSpawner slot; nodes
664                // carrying `executor: NAME` spawn through it (the supervisor awaits
665                // the slot before spawning them).
666                input.parse::<kw::executor>()?;
667                let ident: Ident = input.parse()?;
668                input.parse::<Token![;]>()?;
669                items.push(Item::Executor(ExecutorItem { cfg, ident }));
670            } else {
671                return Err(input.error(
672                    "expected `node`, `pool`, or `executor` (optionally `#[cfg(...)]`-prefixed)",
673                ));
674            }
675        }
676        Ok(GraphSpec { name, items })
677    }
678}
679
680// node IDENT = MODE, deps: [..] [, spawn: <expr>] [, disabled];
681fn parse_node(input: ParseStream, cfg: Vec<Attribute>) -> SynResult<NodeItem> {
682    input.parse::<kw::node>()?;
683    let ident: Ident = input.parse()?;
684    input.parse::<Token![=]>()?;
685    let mode: Ident = input.parse()?;
686    input.parse::<Token![,]>()?;
687    input.parse::<kw::deps>()?;
688    input.parse::<Token![:]>()?;
689    let deps = parse_dep_list(input)?;
690
691    let mut spawn = None;
692    let mut task: Option<(kw::task, Expr)> = None;
693    let mut pool_size = None;
694    let mut disabled = false;
695    let mut executor = None;
696    let mut resources: Option<(kw::resources, Vec<ResourceDecl>)> = None;
697    let mut slot_timeout = None;
698    let mut exit: Option<(kw::exit, syn::Type)> = None;
699    let mut state: Option<(kw::state, syn::Type, Expr)> = None;
700    let mut cancel: Option<kw::cancel> = None;
701    while input.peek(Token![,]) {
702        input.parse::<Token![,]>()?;
703        if input.peek(kw::spawn) {
704            input.parse::<kw::spawn>()?;
705            input.parse::<Token![:]>()?;
706            spawn = Some(input.parse::<Expr>()?);
707        } else if input.peek(kw::task) {
708            let k = input.parse::<kw::task>()?;
709            input.parse::<Token![:]>()?;
710            task = Some((k, input.parse::<Expr>()?));
711        } else if input.peek(kw::pool_size) {
712            input.parse::<kw::pool_size>()?;
713            input.parse::<Token![:]>()?;
714            pool_size = Some(input.parse::<LitInt>()?);
715        } else if input.peek(kw::disabled) {
716            input.parse::<kw::disabled>()?;
717            disabled = true;
718        } else if input.peek(kw::cancel) {
719            cancel = Some(input.parse::<kw::cancel>()?);
720        } else if input.peek(kw::executor) {
721            input.parse::<kw::executor>()?;
722            input.parse::<Token![:]>()?;
723            executor = Some(input.parse::<Ident>()?);
724        } else if input.peek(kw::resources) {
725            let k = input.parse::<kw::resources>()?;
726            input.parse::<Token![:]>()?;
727            resources = Some((k, parse_resource_list(input)?));
728        } else if input.peek(kw::slot_timeout) {
729            input.parse::<kw::slot_timeout>()?;
730            input.parse::<Token![:]>()?;
731            slot_timeout = Some(input.parse::<LitInt>()?);
732        } else if input.peek(kw::exit) {
733            let k = input.parse::<kw::exit>()?;
734            input.parse::<Token![:]>()?;
735            exit = Some((k, input.parse::<syn::Type>()?));
736        } else if input.peek(kw::state) {
737            let k = input.parse::<kw::state>()?;
738            input.parse::<Token![:]>()?;
739            let ty: syn::Type = input.parse()?;
740            input.parse::<Token![=]>()?;
741            let init: Expr = input.parse()?;
742            if !cfg!(feature = "heap-state") {
743                return Err(syn::Error::new_spanned(
744                    k,
745                    "`state:` requires the `heap-state` feature \
746                     (embassy-supervisor feature `heap-state`) — per-activation \
747                     boxed state, reclaimed on task exit",
748                ));
749            }
750            state = Some((k, ty, init));
751        } else {
752            return Err(input.error(
753                "expected `spawn:`, `task:`, `pool_size:`, `executor:`, `resources:`, \
754                 `slot_timeout:`, `exit:`, `state:`, `cancel`, or `disabled`",
755            ));
756        }
757    }
758    input.parse::<Token![;]>()?;
759
760    // `slot_timeout: 0` would make every gated spawn fail instantly — reject it
761    // as the declaration bug it is (`base10_parse::<u64>` also rejects suffixed
762    // or oversized literals with a span-attached error).
763    if let Some(st) = &slot_timeout {
764        if st.base10_parse::<u64>()? == 0 {
765            return Err(syn::Error::new_spanned(
766                st,
767                "`slot_timeout:` must be at least 1 (milliseconds)",
768            ));
769        }
770    }
771
772    // Exactly one of `spawn:` / `task:` may pick the node's task.
773    if let (Some(_), Some((k, _))) = (&spawn, &task) {
774        return Err(syn::Error::new_spanned(
775            k,
776            "`task:` and `spawn:` are mutually exclusive — `spawn:` names a \
777             hand-written `#[embassy_executor::task]` fn, `task:` generates one",
778        ));
779    }
780    // `pool_size:` sizes the generated shell's TaskPool; without `task:` there is
781    // no generated shell to size (a `spawn:` task fn declares its own).
782    if let (Some(ps), None) = (&pool_size, &task) {
783        return Err(syn::Error::new_spanned(
784            ps,
785            "`pool_size:` requires `task:` — a `spawn:` task fn sets its own \
786             `#[embassy_executor::task(pool_size = ...)]`",
787        ));
788    }
789    // `resources:` only makes sense with `task:`: the generated shell is what
790    // takes the values out of their slots at spawn and restores them after the
791    // worker returns. A hand-written `spawn:` fn (or a parked node) manages its
792    // own arguments.
793    if let Some((k, decls)) = &resources {
794        if task.is_none() {
795            return Err(syn::Error::new_spanned(
796                k,
797                "`resources:` requires `task:` — resources are handed to the \
798                 generated shell as owned arguments and restored by it; a \
799                 `spawn:` task fn manages its own arguments",
800            ));
801        }
802        if decls.is_empty() {
803            return Err(syn::Error::new_spanned(
804                k,
805                "`resources:` must declare at least one `NAME: Type` entry",
806            ));
807        }
808        // Duplicate names within one node would emit two statics with the same
809        // ident; catch it here with a clearer message than rustc's E0428.
810        for (i, d) in decls.iter().enumerate() {
811            if decls[..i].iter().any(|prev| prev.ident == d.ident) {
812                return Err(syn::Error::new_spanned(
813                    &d.ident,
814                    format!("duplicate resource name `{}`", d.ident),
815                ));
816            }
817        }
818    }
819    if let Some(ps) = &pool_size {
820        if ps.base10_parse::<usize>()? == 0 {
821            return Err(syn::Error::new_spanned(
822                ps,
823                "`pool_size:` must be at least 1",
824            ));
825        }
826    }
827    // `state:` lives in the generated shell (it owns the Box across the worker
828    // call and drops it on exit); a `spawn:` fn can box its own state.
829    if let Some((k, _, _)) = &state
830        && task.is_none()
831    {
832        return Err(syn::Error::new_spanned(
833            k,
834            "`state:` requires `task:` — the generated shell owns the boxed \
835             state across the worker call and drops it on exit; a `spawn:` \
836             task fn can Box its own state",
837        ));
838    }
839    // `exit:` captures the worker's return value in the generated shell — only
840    // `task:` has one. A `spawn:` fn (or a parked node) owns its body and can
841    // `provide()` into any slot itself.
842    if let Some((k, _)) = &exit {
843        if task.is_none() {
844            return Err(syn::Error::new_spanned(
845                k,
846                "`exit:` requires `task:` — the generated shell is what captures \
847                 the worker's return value; a `spawn:` task fn can provide() into \
848                 a slot itself",
849            ));
850        }
851    }
852    // `cancel` rewrites how the generated shell drives the worker; a `spawn:` fn
853    // (or a parked node) owns its own body and can call `run_cancellable` itself.
854    if let Some(k) = &cancel {
855        if task.is_none() {
856            return Err(syn::Error::new_spanned(
857                k,
858                "`cancel` requires `task:` — it wraps the generated shell's call \
859                 to the worker; a `spawn:` task fn can call \
860                 `node.run_cancellable(..)` itself",
861            ));
862        }
863        // A Pause worker is supposed to SURVIVE a stop (ack, park on
864        // `wait_resume()`, keep its resources); `cancel` drops its future and
865        // lets the shell record an exit, after which nothing resumes it. That is
866        // the opposite of the mode, so reject the pair rather than silently
867        // turning a Pause node into a one-shot.
868        if mode == "Pause" {
869            return Err(syn::Error::new_spanned(
870                k,
871                "`cancel` cannot be combined with `Mode::Pause` — a Pause worker \
872                 must survive the stop and park on `wait_resume()`, but `cancel` \
873                 drops its future and records an exit; use `Mode::Terminate` (or \
874                 `OnDemand`), or drive the pause by hand in the worker",
875            ));
876        }
877    }
878    // A `local` resource makes the shell future hold a `!Send`-capable value, and
879    // an `executor:`-routed node spawns through a `SendSpawner`, whose `spawn`
880    // requires a `Send` future. Reject here with the reason instead of letting
881    // rustc surface it as an opaque `F: Send` bound failure deep in the glue.
882    if let (Some((_, decls)), Some(ex)) = (&resources, &executor) {
883        if let Some(l) = decls.iter().find_map(|d| d.local.as_ref()) {
884            return Err(syn::Error::new_spanned(
885                l,
886                format!(
887                    "`local` resources cannot be combined with `executor: {ex}` — a \
888                     local slot exists to carry `!Send` values, and a node routed \
889                     through a `SpawnerSlot` (`SendSpawner`) must have a `Send` \
890                     future; run the node on the supervisor's own executor"
891                ),
892            ));
893        }
894    }
895    let source = match (spawn, task) {
896        (Some(e), _) => Some(TaskSource::Spawn(e)),
897        (None, Some((_, e))) => Some(TaskSource::Shell(e)),
898        (None, None) => None,
899    };
900
901    Ok(NodeItem {
902        cfg,
903        ident,
904        mode,
905        deps,
906        source,
907        pool_size,
908        disabled,
909        executor,
910        resources: resources.map(|(_, decls)| decls).unwrap_or_default(),
911        slot_timeout,
912        exit: exit.map(|(_, ty)| ty),
913        state: state.map(|(_, ty, init)| (ty, init)),
914        cancel: cancel.is_some(),
915        fragment: None,
916    })
917}
918
919// pool IDENT = [MODES], deps: [..][, executor: EXEC], spawn: <fn>, policy: EXPR, min: N, max: M;
920fn parse_pool(input: ParseStream, cfg: Vec<Attribute>) -> SynResult<PoolItem> {
921    input.parse::<kw::pool>()?;
922    let ident: Ident = input.parse()?;
923    input.parse::<Token![=]>()?;
924    let modes = parse_mode_list(input)?;
925    input.parse::<Token![,]>()?;
926    input.parse::<kw::deps>()?;
927    input.parse::<Token![:]>()?;
928    let deps = parse_dep_list(input)?;
929    input.parse::<Token![,]>()?;
930    // Optional `executor: NAME,` — run the whole pool on the named SpawnerSlot's
931    // executor (e.g. a worker pool on the second core, scaled from this one).
932    let executor = if input.peek(kw::executor) {
933        input.parse::<kw::executor>()?;
934        input.parse::<Token![:]>()?;
935        let ex: Ident = input.parse()?;
936        input.parse::<Token![,]>()?;
937        Some(ex)
938    } else {
939        None
940    };
941    // The member task: a path, or a partial call supplying extra args (the macro
942    // injects `&POOL[j]` as the first argument in either case). `spawn:` names a
943    // hand-written `#[embassy_executor::task(pool_size = K)]` fn; `task:` names a
944    // plain (possibly generic) async fn the macro wraps in ONE generated shell
945    // task sized `pool_size = K`.
946    let source = if input.peek(kw::task) {
947        input.parse::<kw::task>()?;
948        input.parse::<Token![:]>()?;
949        TaskSource::Shell(input.parse()?)
950    } else {
951        input.parse::<kw::spawn>()?;
952        input.parse::<Token![:]>()?;
953        TaskSource::Spawn(input.parse()?)
954    };
955    input.parse::<Token![,]>()?;
956    // Pool `resources:` — `shared` entries only. A take-kind slot holds ONE
957    // value and pool members all run the same worker: they would contend for
958    // that single instance and every member past the first would fail its
959    // spawn. A `shared` entry is a non-destructive fan-out copy, so members
960    // don't contend — each glue `get()`s the same `Copy` handle.
961    let resources = if input.peek(kw::resources) {
962        input.parse::<kw::resources>()?;
963        input.parse::<Token![:]>()?;
964        let decls = parse_resource_list(input)?;
965        // Take-kind entries (lend/consume) become per-member SLOT ARRAYS
966        // (`[ResourceSlot<T>; K]`, member `I` takes/restores index `I`), so
967        // members no longer contend. Only TAKE-KIND `local` stays rejected:
968        // its single-core provide/take/restore contract interacts with
969        // per-member restore in ways deferred for now. A `shared local`
970        // entry is fine — it rides the pool-wide shared-slot path (one
971        // graph-site slot, non-destructive `get()`, no restore), exactly as
972        // before per-member resources existed.
973        if let Some(bad) = decls
974            .iter()
975            .find(|d| d.local.is_some() && d.shared.is_none())
976        {
977            return Err(syn::Error::new_spanned(
978                &bad.ident,
979                "`local` is not supported on take-kind `pool` resources (the single-core \
980                 slot contract + per-member restore is deferred); a `shared local` entry \
981                 works (one pool-wide fan-out slot), or declare the take-kind `local` \
982                 resource on a node",
983            ));
984        }
985        input.parse::<Token![,]>()?;
986        decls
987    } else {
988        Vec::new()
989    };
990    // Optional `state: Type = expr,` — per-member per-activation boxed state.
991    let state = if input.peek(kw::state) {
992        let k = input.parse::<kw::state>()?;
993        input.parse::<Token![:]>()?;
994        let ty: syn::Type = input.parse()?;
995        input.parse::<Token![=]>()?;
996        let init: Expr = input.parse()?;
997        input.parse::<Token![,]>()?;
998        if !cfg!(feature = "heap-state") {
999            return Err(syn::Error::new_spanned(
1000                k,
1001                "`state:` requires the `heap-state` feature \
1002                 (embassy-supervisor feature `heap-state`) — per-activation \
1003                 boxed state, reclaimed on task exit",
1004            ));
1005        }
1006        Some((ty, init))
1007    } else {
1008        None
1009    };
1010    // `exit:` would land here positionally; reject it with the reason instead of
1011    // the generic "expected `policy`" the positional grammar produces.
1012    if input.peek(kw::exit) {
1013        let k = input.parse::<kw::exit>()?;
1014        return Err(syn::Error::new_spanned(
1015            k,
1016            "`exit:` is not supported on `pool` — the K members share one shell, \
1017             so per-member exit values need per-member storage; use per-node \
1018             `exit:` declarations, or have the worker provide() into an \
1019             app-declared slot itself",
1020        ));
1021    }
1022    input.parse::<kw::policy>()?;
1023    input.parse::<Token![:]>()?;
1024    // Optional explicit policy type: `policy: <Ty> = <expr>`. Fork to see if a `Type`
1025    // is followed by `=`; if so it's an annotation (commit on the real stream + eat the
1026    // `=`), otherwise rewind and treat the whole thing as the value expr (type derived
1027    // from it in `emit_pool`). For the common `Ty::new(..)` value the fork parses only a
1028    // partial type and then sees `(`, not `=`, so it correctly falls back to the derive
1029    // path — this keeps the bare form working unchanged.
1030    let policy_ty = {
1031        let fork = input.fork();
1032        if fork.parse::<Type>().is_ok() && fork.peek(Token![=]) {
1033            let ty: Type = input.parse()?;
1034            input.parse::<Token![=]>()?;
1035            Some(ty)
1036        } else {
1037            None
1038        }
1039    };
1040    let policy: Expr = input.parse()?;
1041    input.parse::<Token![,]>()?;
1042    input.parse::<kw::min>()?;
1043    input.parse::<Token![:]>()?;
1044    let min: Expr = input.parse()?;
1045    input.parse::<Token![,]>()?;
1046    input.parse::<kw::max>()?;
1047    input.parse::<Token![:]>()?;
1048    let max: Expr = input.parse()?;
1049    // Optional trailing `, slot_timeout: N` (milliseconds, ≥ 1) — every member's
1050    // pre-spawn slot/gate wait bound (see the node clause of the same name).
1051    let slot_timeout = if input.peek(Token![,]) && input.peek2(kw::slot_timeout) {
1052        input.parse::<Token![,]>()?;
1053        input.parse::<kw::slot_timeout>()?;
1054        input.parse::<Token![:]>()?;
1055        let st: LitInt = input.parse()?;
1056        if st.base10_parse::<u64>()? == 0 {
1057            return Err(syn::Error::new_spanned(
1058                &st,
1059                "`slot_timeout:` must be at least 1 (milliseconds)",
1060            ));
1061        }
1062        Some(st)
1063    } else {
1064        None
1065    };
1066    // Optional trailing `, cancel` — the pool's one shared shell owns the
1067    // shutdown race for every member (see the node clause of the same name).
1068    // Shrink and teardown already signal each member; `cancel` is what makes a
1069    // worker that never returns answer them.
1070    let cancel = if input.peek(Token![,]) && input.peek2(kw::cancel) {
1071        input.parse::<Token![,]>()?;
1072        Some(input.parse::<kw::cancel>()?)
1073    } else {
1074        None
1075    };
1076    input.parse::<Token![;]>()?;
1077    if let Some(k) = &cancel {
1078        // Same reason as the node: `cancel` rewrites how the GENERATED shell
1079        // drives the worker, so there must be one.
1080        if matches!(source, TaskSource::Spawn(_)) {
1081            return Err(syn::Error::new_spanned(
1082                k,
1083                "`cancel` requires `task:` — it wraps the generated shell's call to \
1084                 the member worker; a `spawn:` member fn can call \
1085                 `node.run_cancellable(..)` itself",
1086            ));
1087        }
1088        // A Pause member must survive its stop and park on `wait_resume()`;
1089        // `cancel` drops its future and records an exit instead. Reject the pair
1090        // per member rather than silently turning parked members into one-shots.
1091        if let Some(m) = modes.iter().find(|m| *m == "Pause") {
1092            return Err(syn::Error::new_spanned(
1093                m,
1094                "`cancel` cannot be combined with a `Pause` member — a Pause worker \
1095                 must survive the stop and park on `wait_resume()`, but `cancel` \
1096                 drops its future and records an exit; use `Terminate` (or \
1097                 `OnDemand`) members, or drive the pause by hand in the worker",
1098            ));
1099        }
1100    }
1101    // Same `Send` reasoning as the node-side check: a `local` (i.e. `!Send`able)
1102    // resource cannot ride members routed through a `SendSpawner`.
1103    if let Some(ex) = &executor {
1104        if let Some(l) = resources.iter().find_map(|d| d.local.as_ref()) {
1105            return Err(syn::Error::new_spanned(
1106                l,
1107                format!(
1108                    "`local` resources cannot be combined with `executor: {ex}` — a \
1109                     local slot exists to carry `!Send` values, and a pool routed \
1110                     through a `SpawnerSlot` (`SendSpawner`) must have `Send` \
1111                     futures; run the pool on the supervisor's own executor"
1112                ),
1113            ));
1114        }
1115    }
1116    Ok(PoolItem {
1117        cfg,
1118        ident,
1119        modes,
1120        deps,
1121        source,
1122        policy,
1123        policy_ty,
1124        executor,
1125        resources,
1126        slot_timeout,
1127        min,
1128        max,
1129        state,
1130        cancel: cancel.is_some(),
1131        fragment: None,
1132    })
1133}
1134
1135/// The node/pool name string: ident lowercased with `_`→`-` (`WIFI_CTRL` → "wifi-ctrl").
1136fn name_string(ident: &Ident) -> String {
1137    ident.to_string().to_lowercase().replace('_', "-")
1138}
1139
1140/// Build a task-call expression with leading arguments injected ahead of the
1141/// user-supplied extras — the node ref (`&NODE` / `&POOL[i]`) first, then the
1142/// item's threaded `resources:` values: a bare path `f` => `f(lead..)`; a
1143/// partial call `f(a, b)` => `f(lead.., a, b)`.
1144fn inject_call_with(task: &Expr, lead: &[TokenStream2]) -> SynResult<TokenStream2> {
1145    match task {
1146        Expr::Path(_) => Ok(quote!(#task(#(#lead),*))),
1147        Expr::Call(c) => {
1148            let f = &c.func;
1149            let args = c.args.iter();
1150            Ok(quote!(#f(#(#lead),* #(, #args)*)))
1151        }
1152        other => Err(syn::Error::new_spanned(
1153            other,
1154            "expected a task-fn path or a partial call like `f(extra_args)`",
1155        )),
1156    }
1157}
1158
1159/// Combine an item's `#[cfg(...)]` attributes into one predicate (`all(..)` if
1160/// several), used to gate its `GRAPH.nodes` slot to `Some`/`None`. `None` = always present.
1161fn cfg_predicate(attrs: &[Attribute]) -> Option<TokenStream2> {
1162    let preds: Vec<TokenStream2> = attrs
1163        .iter()
1164        .filter_map(|a| match &a.meta {
1165            Meta::List(ml) if ml.path.is_ident("cfg") => Some(ml.tokens.clone()),
1166            _ => None,
1167        })
1168        .collect();
1169    match preds.len() {
1170        0 => None,
1171        1 => Some(preds[0].clone()),
1172        _ => Some(quote!(all(#(#preds),*))),
1173    }
1174}
1175
1176/// Gate-array tokens for a `resources:` list: the element list (each entry
1177/// `#[cfg]`-gated — cfg on array elements is stable, same as the deps table)
1178/// and a matching length expression. A cfg'd-out element must also subtract
1179/// from the fixed array length, so with any per-entry cfg the length becomes a
1180/// sum of cfg-block 1/0 terms (the `GRAPH.nodes` Some/None trick, in const
1181/// position); without, it stays the plain count.
1182fn gate_tokens(resources: &[ResourceDecl]) -> (TokenStream2, Vec<TokenStream2>) {
1183    let gate_refs: Vec<TokenStream2> = resources
1184        .iter()
1185        .map(|r| {
1186            let cfg = &r.cfg;
1187            let res = &r.ident;
1188            quote!(#(#cfg)* &#res)
1189        })
1190        .collect();
1191    let any_cfg = resources.iter().any(|r| cfg_predicate(&r.cfg).is_some());
1192    let len = if any_cfg {
1193        let terms: Vec<TokenStream2> = resources
1194            .iter()
1195            .map(|r| match cfg_predicate(&r.cfg) {
1196                None => quote!(1usize),
1197                Some(pred) => quote!({
1198                    #[cfg(#pred)]
1199                    {
1200                        1usize
1201                    }
1202                    #[cfg(not(#pred))]
1203                    {
1204                        0usize
1205                    }
1206                }),
1207            })
1208            .collect();
1209        quote!(0usize #(+ #terms)*)
1210    } else {
1211        let n = resources.len();
1212        quote!(#n)
1213    };
1214    (len, gate_refs)
1215}
1216
1217/// `" (from fragment \`X\`)"` when the item was forwarded through a
1218/// `supervisor_fragment!` relay, else empty — error-message attribution.
1219fn fragment_suffix(fragment: &Option<String>) -> String {
1220    match fragment {
1221        Some(f) => format!(" (from fragment `{f}`)"),
1222        None => String::new(),
1223    }
1224}
1225
1226/// Build the `[&'static TaskNode; n]` element and length tokens for a node's or
1227/// pool's `ready`-marked deps, cfg-aware like `gate_tokens`. A dep naming a pool
1228/// resolves to the pool's floor member (`&POOL[0]`), matching how `deps: [POOL]`
1229/// resolves for spawn ordering.
1230fn ready_tokens(
1231    deps: &[Dep],
1232    pool_names: &std::collections::HashSet<String>,
1233) -> Option<(TokenStream2, Vec<TokenStream2>)> {
1234    let marked: Vec<&Dep> = deps.iter().filter(|d| d.ready.is_some()).collect();
1235    if marked.is_empty() {
1236        return None;
1237    }
1238    let refs: Vec<TokenStream2> = marked
1239        .iter()
1240        .map(|d| {
1241            let cfg = &d.cfg;
1242            let ident = &d.ident;
1243            if pool_names.contains(&ident.to_string()) {
1244                quote!(#(#cfg)* &#ident[0])
1245            } else {
1246                quote!(#(#cfg)* &#ident)
1247            }
1248        })
1249        .collect();
1250    let any_cfg = marked.iter().any(|d| cfg_predicate(&d.cfg).is_some());
1251    let len = if any_cfg {
1252        let terms: Vec<TokenStream2> = marked
1253            .iter()
1254            .map(|d| match cfg_predicate(&d.cfg) {
1255                None => quote!(1usize),
1256                Some(pred) => quote!({
1257                    #[cfg(#pred)]
1258                    {
1259                        1usize
1260                    }
1261                    #[cfg(not(#pred))]
1262                    {
1263                        0usize
1264                    }
1265                }),
1266            })
1267            .collect();
1268        quote!(0usize #(+ #terms)*)
1269    } else {
1270        let n = marked.len();
1271        quote!(#n)
1272    };
1273    Some((len, refs))
1274}
1275
1276/// Extract the policy *type* from a `Type::new(..)` constructor expression. Only used
1277/// on the derive path (no explicit `policy: <Ty> = ..` annotation); the type is the
1278/// call's path minus its last segment (`DeferredShrink::new` -> `DeferredShrink`).
1279fn policy_type(expr: &Expr) -> SynResult<Path> {
1280    if let Expr::Call(call) = expr
1281        && let Expr::Path(p) = &*call.func
1282    {
1283        let n = p.path.segments.len();
1284        if n >= 2 {
1285            let segs: Punctuated<_, Token![::]> =
1286                p.path.segments.iter().take(n - 1).cloned().collect();
1287            return Ok(Path {
1288                leading_colon: p.path.leading_colon,
1289                segments: segs,
1290            });
1291        }
1292    }
1293    Err(syn::Error::new_spanned(
1294        expr,
1295        "pool `policy:` must be a `Type::new(..)` constructor (e.g. `DeferredShrink::new(..)`), \
1296         or give the type explicitly: `policy: <Type> = <expr>`",
1297    ))
1298}
1299
1300/// One emitted node slot, in final index order.
1301struct Slot {
1302    /// Presence predicate (`None` = unconditional), gates the node slot (`GRAPH.nodes`) entry.
1303    cfg_pred: Option<TokenStream2>,
1304    /// `&NODE` or `&POOL[j]`.
1305    reference: TokenStream2,
1306    /// Raw deps, resolved to indices in the second pass.
1307    deps: Vec<Dep>,
1308    /// The `supervisor_fragment!` the owning item came from, for error
1309    /// attribution when a dep fails to resolve across the relay.
1310    fragment: Option<String>,
1311}
1312
1313/// The `Option<fn(..)>` spawn expression for a node. `None` (no `spawn:`) is a
1314/// parked node the app spawns itself. A path or partial call is a task fn taking
1315/// `&NODE` first (plus any given args); the macro wraps it as
1316/// `|s| { s.spawn(<task>(&NODE, ..)?); Ok(()) }`. Anything else (a closure, or a
1317/// ready spawn fn) is emitted verbatim. Every form is cast to `spawn_fn` so it
1318/// coerces cleanly inside `Option::Some(..)`.
1319fn node_spawn(
1320    ident: &Ident,
1321    spawn: &Option<Expr>,
1322    executor: &Option<Ident>,
1323    resources: &[ResourceDecl],
1324    // `state:`: fallibly box the init value in the glue, BEFORE the resource
1325    // takes (a failed alloc strands nothing) — `SpawnError::Busy`, retryable.
1326    state: Option<&(syn::Type, Expr)>,
1327    spawn_fn: &TokenStream2,
1328    helpers: &HelperIdents,
1329) -> SynResult<TokenStream2> {
1330    // `resources:` take-prelude + the taken values as extra shell arguments.
1331    // Taking here — in the glue, BEFORE the spawn — is the point: an unprovided
1332    // slot fails `Supervisor::start` with `SpawnError::Busy` (the supervisor
1333    // logs the node name), instead of panicking inside an already-spawned task.
1334    // The values ride into the task as ordinary `#[embassy_executor::task]`
1335    // arguments (embassy stores them in the shell's TaskPool slot). A `shared`
1336    // entry copies the value out non-destructively (`get()` — the slot stays
1337    // filled for the other consumers) instead of `take()`ing it; `get`'s
1338    // `T: Copy` bound is what enforces "shared handles must be Copy".
1339    let take_prelude: Vec<TokenStream2> = resources
1340        .iter()
1341        .enumerate()
1342        .map(|(i, r)| {
1343            let cfg = &r.cfg;
1344            let res = &r.ident;
1345            let var = format_ident!("__r{}", i);
1346            let getter = if r.shared.is_some() {
1347                quote!(get)
1348            } else {
1349                quote!(take)
1350            };
1351            quote! {
1352                #(#cfg)*
1353                let #var = #res
1354                    .#getter()
1355                    .ok_or(::embassy_executor::SpawnError::Busy)?;
1356            }
1357        })
1358        .collect();
1359    // Per-entry `#[cfg]` rides on the call ARGUMENT too (stable in call
1360    // position, like the cfg'd array elements in the deps table), so a
1361    // cfg'd-out entry vanishes from the glue, the shell signature, and the
1362    // worker call consistently.
1363    let res_args: Vec<TokenStream2> = resources
1364        .iter()
1365        .enumerate()
1366        .map(|(i, r)| {
1367            let cfg = &r.cfg;
1368            let var = format_ident!("__r{}", i);
1369            quote!(#(#cfg)* #var)
1370        })
1371        .collect();
1372    let try_box = &helpers.try_box;
1373    let (state_prelude, state_arg) = match state {
1374        Some((_, init)) => (
1375            quote! {
1376                let __state = #try_box(#init)
1377                    .ok_or(::embassy_executor::SpawnError::Busy)?;
1378            },
1379            vec![quote!(__state)],
1380        ),
1381        None => (quote!(), vec![]),
1382    };
1383    Ok(match (spawn, executor) {
1384        (None, None) => quote!(::core::option::Option::None),
1385        // `executor:` needs the macro to perform the spawn, so it composes only
1386        // with the path / partial-call `spawn:` forms below.
1387        (None, Some(ex)) => {
1388            return Err(syn::Error::new_spanned(
1389                ex,
1390                "`executor:` requires a `spawn:` (a parked node is spawned by the \
1391                 application, which picks its own spawner)",
1392            ));
1393        }
1394        // A path or a partial call: a task fn taking `&NODE` first (plus any
1395        // given args); generate `|s| { s.spawn(<task>(&NODE, ..)?); Ok(()) }`.
1396        // With `executor: NAME` the glue ignores the supervisor's `Spawner` and
1397        // spawns through the named `SpawnerSlot` (a `SendSpawner` the app
1398        // registers at runtime): an unfilled slot fails the spawn with
1399        // `SpawnError::Busy` — loud misconfiguration, not a missing task. The
1400        // task future must then be `Send` (enforced by `SendSpawner::spawn`).
1401        (Some(e @ (Expr::Path(_) | Expr::Call(_))), executor) => {
1402            let mut lead: Vec<TokenStream2> = vec![quote!(&#ident)];
1403            lead.extend(res_args.iter().cloned());
1404            lead.extend(state_arg.iter().cloned());
1405            let call = inject_call_with(e, &lead)?;
1406            match executor {
1407                None => {
1408                    let stmts = spawn_stmts(&call, &quote!(&#ident), &quote!(s));
1409                    quote!(::core::option::Option::Some(
1410                        (|s| {
1411                            #state_prelude
1412                            #(#take_prelude)*
1413                            #stmts
1414                            ::core::result::Result::Ok(())
1415                        }) as #spawn_fn
1416                    ))
1417                }
1418                Some(ex) => {
1419                    let stmts = spawn_stmts(&call, &quote!(&#ident), &quote!(__sp));
1420                    quote!(::core::option::Option::Some(
1421                        (|_s| {
1422                            // The supervisor awaits this slot's `ready()` before
1423                            // invoking the glue (the node carries `.with_executor(&EX)`
1424                            // and the bring-up bounds the wait), so `get()` is already
1425                            // filled; `ok_or` is the belt-and-braces unfilled guard.
1426                            // Resources are taken AFTER the spawner guard, so an
1427                            // unfilled executor never consumes (and strands) them.
1428                            let __sp = #ex
1429                                .get()
1430                                .ok_or(::embassy_executor::SpawnError::Busy)?;
1431                            #state_prelude
1432                            #(#take_prelude)*
1433                            #stmts
1434                            ::core::result::Result::Ok(())
1435                        }) as #spawn_fn
1436                    ))
1437                }
1438            }
1439        }
1440        (Some(_), Some(ex)) => {
1441            return Err(syn::Error::new_spanned(
1442                ex,
1443                "`executor:` cannot be combined with a verbatim spawn closure (the \
1444                 closure owns the spawn; use the named SpawnerSlot inside it instead)",
1445            ));
1446        }
1447        // Anything else (a closure, or a ready spawn fn) is emitted verbatim.
1448        // NOTE: with the `trace` feature such a node is not auto-mapped — the
1449        // closure owns the SpawnToken; call `adopt`/`set_task_id` in it yourself.
1450        (Some(e), None) => quote!(::core::option::Option::Some((#e) as #spawn_fn)),
1451    })
1452}
1453
1454/// The spawn statement(s) for the generated glue. Plain `s.spawn(<call>?)`
1455/// normally; with the `trace` feature the `SpawnToken` is bound first so its task
1456/// id can be captured into the node (`set_task_id`) — the id→node mapping the
1457/// supervisor's `trace` recorders resolve against (in embassy-executor 0.10 the
1458/// task-fn call returns `Result<SpawnToken, SpawnError>` and `Spawner::spawn`
1459/// itself is infallible, so the token is available between the two).
1460///
1461/// Three shapes, resolved at expansion by the macro crate's own features:
1462/// * `trace` on → bind the token and `adopt` it (`set_task_id` + name stamp under
1463///   `metadata-names`).
1464/// * `trace` off but `metadata-names` on → bind the token and `stamp_name` only:
1465///   the node name reaches the task Metadata (for rtos-trace/SystemView) with no id
1466///   capture and no dependency on the `_embassy_trace_*` hooks.
1467/// * neither → plain infallible spawn.
1468fn spawn_stmts(call: &TokenStream2, node_ref: &TokenStream2, sp: &TokenStream2) -> TokenStream2 {
1469    if cfg!(feature = "trace") {
1470        // `adopt` = set_task_id + (under metadata-names) Metadata name stamp.
1471        quote! {
1472            let __token = #call?;
1473            (#node_ref).adopt(&__token);
1474            #sp.spawn(__token);
1475        }
1476    } else if cfg!(feature = "metadata-names") {
1477        // Name-only path: stamp the node name into the task Metadata, nothing else.
1478        quote! {
1479            let __token = #call?;
1480            (#node_ref).stamp_name(&__token);
1481            #sp.spawn(__token);
1482        }
1483    } else {
1484        quote!(#sp.spawn(#call?);)
1485    }
1486}
1487
1488/// Emit the `#[embassy_executor::task]` shell for a `task:` clause: a concrete,
1489/// non-generic task fn that takes only the node and awaits the user's worker with
1490/// the node injected first. This is how a **generic** worker becomes spawnable —
1491/// embassy forbids generic tasks (one static `TaskPool` per concrete future type),
1492/// so a monomorphized shell is stamped per declaration. Worker args are evaluated
1493/// inside the shell — at the task's first poll, on the node's own executor — so
1494/// the DSL never needs the arg types and a cross-core node builds its resources on
1495/// the core that runs them.
1496///
1497/// Returns the shell item and a path `Expr` naming it, which feeds the ordinary
1498/// `spawn:` path-form glue (executor routing and trace `adopt` compose unchanged).
1499// One argument per independent codegen input; a bundling struct would only
1500// rename the coupling.
1501#[allow(clippy::too_many_arguments)]
1502fn emit_shell(
1503    owner: &Ident,
1504    cfg: &[Attribute],
1505    worker: &Expr,
1506    pool_size: usize,
1507    resources: &[ResourceDecl],
1508    exit: Option<&syn::Type>,
1509    // `state: Type = ..`: the shell owns the glue-boxed state across the worker
1510    // call (worker sees `&mut Type`) and DROPS it first thing after the worker
1511    // returns — reclaimed before restores/exit-provide/mark_exited.
1512    state: Option<&(syn::Type, Expr)>,
1513    // Pool shells restore lend entries to a slot REFERENCE parameter (the
1514    // member's own array element, passed by the wrapper) instead of a slot
1515    // named statically — restore-to-same-index by construction.
1516    pool_member: bool,
1517    // `cancel`: drive the worker under `run_cancellable` and DON'T lead its
1518    // arguments with the node — the worker is a plain future that never returns
1519    // on its own, so the shell owns the shutdown race on its behalf.
1520    cancel: bool,
1521    cr: &TokenStream2,
1522    helpers: &HelperIdents,
1523) -> SynResult<(TokenStream2, Expr)> {
1524    if !matches!(worker, Expr::Path(_) | Expr::Call(_)) {
1525        return Err(syn::Error::new_spanned(
1526            worker,
1527            "`task:` names an async worker fn — a path or a partial call like \
1528             `worker(args)`; for a closure or a ready spawn fn use `spawn:`",
1529        ));
1530    }
1531    let shell = format_ident!("__sv_task_{}", owner.to_string().to_lowercase());
1532    // `resources:` values arrive as owned task arguments (the spawn glue took
1533    // them out of their slots); the shell keeps ownership, lends the worker
1534    // `&mut`, and restores each value to its slot after the worker returns —
1535    // i.e. after the worker's clean shutdown ack — so a Terminate respawn
1536    // re-takes the SAME instance instead of re-acquiring hardware. A `Pause`
1537    // worker parks instead of returning, so it simply retains its resources
1538    // (the restore lines below are unreachable for it — correct, same as a
1539    // hand-written parked task holding its arguments).
1540    //
1541    // A `consume` entry is forwarded to the worker BY VALUE instead — the worker
1542    // owns it (it can drop it at teardown, e.g. a driver whose `Drop` releases
1543    // pins/DMA) and no restore is emitted: the slot stays empty until the app
1544    // re-`provide()`s, which the supervisor's pre-respawn gate wait turns into
1545    // fail-closed `SpawnError::Busy` rather than a stale-value reuse.
1546    //
1547    // A `shared` entry is also by value with no restore — but because the glue
1548    // COPIED it out (`get()`), the slot stays filled; the worker's value is its
1549    // own copy of the fan-out handle.
1550    //
1551    // Per-entry `#[cfg]` rides on params, worker-call arguments, and restore
1552    // statements alike, so a cfg'd-out entry disappears from the whole chain
1553    // (the worker fn must gate its matching parameter with the same `#[cfg]`).
1554    let by_value = |r: &ResourceDecl| r.consume.is_some() || r.shared.is_some();
1555    let res_params: Vec<TokenStream2> = resources
1556        .iter()
1557        .enumerate()
1558        .map(|(i, r)| {
1559            let cfg = &r.cfg;
1560            let var = format_ident!("__r{}", i);
1561            let ty = &r.ty;
1562            if by_value(r) {
1563                quote!(#(#cfg)* #var: #ty)
1564            } else if pool_member {
1565                // Lend entry of a pool: value + the member's own slot element.
1566                let slot_param = format_ident!("__r{}_slot", i);
1567                quote!(#(#cfg)* mut #var: #ty, #(#cfg)* #slot_param: &'static #cr::ResourceSlot<#ty>)
1568            } else {
1569                quote!(#(#cfg)* mut #var: #ty)
1570            }
1571        })
1572        .collect();
1573    let res_leases: Vec<TokenStream2> = resources
1574        .iter()
1575        .enumerate()
1576        .map(|(i, r)| {
1577            let cfg = &r.cfg;
1578            let var = format_ident!("__r{}", i);
1579            if by_value(r) {
1580                quote!(#(#cfg)* #var)
1581            } else {
1582                quote!(#(#cfg)* &mut #var)
1583            }
1584        })
1585        .collect();
1586    let restores: Vec<TokenStream2> = resources
1587        .iter()
1588        .enumerate()
1589        .filter(|(_, r)| !by_value(r))
1590        .map(|(i, r)| {
1591            let cfg = &r.cfg;
1592            let var = format_ident!("__r{}", i);
1593            if pool_member {
1594                let slot_param = format_ident!("__r{}_slot", i);
1595                quote!(#(#cfg)* #slot_param.restore(#var);)
1596            } else {
1597                let res = &r.ident;
1598                quote!(#(#cfg)* #res.restore(#var);)
1599            }
1600        })
1601        .collect();
1602    let alloc_alias = &helpers.alloc_alias;
1603    let (state_param, state_lease, state_drop) = match state {
1604        Some((ty, _)) => (
1605            quote!(, mut __state: #alloc_alias::boxed::Box<#ty>),
1606            vec![quote!(&mut *__state)],
1607            // Reclaim the bulk FIRST: before restores, exit-provide, and the
1608            // completion record, so has_exited() implies the heap is back.
1609            quote!(::core::mem::drop(__state);),
1610        ),
1611        None => (quote!(), vec![], quote!()),
1612    };
1613    // `cancel` workers take no node: the shell holds it and races the worker's
1614    // future against the shutdown signal itself, which is the whole point of the
1615    // flag — the worker stays a plain async fn with no supervisor in its
1616    // signature.
1617    let mut lead: Vec<TokenStream2> = if cancel {
1618        Vec::new()
1619    } else {
1620        vec![quote!(__node)]
1621    };
1622    lead.extend(res_leases);
1623    lead.extend(state_lease);
1624    let call = inject_call_with(worker, &lead)?;
1625    // Unsuffixed literal: `#[task]`'s own parser wants a plain integer.
1626    let ps = LitInt::new(&pool_size.to_string(), proc_macro2::Span::call_site());
1627    // A diverging (`-> !`) worker makes the trailing statements unreachable —
1628    // legitimate (a detached/`Pause` worker retains its resources forever), so
1629    // silence rustc's `unreachable_code` lint on the generated body. Always
1630    // emitted: the completion record below is an unconditional trailing
1631    // statement.
1632    let allow_unreachable = quote!(#[allow(unreachable_code)]);
1633    // `exit: Type`: bind the worker's return value and provide() it into the
1634    // node's exit slot BEFORE mark_exited, so has_exited() implies the value is
1635    // present. A worker whose return type mismatches the declared `exit:` fails
1636    // at this provide with a plain rustc type error on the shell.
1637    // Under `cancel` the worker may not have returned at all — the shell holds a
1638    // `Result<Output, Aborted>` — so the exit value is provided only on a real
1639    // completion. An aborted worker leaves `<NODE>_EXIT` empty (and
1640    // `shutdown_requested()` set), which is how a waiter tells "it finished" from
1641    // "it was stopped".
1642    //
1643    // A DIVERGING worker (`-> !`) makes that provide dead code: its future has
1644    // no output, so the slot could never be filled and every `wait_take()` on
1645    // it would hang forever. The blanket allow above would hide that, so the
1646    // provide re-DENIES `unreachable_code` on itself — the one statement in the
1647    // shell where unreachability is a declaration error rather than a
1648    // legitimate parked/detached worker. Spanned on the declared `exit:` type,
1649    // so rustc points at the clause the user has to remove (a bare diverging
1650    // worker stays legal: that is what `cancel` is for).
1651    let exit_ident = format_ident!("{}_EXIT", owner);
1652    let provide = |exit: &syn::Type| {
1653        // Every token of the statement carries the `exit:` type's span, so the
1654        // lint's own label lands on that clause instead of the whole item.
1655        let slot = Ident::new(&exit_ident.to_string(), exit.span());
1656        quote::quote_spanned!(exit.span()=>
1657            #[deny(unreachable_code)]
1658            #slot.provide(__out);
1659        )
1660    };
1661    let (drive, provide_exit) = match (cancel, exit) {
1662        (false, Some(ty)) => {
1663            let provide = provide(ty);
1664            (quote!(let __out = #call.await;), provide)
1665        }
1666        (false, None) => (quote!(#call.await;), quote!()),
1667        (true, Some(ty)) => {
1668            let provide = provide(ty);
1669            (
1670                quote!(let __res = __node.run_cancellable(#call).await;),
1671                quote!(if let ::core::result::Result::Ok(__out) = __res {
1672                    #provide
1673                }),
1674            )
1675        }
1676        (true, None) => (
1677            quote!(let _ = __node.run_cancellable(#call).await;),
1678            quote!(),
1679        ),
1680    };
1681    let def = quote! {
1682        #(#cfg)*
1683        #[::embassy_executor::task(pool_size = #ps)]
1684        #allow_unreachable
1685        async fn #shell(__node: &'static #cr::TaskNode #(, #res_params)* #state_param) {
1686            #drive
1687            #state_drop
1688            #(#restores)*
1689            #provide_exit
1690            // Record the completion (and ack any pending shutdown handshake):
1691            // a worker that returns on its own reads as down, not running
1692            // forever, and a control Activate can respawn it.
1693            __node.mark_exited();
1694        }
1695    };
1696    let path: Expr = syn::parse_quote!(#shell);
1697    Ok((def, path))
1698}
1699
1700/// Emit a `node`: its `pub static #ident: TaskNode` definition and its `Slot`. The
1701/// caller assigns the slot index and records the name, so this touches neither.
1702/// A `task:` node additionally emits its generated shell ahead of the static.
1703fn emit_node(
1704    n: &NodeItem,
1705    cr: &TokenStream2,
1706    spawn_fn: &TokenStream2,
1707    // Threaded to ready_tokens: a ready dep naming a pool refs its floor member.
1708    pool_names: &std::collections::HashSet<String>,
1709    helpers: &HelperIdents,
1710) -> SynResult<(TokenStream2, Slot)> {
1711    let ident = &n.ident;
1712    let cfg = &n.cfg;
1713    let mode = &n.mode;
1714    let name = name_string(&n.ident);
1715    let disabled = n.disabled;
1716    let (shell_def, spawn_expr) = match &n.source {
1717        Some(TaskSource::Shell(worker)) => {
1718            let ps = match &n.pool_size {
1719                Some(l) => l.base10_parse::<usize>()?,
1720                None => 1,
1721            };
1722            let (def, path) = emit_shell(
1723                ident,
1724                cfg,
1725                worker,
1726                ps,
1727                &n.resources,
1728                n.exit.as_ref(),
1729                n.state.as_ref(),
1730                false,
1731                n.cancel,
1732                cr,
1733                helpers,
1734            )?;
1735            (def, Some(path))
1736        }
1737        Some(TaskSource::Spawn(e)) => (quote!(), Some(e.clone())),
1738        None => (quote!(), None),
1739    };
1740    let spawn = node_spawn(
1741        ident,
1742        &spawn_expr,
1743        &n.executor,
1744        &n.resources,
1745        n.state.as_ref(),
1746        spawn_fn,
1747        helpers,
1748    )?;
1749    // `executor: NAME` routes the node through that SpawnerSlot; the supervisor
1750    // awaits the slot before spawning (see `TaskNode::with_executor`).
1751    let with_exec = match &n.executor {
1752        Some(ex) => quote!( .with_executor(&#ex) ),
1753        None => quote!(),
1754    };
1755    // `resources: [NAME: Type, ..]` — one `pub static NAME: ResourceSlot<Type>`
1756    // per entry (main moves the resource in with `NAME.provide(..)`), plus a
1757    // type-erased gate array wired into the node so the supervisor can await
1758    // provisioning/restore before each (re)spawn (see `TaskNode::with_resources`).
1759    // The unsized coercion `&NAME` -> `&dyn ResourceGate` happens in the static
1760    // initializer, where it is allowed.
1761    let (res_defs, with_res) = if n.resources.is_empty() {
1762        (quote!(), quote!())
1763    } else {
1764        let gates_ident = format_ident!("__SV_GATES_{}", ident);
1765        // `shared` slots are emitted once per graph in `expand` (several items
1766        // may declare the same one); only this node's exclusive (take-kind)
1767        // slots are emitted here.
1768        let slot_defs = n.resources.iter().filter(|r| r.shared.is_none()).map(|r| {
1769            let ecfg = &r.cfg;
1770            let res = &r.ident;
1771            let ty = &r.ty;
1772            // `local` entries use the graph-site slot type (emitted once per
1773            // graph in `expand`): same provide/take protocol as `ResourceSlot`
1774            // but without its `T: Send` bound, for `!Send` driver handles on a
1775            // single-core system. `consume` changes only shell codegen (by-value
1776            // arg, no restore) — the slot type is the same either way.
1777            let slot_ty = if r.local.is_some() {
1778                let local = &helpers.local_slot;
1779                quote!(#local<#ty>)
1780            } else {
1781                quote!(#cr::ResourceSlot<#ty>)
1782            };
1783            let doc = if r.consume.is_some() {
1784                format!(
1785                    "Resource slot for node `{ident}` (generated by `supervisor_graph!`). \
1786                         Move the resource in with `.provide(..)` before `Supervisor::start`. \
1787                         `consume`: the worker owns (and may drop) the value, so the slot is \
1788                         empty after the task exits — re-`provide()` before any respawn."
1789                )
1790            } else {
1791                format!(
1792                    "Resource slot for node `{ident}` (generated by `supervisor_graph!`). \
1793                         Move the resource in with `.provide(..)` before `Supervisor::start`."
1794                )
1795            };
1796            quote! {
1797                #(#cfg)*
1798                #(#ecfg)*
1799                #[doc = #doc]
1800                pub static #res: #slot_ty = <#slot_ty>::new();
1801            }
1802        });
1803        let (gates_len, gate_refs) = gate_tokens(&n.resources);
1804        (
1805            quote! {
1806                #(#slot_defs)*
1807                #(#cfg)*
1808                static #gates_ident: [&'static dyn #cr::ResourceGate; #gates_len] =
1809                    [#(#gate_refs),*];
1810            },
1811            quote!( .with_resources(&#gates_ident) ),
1812        )
1813    };
1814    // `slot_timeout: N` — override the node's pre-spawn slot/gate wait bound
1815    // (see `TaskNode::with_slot_timeout`; sized to a provider node's build time).
1816    let with_timeout = match &n.slot_timeout {
1817        Some(ms) => quote!( .with_slot_timeout(#cr::_export::Duration::from_millis(#ms)) ),
1818        None => quote!(),
1819    };
1820    // `deps: [X ready, ..]` — the ready-marked subset becomes a per-node
1821    // `[&'static TaskNode; n]` array wired via `.with_ready_deps`: bring-up
1822    // awaits each one's set_ready() (bounded by slot_timeout) after the
1823    // resource gates. Spawn-order deps are unaffected (same DEPS table).
1824    let (ready_def, with_ready) = match ready_tokens(&n.deps, pool_names) {
1825        Some((len, refs)) => {
1826            let ready_ident = format_ident!("__SV_READY_{}", ident);
1827            (
1828                quote! {
1829                    #(#cfg)*
1830                    static #ready_ident: [&'static #cr::TaskNode; #len] = [#(#refs),*];
1831                },
1832                quote!( .with_ready_deps(&#ready_ident) ),
1833            )
1834        }
1835        None => (quote!(), quote!()),
1836    };
1837    // `exit: Type` — one `pub static <NODE>_EXIT: ResourceSlot<Type>` the shell
1838    // provide()s the worker's return value into just before mark_exited. Plain
1839    // `ResourceSlot` on purpose: it is an outbound mailbox, not a gated input,
1840    // so it joins no gate array (an empty exit slot must not block a spawn).
1841    let exit_def = match &n.exit {
1842        Some(ty) => {
1843            let exit_ident = format_ident!("{}_EXIT", ident);
1844            let doc = format!(
1845                "Exit-value slot for node `{ident}` (generated by `supervisor_graph!`). \
1846                 The generated shell `provide()`s the worker's return value here just \
1847                 before recording the exit; read it with `.wait_take()` (or `.take()` \
1848                 after `has_exited()`). Overwritten by the next completion."
1849            );
1850            quote! {
1851                #(#cfg)*
1852                #[doc = #doc]
1853                pub static #exit_ident: #cr::ResourceSlot<#ty> =
1854                    #cr::ResourceSlot::new();
1855            }
1856        }
1857        None => quote!(),
1858    };
1859    // Every emitted `pub` item carries a doc string: a consumer crate may be
1860    // `#![deny(missing_docs)]`, and the lint fires on macro-generated items.
1861    let node_doc = format!(
1862        "Supervised node `{ident}` (`{mode}`), generated by `supervisor_graph!`. \
1863         Pass it to the supervisor's per-node verbs (`start_node`, `stop_node`, \
1864         `resume_node`, `activate`/`deactivate`); the worker gets the same \
1865         `&'static TaskNode` for the task-side protocol."
1866    );
1867    let def = quote! {
1868        #res_defs
1869        #exit_def
1870        #ready_def
1871        #shell_def
1872        #(#cfg)*
1873        #[doc = #node_doc]
1874        pub static #ident: #cr::TaskNode =
1875            #cr::TaskNode::new(#name, #cr::Mode::#mode, #spawn, #disabled)
1876                #with_exec #with_res #with_timeout #with_ready;
1877    };
1878    let slot = Slot {
1879        cfg_pred: cfg_predicate(cfg),
1880        reference: quote!(&#ident),
1881        deps: n.deps.clone(),
1882        fragment: n.fragment.clone(),
1883    };
1884    Ok((def, slot))
1885}
1886
1887/// Emit a `pool`: the member `[TaskNode; K]` array, the `spawn_<pool>` glue fn, and
1888/// the `ElasticPool` static (returned as `defs`, in that emission order), plus the
1889/// pool-registry entry (for `GRAPH.pools`) and one `Slot` per member (members occupy
1890/// slots but aren't name-addressable, so no name is recorded).
1891fn emit_pool(
1892    p: &PoolItem,
1893    cr: &TokenStream2,
1894    spawn_fn: &TokenStream2,
1895    // Threaded to ready_tokens: a ready dep naming a pool refs its floor member.
1896    pool_names: &std::collections::HashSet<String>,
1897    helpers: &HelperIdents,
1898) -> SynResult<(Vec<TokenStream2>, TokenStream2, Vec<Slot>)> {
1899    let ident = &p.ident;
1900    let cfg = &p.cfg;
1901    let lname = name_string(&p.ident);
1902    let pool_static = format_ident!("{}_POOL", ident);
1903    let k = p.modes.len();
1904
1905    // Validate the scaling bounds. Two paths:
1906    // - both int literals (the common case): validated HERE, at expansion time,
1907    //   with the best possible spans. `base10_parse::<u8>` also rejects values
1908    //   > 255 (the `ElasticPool` fields are `u8`).
1909    // - otherwise (paths, const exprs — e.g. `min: HTTP_FLOOR`): the emitted
1910    //   `<POOL>_MIN`/`<POOL>_MAX` consts become the source of truth and
1911    //   `const _: () = assert!(..)` guards enforce min <= max <= members <= 255
1912    //   at const-eval time (rendered like the cycle error, with rust-src spans).
1913    // `min > max` makes the policy contradict itself; `max > k` is a ceiling the
1914    // pool can never reach (only `k` member slots exist) — declaration bugs
1915    // either way. `max < k` is allowed (spare declared members below the
1916    // ceiling), as is `min: 0` (scale to zero when idle). The member count `k`
1917    // itself stays a structural literal: it drives how many nodes, shells, name
1918    // strings and graph slots are EMITTED, which a proc macro cannot derive
1919    // from a const it can't evaluate.
1920    let lit_bounds = match (&p.min, &p.max) {
1921        (Expr::Lit(lmin), Expr::Lit(lmax)) => match (&lmin.lit, &lmax.lit) {
1922            (syn::Lit::Int(imin), syn::Lit::Int(imax)) => {
1923                Some((imin.base10_parse::<u8>()?, imax.base10_parse::<u8>()?))
1924            }
1925            _ => None,
1926        },
1927        _ => None,
1928    };
1929    if let Some((min_v, max_v)) = lit_bounds {
1930        if min_v > max_v {
1931            return Err(syn::Error::new_spanned(
1932                &p.min,
1933                format!("pool `min:` ({min_v}) must not exceed `max:` ({max_v})"),
1934            ));
1935        }
1936        if usize::from(max_v) > k {
1937            return Err(syn::Error::new_spanned(
1938                &p.max,
1939                format!("pool `max:` ({max_v}) exceeds the declared member count ({k})"),
1940            ));
1941        }
1942    }
1943
1944    // Pool `resources:` (all `shared`, enforced at parse) additionally require
1945    // `task:` — same rule as nodes: the generated shell is what receives the
1946    // values as arguments (a hand-written `spawn:` task fn manages its own).
1947    if !p.resources.is_empty() && matches!(p.source, TaskSource::Spawn(_)) {
1948        return Err(syn::Error::new_spanned(
1949            &p.resources[0].ident,
1950            "pool `resources:` requires `task:` — the values are handed to the \
1951             generated shell as arguments (and lend entries restored by it); a \
1952             `spawn:` task fn manages its own arguments",
1953        ));
1954    }
1955    if let Some((ty, _)) = &p.state
1956        && matches!(p.source, TaskSource::Spawn(_))
1957    {
1958        return Err(syn::Error::new_spanned(
1959            ty,
1960            "pool `state:` requires `task:` — the generated shell owns the boxed \
1961             state across the worker call; a `spawn:` task fn can Box its own",
1962        ));
1963    }
1964
1965    // Resolve the member task: `spawn:` uses the given expr directly; `task:`
1966    // first stamps ONE generated shell sized `pool_size = K` (all members share a
1967    // single concrete future type) and targets that. Shared resources become
1968    // by-value shell parameters, exactly like a node's.
1969    let (shell_def, member_expr) = match &p.source {
1970        TaskSource::Spawn(e) => (quote!(), e.clone()),
1971        TaskSource::Shell(worker) => emit_shell(
1972            ident,
1973            cfg,
1974            worker,
1975            k,
1976            &p.resources,
1977            None,
1978            p.state.as_ref(),
1979            true,
1980            p.cancel,
1981            cr,
1982            helpers,
1983        )?,
1984    };
1985    // Build member `I`'s spawn call from the member task, injecting `&POOL[I]`
1986    // as the first argument, then the shared resource copies (see
1987    // `inject_call_with`).
1988    let res_args: Vec<TokenStream2> = p
1989        .resources
1990        .iter()
1991        .enumerate()
1992        .flat_map(|(i, r)| {
1993            let ecfg = &r.cfg;
1994            let var = format_ident!("__r{}", i);
1995            let res = &r.ident;
1996            if r.shared.is_none() && r.consume.is_none() {
1997                // Lend: value + the member's own slot element, so the shell
1998                // restores to the same index it was taken from.
1999                vec![quote!(#(#ecfg)* #var), quote!(#(#ecfg)* &#res[I])]
2000            } else {
2001                vec![quote!(#(#ecfg)* #var)]
2002            }
2003        })
2004        .collect();
2005    let try_box = &helpers.try_box;
2006    let (state_prelude, state_arg) = match &p.state {
2007        Some((_, init)) => (
2008            quote! {
2009                let __state = #try_box(#init)
2010                    .ok_or(::embassy_executor::SpawnError::Busy)?;
2011            },
2012            vec![quote!(__state)],
2013        ),
2014        None => (quote!(), vec![]),
2015    };
2016    let mut lead: Vec<TokenStream2> = vec![quote!(&#ident[I])];
2017    lead.extend(res_args);
2018    lead.extend(state_arg);
2019    let call = inject_call_with(&member_expr, &lead)?;
2020    // Per-member spawn fn: a generated `spawn_<pool>::<I>` wrapper. Same optional
2021    // trace capture as a node's closure, against member `I`'s slot. With
2022    // `executor: NAME` the wrapper ignores the supervisor's `Spawner` and spawns
2023    // through the named SpawnerSlot; each member node carries `.with_executor(&EX)`,
2024    // so the supervisor awaits the slot (bounded) before invoking the wrapper and
2025    // the wrapper's `get()` is already filled (`SpawnError::Busy` guards a never-
2026    // filled slot; member futures must be `Send`). A whole worker pool can thus live
2027    // on another executor — e.g. the second core — while this core scales it.
2028    let (param, prelude, sp_tokens) = match &p.executor {
2029        None => (quote!(s), quote!(), quote!(s)),
2030        Some(ex) => (
2031            quote!(_s),
2032            quote! {
2033                let __sp = #ex
2034                    .get()
2035                    .ok_or(::embassy_executor::SpawnError::Busy)?;
2036            },
2037            quote!(__sp),
2038        ),
2039    };
2040    // Resource prelude, kind-aware. `shared`: copy the fan-out handle out
2041    // non-destructively (slot stays filled for the next member/consumer).
2042    // Take kinds (lend/consume): take from THIS member's array element —
2043    // `RES[I]`, per-member exclusive by construction. Either way an unprovided
2044    // slot fail-closes the member's spawn with `SpawnError::Busy`. After the
2045    // executor-slot guard, same ordering rationale as a node's glue.
2046    let get_prelude: Vec<TokenStream2> = p
2047        .resources
2048        .iter()
2049        .enumerate()
2050        .map(|(i, r)| {
2051            let ecfg = &r.cfg;
2052            let res = &r.ident;
2053            let var = format_ident!("__r{}", i);
2054            if r.shared.is_some() {
2055                quote! {
2056                    #(#ecfg)*
2057                    let #var = #res
2058                        .get()
2059                        .ok_or(::embassy_executor::SpawnError::Busy)?;
2060                }
2061            } else {
2062                quote! {
2063                    #(#ecfg)*
2064                    let #var = #res[I]
2065                        .take()
2066                        .ok_or(::embassy_executor::SpawnError::Busy)?;
2067                }
2068            }
2069        })
2070        .collect();
2071    let pool_spawn_stmts = spawn_stmts(&call, &quote!(&#ident[I]), &sp_tokens);
2072    let wrapper = format_ident!("spawn_{}", lname);
2073    let mut defs: Vec<TokenStream2> = Vec::new();
2074    defs.push(shell_def);
2075    defs.push(quote! {
2076        #(#cfg)*
2077        fn #wrapper<const I: usize>(
2078            #param: ::embassy_executor::Spawner,
2079        ) -> ::core::result::Result<(), ::embassy_executor::SpawnError> {
2080            #prelude
2081            #state_prelude
2082            #(#get_prelude)*
2083            #pool_spawn_stmts
2084            ::core::result::Result::Ok(())
2085        }
2086    });
2087    let member_spawn: Vec<TokenStream2> = (0..k).map(|j| quote!(#wrapper::<#j>)).collect();
2088
2089    // `executor: NAME` on the pool routes every member through that SpawnerSlot; the
2090    // supervisor awaits it before spawning each member (see `TaskNode::with_executor`).
2091    let member_with_exec = match &p.executor {
2092        Some(ex) => quote!( .with_executor(&#ex) ),
2093        None => quote!(),
2094    };
2095    // Take-kind entries (lend/consume) get per-member SLOT ARRAYS: member `I`
2096    // takes/restores index `I` exclusively, so members don't contend and the
2097    // elastic floor can come up with only floor-many elements provided. The
2098    // shared slot statics are emitted once per graph in `expand`, as for nodes.
2099    for r in p.resources.iter().filter(|r| r.shared.is_none()) {
2100        let ecfg = &r.cfg;
2101        let res = &r.ident;
2102        let ty = &r.ty;
2103        let doc = format!(
2104            "Per-member resource slots for pool `{ident}` (generated by \
2105             `supervisor_graph!`): member `I` takes/restores element `I`. \
2106             Provide at least the floor members' elements before \
2107             `Supervisor::start`; a member whose element is empty fail-closes \
2108             its (re)spawn with `SpawnError::Busy`."
2109        );
2110        defs.push(quote! {
2111            #(#cfg)*
2112            #(#ecfg)*
2113            #[doc = #doc]
2114            pub static #res: [#cr::ResourceSlot<#ty>; #k] =
2115                [const { #cr::ResourceSlot::new() }; #k];
2116        });
2117    }
2118    // Per-member gate arrays: member `j` gates on ITS OWN take-kind elements
2119    // plus the pool-wide shared slots. Same cfg-aware length for every member.
2120    let member_with_res: Vec<TokenStream2> = if p.resources.is_empty() {
2121        (0..k).map(|_| quote!()).collect()
2122    } else {
2123        let any_cfg = p.resources.iter().any(|r| cfg_predicate(&r.cfg).is_some());
2124        let gates_len = if any_cfg {
2125            let terms: Vec<TokenStream2> = p
2126                .resources
2127                .iter()
2128                .map(|r| match cfg_predicate(&r.cfg) {
2129                    None => quote!(1usize),
2130                    Some(pred) => quote!({
2131                        #[cfg(#pred)]
2132                        {
2133                            1usize
2134                        }
2135                        #[cfg(not(#pred))]
2136                        {
2137                            0usize
2138                        }
2139                    }),
2140                })
2141                .collect();
2142            quote!(0usize #(+ #terms)*)
2143        } else {
2144            let n = p.resources.len();
2145            quote!(#n)
2146        };
2147        (0..k)
2148            .map(|j| {
2149                let gates_ident = format_ident!("__SV_GATES_{}_{}", ident, j);
2150                let gate_refs: Vec<TokenStream2> = p
2151                    .resources
2152                    .iter()
2153                    .map(|r| {
2154                        let ecfg = &r.cfg;
2155                        let res = &r.ident;
2156                        if r.shared.is_some() {
2157                            quote!(#(#ecfg)* &#res)
2158                        } else {
2159                            quote!(#(#ecfg)* &#res[#j])
2160                        }
2161                    })
2162                    .collect();
2163                defs.push(quote! {
2164                    #(#cfg)*
2165                    static #gates_ident: [&'static dyn #cr::ResourceGate; #gates_len] =
2166                        [#(#gate_refs),*];
2167                });
2168                quote!( .with_resources(&#gates_ident) )
2169            })
2170            .collect()
2171    };
2172    // `deps: [X ready, ..]` — ONE shared ready-dep array for the whole pool
2173    // (markers apply to every member; growth also checks it synchronously).
2174    let member_with_ready = match ready_tokens(&p.deps, pool_names) {
2175        Some((len, refs)) => {
2176            let ready_ident = format_ident!("__SV_READY_{}", ident);
2177            defs.push(quote! {
2178                #(#cfg)*
2179                static #ready_ident: [&'static #cr::TaskNode; #len] = [#(#refs),*];
2180            });
2181            quote!( .with_ready_deps(&#ready_ident) )
2182        }
2183        None => quote!(),
2184    };
2185    // `slot_timeout: N` — every member's pre-spawn slot/gate wait bound.
2186    let member_with_timeout = match &p.slot_timeout {
2187        Some(ms) => quote!( .with_slot_timeout(#cr::_export::Duration::from_millis(#ms)) ),
2188        None => quote!(),
2189    };
2190    let members = p
2191        .modes
2192        .iter()
2193        .zip(&member_spawn)
2194        .enumerate()
2195        .map(|(j, (mode, sp))| {
2196            let nm = format!("{lname}{j}");
2197            let with_res = &member_with_res[j];
2198            quote! {
2199                #cr::TaskNode::new(
2200                    #nm, #cr::Mode::#mode,
2201                    ::core::option::Option::Some((#sp) as #spawn_fn), false,
2202                ) #member_with_exec #with_res #member_with_timeout #member_with_ready
2203            }
2204        });
2205    defs.push(quote! {
2206        #(#cfg)*
2207        #[doc = concat!("Pool `", stringify!(#ident), "`'s members, one `TaskNode` per slot \
2208            (index = member index). Index it for the per-node verbs; the pool itself is \
2209            `", stringify!(#ident), "_POOL`.")]
2210        pub static #ident: [#cr::TaskNode; #k] = [ #(#members),* ];
2211    });
2212
2213    // Structural constants, for downstream compile-time sizing (e.g. a socket
2214    // budget: `const BUDGET: usize = HTTP_MAX + 1`). Emitted because user code
2215    // can't derive them from the member array — a `const` cannot refer to a
2216    // `static` (E0013), so `HTTP.len()` is unusable in const context and the
2217    // count would otherwise have to be duplicated by hand next to the DSL.
2218    let min_const = format_ident!("{}_MIN", ident);
2219    let max_const = format_ident!("{}_MAX", ident);
2220    let members_const = format_ident!("{}_MEMBERS", ident);
2221    // Literal path: emit the *validated* u8 values. Expr path: the consts ARE
2222    // the source of truth (any const-evaluable usize expr) and const asserts
2223    // enforce what the literal path checked at expansion.
2224    let (min_tokens, max_tokens, bound_asserts) = match lit_bounds {
2225        Some((min_v, max_v)) => {
2226            let (min_u, max_u) = (usize::from(min_v), usize::from(max_v));
2227            (quote!(#min_u), quote!(#max_u), quote!())
2228        }
2229        None => {
2230            let (min_e, max_e) = (&p.min, &p.max);
2231            (
2232                quote!({ #min_e }),
2233                quote!({ #max_e }),
2234                quote! {
2235                    #(#cfg)*
2236                    const _: () = ::core::assert!(
2237                        #min_const <= #max_const,
2238                        "pool `min:` must not exceed `max:`",
2239                    );
2240                    #(#cfg)*
2241                    const _: () = ::core::assert!(
2242                        #max_const <= #members_const,
2243                        "pool `max:` exceeds the declared member count",
2244                    );
2245                    #(#cfg)*
2246                    const _: () = ::core::assert!(
2247                        #max_const <= 255,
2248                        "pool `max:` exceeds 255 (ElasticPool bounds are u8)",
2249                    );
2250                },
2251            )
2252        }
2253    };
2254    defs.push(quote! {
2255        #(#cfg)*
2256        #[doc = concat!("Pool `", stringify!(#ident), "`'s `min:` floor (validated at expansion or by const assert).")]
2257        pub const #min_const: usize = #min_tokens;
2258        #(#cfg)*
2259        #[doc = concat!("Pool `", stringify!(#ident), "`'s `max:` scaling ceiling — the most members ever running concurrently.")]
2260        pub const #max_const: usize = #max_tokens;
2261        #(#cfg)*
2262        #[doc = concat!("Pool `", stringify!(#ident), "`'s declared member count (the `[TaskNode; K]` array length).")]
2263        pub const #members_const: usize = #k;
2264        #bound_asserts
2265    });
2266
2267    let member_refs = (0..k).map(|j| quote!(&#ident[#j]));
2268    let policy = &p.policy;
2269    // The `ElasticPool<P>` type argument: honor an explicit `policy: <Ty> = ..`
2270    // annotation, else derive `P` from the constructor expr (`Ty::new(..)` shape).
2271    let policy_ty = match &p.policy_ty {
2272        Some(ty) => quote!(#ty),
2273        None => {
2274            let path = policy_type(policy)?;
2275            quote!(#path)
2276        }
2277    };
2278    // The u8 fields come from the emitted consts, which both paths validate
2279    // (parse-time for literals, const asserts otherwise) — so the `as u8` casts
2280    // cannot truncate. Going through the consts also keeps a suffixed literal
2281    // like `min: 3usize` working (the const is usize either way).
2282    defs.push(quote! {
2283        #(#cfg)*
2284        #[doc = concat!("The `ElasticPool` over the `", stringify!(#ident), "` members: \
2285            the `min:`/`max:` bounds and the scaling policy `Supervisor::run_pools` \
2286            drives. Also reachable through `GRAPH.pools`.")]
2287        pub static #pool_static: #cr::ElasticPool<#policy_ty> = #cr::ElasticPool {
2288            nodes: &[ #(#member_refs),* ],
2289            min: #min_const as u8,
2290            max: #max_const as u8,
2291            policy: #policy,
2292        };
2293    });
2294
2295    let pool_entry = quote!( #(#cfg)* &#pool_static );
2296
2297    let pred = cfg_predicate(cfg);
2298    let slots = (0..k)
2299        .map(|j| Slot {
2300            cfg_pred: pred.clone(),
2301            reference: quote!(&#ident[#j]),
2302            deps: p.deps.clone(),
2303            fragment: p.fragment.clone(),
2304        })
2305        .collect();
2306
2307    Ok((defs, pool_entry, slots))
2308}
2309
2310/// Second pass: build the node-slot entries for `GRAPH.nodes` (`Option`, cfg-gated) and
2311/// the cfg-aware dep-index entries for `GRAPH.deps`. Runs after every slot + name is
2312/// known, since a dep may forward-reference a node declared later. An unknown dep name
2313/// is a compile error.
2314fn slot_tables(
2315    slots: &[Slot],
2316    names: &HashMap<String, usize>,
2317) -> SynResult<(Vec<TokenStream2>, Vec<TokenStream2>)> {
2318    let mut all_entries: Vec<TokenStream2> = Vec::new();
2319    let mut deps_entries: Vec<TokenStream2> = Vec::new();
2320    for slot in slots {
2321        let reference = &slot.reference;
2322        all_entries.push(match &slot.cfg_pred {
2323            None => quote!(::core::option::Option::Some(#reference)),
2324            Some(pred) => quote!({
2325                #[cfg(#pred)]
2326                { ::core::option::Option::Some(#reference) }
2327                #[cfg(not(#pred))]
2328                { ::core::option::Option::None }
2329            }),
2330        });
2331
2332        let mut dep_toks: Vec<TokenStream2> = Vec::new();
2333        // Duplicate deps are a compile error: `deps: [A, A]` would emit a doubled
2334        // index, which `topo_sort_const` counts twice in the in-degree but decrements
2335        // once — misreported as a dependency cycle. Compared by *resolved* slot index
2336        // (so a repeated pool name trips it too); two cfg-gated variants of the same
2337        // dep are allowed only when their cfg predicates differ.
2338        let mut seen: Vec<(u8, String)> = Vec::new();
2339        for d in &slot.deps {
2340            let idx = match names.get(&d.ident.to_string()) {
2341                Some(&i) => i as u8,
2342                None => {
2343                    return Err(syn::Error::new_spanned(
2344                        &d.ident,
2345                        format!(
2346                            "unknown dependency `{}` — not a declared node or pool{}",
2347                            d.ident,
2348                            fragment_suffix(&slot.fragment),
2349                        ),
2350                    ));
2351                }
2352            };
2353            let cfg = &d.cfg;
2354            let cfg_key = quote!( #(#cfg)* ).to_string();
2355            if seen.iter().any(|(i, k)| *i == idx && *k == cfg_key) {
2356                return Err(syn::Error::new_spanned(
2357                    &d.ident,
2358                    format!("duplicate dependency `{}`", d.ident),
2359                ));
2360            }
2361            seen.push((idx, cfg_key));
2362            dep_toks.push(quote!( #(#cfg)* #idx ));
2363        }
2364        deps_entries.push(quote!( &[ #(#dep_toks),* ] ));
2365    }
2366    Ok((all_entries, deps_entries))
2367}
2368
2369fn expand(graph: GraphSpec) -> SynResult<TokenStream2> {
2370    let cr = quote!(::embassy_supervisor);
2371    let helpers = HelperIdents::new(graph.name.as_ref());
2372    // The node spawn fn-pointer type. Spawn exprs (closures / const-generic fns) are
2373    // cast to this so they coerce cleanly inside `Option::Some(..)`.
2374    let spawn_fn = quote!(
2375        fn(
2376            ::embassy_executor::Spawner,
2377        ) -> ::core::result::Result<(), ::embassy_executor::SpawnError>
2378    );
2379
2380    // First pass: emit the statics/glue in declaration order, assign stable slot
2381    // indices, and record each slot + its raw deps. `names` maps a dep-addressable ident
2382    // to its slot index for dep resolution — keyed on the *raw* ident (not the runtime
2383    // `name_string`). A `node` maps to its own slot; a `pool` maps to its floor member's
2384    // slot (so `deps: [POOL]` = "after the pool is up"). Individual pool members are not
2385    // separately name-addressable.
2386    let mut defs: Vec<TokenStream2> = Vec::new();
2387    let mut pool_entries: Vec<TokenStream2> = Vec::new();
2388    let mut slots: Vec<Slot> = Vec::new();
2389    let mut names: HashMap<String, usize> = HashMap::new();
2390
2391    // Iff any `resources:` entry is `local`-marked, emit the local slot TYPE once
2392    // per graph (the per-entry statics in `emit_node` reference it by name). It
2393    // mirrors `embassy_supervisor::ResourceSlot` — same provide/take/restore
2394    // protocol, same critical-section interior, same `ResourceGate` view — but
2395    // WITHOUT the `T: Send` bound, so it can carry the `!Send` driver handles
2396    // (`RefCell`-/`NoopRawMutex`-based: `embassy_net::Stack` runners,
2397    // `cyw43::Control`, …) that a single-core system hands between its own tasks.
2398    // That requires asserting `Sync` for a `!Send` payload, so like the
2399    // `trace-hooks` symbols it is emitted here, at the graph declaration site,
2400    // where the application owns the soundness contract (see the SAFETY note).
2401    // `state:` anywhere in the graph: emit the fallible-boxing helper ONCE, at
2402    // the graph site (like the local slot type). This is the `heap-state`
2403    // feature's ENTIRE unsafe surface, and it lives in the CONSUMER crate (the
2404    // `local-resources` precedent): raw alloc + null check + ptr::write +
2405    // Box::from_raw — after which it is a NORMAL Box, freed by ordinary drop
2406    // when the shell drops it on task exit. Alloc failure returns None (the
2407    // glue maps it to SpawnError::Busy; the init value is dropped normally).
2408    let any_state = graph.items.iter().any(|item| match item {
2409        Item::Node(n) => n.state.is_some(),
2410        Item::Pool(p) => p.state.is_some(),
2411        Item::Executor(_) => false,
2412    });
2413    if any_state {
2414        let try_box = &helpers.try_box;
2415        let alloc_alias = &helpers.alloc_alias;
2416        defs.push(quote! {
2417            extern crate alloc as #alloc_alias;
2418            /// Fallible boxing for `state:` clauses (generated by
2419            /// `supervisor_graph!`). Returns `None` when the global allocator
2420            /// is out of memory — surfaced by the spawn glue as
2421            /// `SpawnError::Busy`, retryable once heap frees up. The value is
2422            /// written to the heap allocation directly; note the INIT argument
2423            /// itself is materialized in this call's frame first (rustc may or
2424            /// may not elide the copy) — keep `state:` types reasonably sized
2425            /// or box internal bulk.
2426            #[doc(hidden)]
2427            fn #try_box<T>(init: T) -> ::core::option::Option<#alloc_alias::boxed::Box<T>> {
2428                let layout = ::core::alloc::Layout::new::<T>();
2429                if layout.size() == 0 {
2430                    // ZST: no allocation. Box<ZST> from a dangling well-aligned
2431                    // pointer is the documented representation; `init` is
2432                    // forgotten so T's drop (if any) runs exactly once, via the
2433                    // Box.
2434                    ::core::mem::forget(init);
2435                    // SAFETY: dangling NonNull is valid for a ZST Box.
2436                    return ::core::option::Option::Some(unsafe {
2437                        #alloc_alias::boxed::Box::from_raw(
2438                            ::core::ptr::NonNull::<T>::dangling().as_ptr(),
2439                        )
2440                    });
2441                }
2442                // SAFETY: `layout` has non-zero size. On success the pointer is
2443                // valid for writes of `T` and exclusively ours; `write`
2444                // initializes it; `from_raw` then owns an allocation made with
2445                // the global allocator and `T`'s layout — a normal `Box`.
2446                unsafe {
2447                    let p = #alloc_alias::alloc::alloc(layout) as *mut T;
2448                    if p.is_null() {
2449                        return ::core::option::Option::None; // `init` drops here
2450                    }
2451                    ::core::ptr::write(p, init);
2452                    ::core::option::Option::Some(#alloc_alias::boxed::Box::from_raw(p))
2453                }
2454            }
2455        });
2456    }
2457
2458    let any_local = graph
2459        .items
2460        .iter()
2461        .any(|item| item_resources(item).iter().any(|r| r.local.is_some()));
2462    if any_local {
2463        let local = helpers.local_slot.clone();
2464        // `Cell<Option<T>>` spelled through absolute paths (macro output must not
2465        // rely on the caller's prelude/imports); the mutex/signal types come from
2466        // the supervisor's `_export` shim so the consumer needs no direct
2467        // `embassy-sync` dependency.
2468        let cell = quote!(::core::cell::Cell<::core::option::Option<T>>);
2469        let raw = quote!(#cr::_export::CriticalSectionRawMutex);
2470        let signal = quote!(#cr::_export::Signal<#raw, ()>);
2471        defs.push(quote! {
2472            /// One-value handoff cell for a `local`-marked `resources:` entry
2473            /// (generated by `supervisor_graph!`). Protocol and fail-closed
2474            /// semantics of `embassy_supervisor::ResourceSlot`, minus its
2475            /// `T: Send` bound — for `!Send` driver handles on a single core.
2476            ///
2477            /// Contract (see the `unsafe impl Sync` below): every `provide` /
2478            /// `take` / `restore` of a given slot must happen on the SAME core.
2479            // `dead_code`/`missing_docs` in the consumer: the type is emitted
2480            // whenever a `local` entry is *declared*, even if every declaring
2481            // node is `#[cfg]`-compiled out of this build.
2482            #[allow(dead_code)]
2483            pub struct #local<T> {
2484                slot: #cr::_export::BlockingMutex<#raw, #cell>,
2485                filled: #signal,
2486            }
2487            // SAFETY: the payload is intentionally NOT `Send` — this assertion is
2488            // exactly the single-core contract: the value only ever moves between
2489            // executors/tasks of one core (interrupt-safe via the critical-section
2490            // mutex around every access), never across cores. The macro rejects
2491            // `local` + `executor:` so a slot cannot feed a `SendSpawner`-routed
2492            // node, and a multi-core application must not `provide`/`take` a given
2493            // slot from different cores.
2494            unsafe impl<T> ::core::marker::Sync for #local<T> {}
2495            #[allow(dead_code)]
2496            impl<T> #local<T> {
2497                /// An empty slot (`const` — it lives in the generated `static`s).
2498                pub const fn new() -> Self {
2499                    Self {
2500                        slot: #cr::_export::BlockingMutex::new(
2501                            ::core::cell::Cell::new(::core::option::Option::None),
2502                        ),
2503                        filled: #cr::_export::Signal::new(),
2504                    }
2505                }
2506                /// Move the resource in and wake the supervisor's pre-spawn wait.
2507                pub fn provide(&self, value: T) {
2508                    self.slot.lock(|c| c.set(::core::option::Option::Some(value)));
2509                    self.filled.signal(());
2510                }
2511                /// Take the resource out, leaving the slot empty (spawn glue).
2512                pub fn take(&self) -> ::core::option::Option<T> {
2513                    self.slot.lock(::core::cell::Cell::take)
2514                }
2515                /// Put the resource back for the next spawn (generated shell;
2516                /// not emitted for `consume` entries).
2517                pub fn restore(&self, value: T) {
2518                    self.provide(value);
2519                }
2520            }
2521            #[allow(dead_code)]
2522            impl<T: ::core::marker::Copy> #local<T> {
2523                /// Copy the value out WITHOUT emptying the slot — the `shared`
2524                /// kind's fan-out read (any number of consumers, slot stays
2525                /// filled). `T: Copy` only.
2526                pub fn get(&self) -> ::core::option::Option<T> {
2527                    self.slot.lock(|c| {
2528                        let v = c.take();
2529                        c.set(v);
2530                        v
2531                    })
2532                }
2533            }
2534            impl<T> ::core::default::Default for #local<T> {
2535                fn default() -> Self {
2536                    Self::new()
2537                }
2538            }
2539            impl<T> #cr::ResourceGate for #local<T> {
2540                fn is_filled(&self) -> bool {
2541                    // Peek without consuming: `Cell` has no `&T` access, so
2542                    // take-and-put-back under the same critical section.
2543                    self.slot.lock(|c| {
2544                        let v = c.take();
2545                        let filled = v.is_some();
2546                        c.set(v);
2547                        filled
2548                    })
2549                }
2550                fn filled_signal(&self) -> &#signal {
2551                    &self.filled
2552                }
2553            }
2554        });
2555    }
2556
2557    // Pre-pass: collect the declared `executor NAME;` slots so a node's
2558    // `executor:` reference can be validated regardless of declaration order.
2559    let helpers = HelperIdents::new(graph.name.as_ref());
2560    let executor_names: Vec<String> = graph
2561        .items
2562        .iter()
2563        .filter_map(|i| match i {
2564            Item::Executor(x) => Some(x.ident.to_string()),
2565            _ => None,
2566        })
2567        .collect();
2568    // Pool idents, known up front: a `ready`-marked dep naming a pool resolves
2569    // to the pool's floor member (`&POOL[0]`), and forward references are legal.
2570    let pool_names: std::collections::HashSet<String> = graph
2571        .items
2572        .iter()
2573        .filter_map(|i| match i {
2574            Item::Pool(p) => Some(p.ident.to_string()),
2575            _ => None,
2576        })
2577        .collect();
2578
2579    // Pre-pass: `resources:` slot names become `pub static`s at the declaration
2580    // site, so take-kind names must be unique across the whole graph — and no
2581    // resource may shadow an `executor NAME;` static. `shared` entries are the
2582    // deliberate exception: the SAME name on several items is one fan-out slot,
2583    // emitted once (below, with the union of the declaring sites' cfg
2584    // predicates so it exists whenever any consumer does) — provided every
2585    // re-declaration repeats the kinds + type verbatim. Caught here with
2586    // targeted messages instead of rustc's downstream duplicate-static E0428.
2587    struct SharedPlan<'a> {
2588        /// First declaration — supplies the emitted static's ident (span), type,
2589        /// and `local` flag.
2590        decl: &'a ResourceDecl,
2591        /// Kinds+type token string every re-declaration must match.
2592        sig: String,
2593        /// One entry per declaring site: `None` = unconditional (the slot is
2594        /// then unconditional too), `Some(pred)` = that site's combined
2595        /// item-level + entry-level cfg predicate.
2596        preds: Vec<Option<TokenStream2>>,
2597        /// Declaring node/pool names, for the generated doc comment.
2598        owners: Vec<String>,
2599    }
2600    let mut shared_plans: Vec<(String, SharedPlan)> = Vec::new();
2601    {
2602        let mut taken: HashSet<String> = HashSet::new();
2603        for item in &graph.items {
2604            let Some((owner, item_cfg)) = item_ident_cfg(item) else {
2605                continue;
2606            };
2607            let item_pred = cfg_predicate(item_cfg);
2608            for r in item_resources(item) {
2609                let key = r.ident.to_string();
2610                if executor_names.contains(&key) {
2611                    return Err(syn::Error::new_spanned(
2612                        &r.ident,
2613                        format!(
2614                            "resource name `{}` shadows an `executor {};` slot — \
2615                             both are statics at the declaration site",
2616                            r.ident, r.ident
2617                        ),
2618                    ));
2619                }
2620                // A site's presence predicate: the item's cfg AND the entry's.
2621                let pred = match (item_pred.clone(), cfg_predicate(&r.cfg)) {
2622                    (None, None) => None,
2623                    (Some(p), None) | (None, Some(p)) => Some(p),
2624                    (Some(a), Some(b)) => Some(quote!(all(#a, #b))),
2625                };
2626                if r.shared.is_some() {
2627                    if taken.contains(&key) {
2628                        return Err(syn::Error::new_spanned(
2629                            &r.ident,
2630                            format!(
2631                                "`{}` is already a take-kind resource elsewhere in \
2632                                 the graph — a name is either one exclusive slot or \
2633                                 one `shared` slot, not both",
2634                                r.ident
2635                            ),
2636                        ));
2637                    }
2638                    let sig = r.shared_signature();
2639                    match shared_plans.iter_mut().find(|(k, _)| *k == key) {
2640                        Some((_, plan)) => {
2641                            if plan.sig != sig {
2642                                return Err(syn::Error::new_spanned(
2643                                    &r.ident,
2644                                    format!(
2645                                        "shared resource `{}` re-declared with a \
2646                                         different shape: `{}` here vs `{}` on \
2647                                         `{}` — every declaration of a shared slot \
2648                                         must repeat the same kind markers and type",
2649                                        r.ident, sig, plan.sig, plan.owners[0]
2650                                    ),
2651                                ));
2652                            }
2653                            plan.preds.push(pred);
2654                            plan.owners.push(owner.to_string());
2655                        }
2656                        None => shared_plans.push((
2657                            key,
2658                            SharedPlan {
2659                                decl: r,
2660                                sig,
2661                                preds: vec![pred],
2662                                owners: vec![owner.to_string()],
2663                            },
2664                        )),
2665                    }
2666                } else {
2667                    if !taken.insert(key.clone()) || shared_plans.iter().any(|(k, _)| *k == key) {
2668                        return Err(syn::Error::new_spanned(
2669                            &r.ident,
2670                            format!(
2671                                "duplicate resource name `{}` — resource slots are \
2672                                 statics and must be unique across the graph (only \
2673                                 `shared` entries may repeat a name)",
2674                                r.ident
2675                            ),
2676                        ));
2677                    }
2678                }
2679            }
2680        }
2681    }
2682    // Emit each shared slot once. Presence: unconditional if ANY declaring site
2683    // is, else `#[cfg(any(<site preds>))]` — the slot exists whenever at least
2684    // one consumer does.
2685    for (_, plan) in &shared_plans {
2686        let res = &plan.decl.ident;
2687        let ty = &plan.decl.ty;
2688        let slot_ty = if plan.decl.local.is_some() {
2689            let local = &helpers.local_slot;
2690            quote!(#local<#ty>)
2691        } else {
2692            quote!(#cr::ResourceSlot<#ty>)
2693        };
2694        let cfg_attr = if plan.preds.iter().any(|p| p.is_none()) {
2695            quote!()
2696        } else {
2697            let preds = plan.preds.iter().flatten();
2698            quote!(#[cfg(any(#(#preds),*))])
2699        };
2700        let doc = format!(
2701            "Shared (fan-out) resource slot declared by `{}` (generated by \
2702             `supervisor_graph!`). `provide()` the `Copy` handle before \
2703             `Supervisor::start`; every consumer's glue copies it out with \
2704             `get()`, so the slot STAYS FILLED — re-`provide()` only to replace \
2705             the handle (e.g. after rebuilding the underlying driver).",
2706            plan.owners.join("`, `"),
2707        );
2708        defs.push(quote! {
2709            #cfg_attr
2710            #[doc = #doc]
2711            pub static #res: #slot_ty = <#slot_ty>::new();
2712        });
2713    }
2714
2715    for item in &graph.items {
2716        match item {
2717            Item::Node(n) => {
2718                if let Some(ex) = &n.executor
2719                    && !executor_names.contains(&ex.to_string())
2720                {
2721                    return Err(syn::Error::new_spanned(
2722                        ex,
2723                        format!(
2724                            "unknown executor `{ex}`; declare it in the graph with \
2725                             `executor {ex};` (declared: [{}])",
2726                            executor_names.join(", ")
2727                        ),
2728                    ));
2729                }
2730                // The index is the slot's position, taken *before* the push.
2731                // A redeclared name is a hard error here (not just the downstream
2732                // `duplicate definition of static`): deps resolve through this map,
2733                // so a silent overwrite would silently rewire earlier `deps:` edges.
2734                if names.insert(n.ident.to_string(), slots.len()).is_some() {
2735                    return Err(syn::Error::new_spanned(
2736                        &n.ident,
2737                        format!(
2738                            "duplicate node/pool name `{}`{}",
2739                            n.ident,
2740                            fragment_suffix(&n.fragment),
2741                        ),
2742                    ));
2743                }
2744                let (def, slot) = emit_node(n, &cr, &spawn_fn, &pool_names, &helpers)?;
2745                defs.push(def);
2746                slots.push(slot);
2747            }
2748            Item::Executor(x) => {
2749                let (cfg, ident) = (&x.cfg, &x.ident);
2750                // A runtime-filled SendSpawner slot: the app registers the
2751                // executor's spawner before `Supervisor::start`; nodes declared
2752                // `executor: NAME` spawn through it. Occupies no graph slot.
2753                defs.push(quote! {
2754                    #(#cfg)*
2755                    /// Spawner slot for the graph's `executor:`-annotated nodes
2756                    /// (generated by `supervisor_graph!`). Fill with
2757                    /// `SpawnerSlot::set` before `Supervisor::start`.
2758                    pub static #ident: #cr::SpawnerSlot = #cr::SpawnerSlot::new();
2759                });
2760            }
2761            Item::Pool(p) => {
2762                // Pools are only meaningful with the supervisor's `pool` feature (which
2763                // forwards to this crate). Without it, `Graph` has no `pools` field and
2764                // `ElasticPool` doesn't exist — so refuse a `pool` with a clear message
2765                // rather than emitting dangling references.
2766                if cfg!(feature = "pool") {
2767                    if let Some(ex) = &p.executor
2768                        && !executor_names.contains(&ex.to_string())
2769                    {
2770                        return Err(syn::Error::new_spanned(
2771                            ex,
2772                            format!(
2773                                "unknown executor `{ex}`; declare it in the graph with \
2774                                 `executor {ex};` (declared: [{}])",
2775                                executor_names.join(", ")
2776                            ),
2777                        ));
2778                    }
2779                    let (pool_defs, pool_entry, pool_slots) =
2780                        emit_pool(p, &cr, &spawn_fn, &pool_names, &helpers)?;
2781                    // A dep on the pool NAME resolves to the pool's floor member (member 0
2782                    // — the `min`-kept, always-started member): `deps: [POOL]` means "after
2783                    // the pool is up". `slots.len()` here is that member's slot index, taken
2784                    // *before* the extend below (pool_slots[0] lands at exactly this index).
2785                    // A redeclared name errors, same as the node arm.
2786                    if names.insert(p.ident.to_string(), slots.len()).is_some() {
2787                        return Err(syn::Error::new_spanned(
2788                            &p.ident,
2789                            format!(
2790                                "duplicate node/pool name `{}`{}",
2791                                p.ident,
2792                                fragment_suffix(&p.fragment),
2793                            ),
2794                        ));
2795                    }
2796                    defs.extend(pool_defs);
2797                    pool_entries.push(pool_entry);
2798                    slots.extend(pool_slots);
2799                } else {
2800                    return Err(syn::Error::new_spanned(
2801                        &p.ident,
2802                        "a `pool` requires enabling embassy-supervisor's `pool` feature",
2803                    ));
2804                }
2805            }
2806        }
2807    }
2808
2809    let m = slots.len();
2810    // Every graph index (dep entries, `topo_sort_const`'s queue/order) is a `u8`, so
2811    // more than 256 slots would silently truncate (`i as u8`) and corrupt the order.
2812    // 256 slots means max index 255 and max per-node dep count 255 — both fit exactly.
2813    if m > 256 {
2814        return Err(syn::Error::new(
2815            proc_macro2::Span::call_site(),
2816            format!(
2817                "supervisor_graph!: {m} node slots declared, but at most 256 are supported \
2818                 (including pool members) — graph indices are `u8`"
2819            ),
2820        ));
2821    }
2822    let (all_entries, deps_entries) = slot_tables(&slots, &names)?;
2823
2824    // `Graph.pools` is `#[cfg(feature = "pool")]`; emit that field iff this macro was
2825    // built with pool support (forwarded from the supervisor's `pool` feature).
2826    let pools_field = if cfg!(feature = "pool") {
2827        quote!( pools: &[ #(#pool_entries),* ], )
2828    } else {
2829        quote!()
2830    };
2831
2832    // embassy-executor's trace hooks (declared `unsafe extern "Rust"` in the
2833    // executor), defined once here at the graph declaration site — the supervisor
2834    // crate is `forbid(unsafe_code)` and cannot carry `#[unsafe(no_mangle)]` items.
2835    // They forward to the supervisor's `trace` recorders. `task_new` and
2836    // `task_ready_begin` carry nothing the recorders need (the id→node mapping
2837    // comes from the spawn glue above), so they are no-ops. Exactly one definition
2838    // of each may exist per binary: enable `trace-hooks` OR write your own set.
2839    // Requires an edition-2024 consumer (`#[unsafe(no_mangle)]` syntax).
2840    // Named graphs never emit the hook symbols: `no_mangle` items exist once
2841    // per binary, and a multi-graph binary's PRIMARY (unnamed) graph carries
2842    // them; the recorders resolve every registered graph's nodes regardless.
2843    let trace_hooks = if cfg!(feature = "trace-hooks") && graph.name.is_none() {
2844        quote! {
2845            #[unsafe(no_mangle)]
2846            fn _embassy_trace_poll_start(executor_id: u32) {
2847                #cr::trace::on_poll_start(executor_id);
2848            }
2849            #[unsafe(no_mangle)]
2850            fn _embassy_trace_task_new(_executor_id: u32, _task_id: u32) {}
2851            #[unsafe(no_mangle)]
2852            fn _embassy_trace_task_end(executor_id: u32, task_id: u32) {
2853                #cr::trace::on_task_end(executor_id, task_id);
2854            }
2855            #[unsafe(no_mangle)]
2856            fn _embassy_trace_task_exec_begin(executor_id: u32, task_id: u32) {
2857                #cr::trace::on_task_exec_begin(executor_id, task_id);
2858            }
2859            #[unsafe(no_mangle)]
2860            fn _embassy_trace_task_exec_end(executor_id: u32, task_id: u32) {
2861                #cr::trace::on_task_exec_end(executor_id, task_id);
2862            }
2863            #[unsafe(no_mangle)]
2864            fn _embassy_trace_task_ready_begin(_executor_id: u32, _task_id: u32) {}
2865            #[unsafe(no_mangle)]
2866            fn _embassy_trace_executor_idle(executor_id: u32) {
2867                #cr::trace::on_executor_idle(executor_id);
2868            }
2869        }
2870    } else {
2871        quote!()
2872    };
2873
2874    // `name: X;` renames the emitted graph static and suffixes the private
2875    // backing tables, so several graphs coexist — even in one module. Unnamed
2876    // keeps the historical `GRAPH`/`NODES`/`DEPS` idents.
2877    let graph_ident = graph
2878        .name
2879        .clone()
2880        .unwrap_or_else(|| Ident::new("GRAPH", proc_macro2::Span::call_site()));
2881    let (nodes_ident, deps_ident) = match &graph.name {
2882        Some(n) => (
2883            format_ident!("__SV_NODES_{}", n),
2884            format_ident!("__SV_DEPS_{}", n),
2885        ),
2886        None => (
2887            Ident::new("NODES", proc_macro2::Span::call_site()),
2888            Ident::new("DEPS", proc_macro2::Span::call_site()),
2889        ),
2890    };
2891    Ok(quote! {
2892        #(#defs)*
2893
2894        // Private backing tables — the application uses the graph static. The
2895        // topological order and pools are inlined into its literal below; the
2896        // node count is `.nodes.len()`.
2897        static #nodes_ident: [::core::option::Option<&'static #cr::TaskNode>; #m] = [ #(#all_entries),* ];
2898        const #deps_ident: [&'static [u8]; #m] = [ #(#deps_entries),* ];
2899
2900        /// The compile-time task graph — node slots, dependency table, topological order,
2901        /// and (with the `pool` feature) the elastic pools. Pass to `Supervisor::new`.
2902        pub static #graph_ident: #cr::Graph<#m> = #cr::Graph {
2903            nodes: &#nodes_ident,
2904            deps: &#deps_ident,
2905            order: #cr::topo_sort_const(&#deps_ident),
2906            #pools_field
2907        };
2908
2909        #trace_hooks
2910    })
2911}
2912
2913/// Declare a supervised task graph; see the crate docs for the surface syntax.
2914#[proc_macro]
2915pub fn supervisor_graph(input: TokenStream) -> TokenStream {
2916    let graph = syn::parse_macro_input!(input as GraphSpec);
2917    expand(graph)
2918        .unwrap_or_else(syn::Error::into_compile_error)
2919        .into()
2920}
2921
2922/// Declare a **graph fragment**: `supervisor_fragment! { name: NET_FRAG; <items> }`
2923/// emits a `#[macro_export] macro_rules! NET_FRAG` relay that forwards the items
2924/// (verbatim, wrapped in `@fragment`/`@endfragment` attribution markers) into the
2925/// single `supervisor_graph!` expansion a `compose_graph!` call site assembles —
2926/// so every whole-graph compile-time pass (name map, u8 slot indices, topo order,
2927/// shared-slot dedup, the 256 cap) still sees ALL items, across crates.
2928///
2929/// Item syntax is validated here, with fragment-site spans; dep/executor NAMES
2930/// resolve at the compose site (cross-fragment references are the point).
2931/// Fragment authors reference their own workers/types via `$crate::…` (which
2932/// hygienically resolves to the fragment's crate at every compose site) or a
2933/// fully-qualified `::crate_name::…` path; a bare `crate::…` would resolve at
2934/// the COMPOSE crate and is a bug. No `$` other than `$crate` is permitted.
2935/// `#[cfg(...)]` inside a fragment is evaluated against the COMPOSE crate's
2936/// features (the tokens expand there) — export differently-named fragment
2937/// variants instead of feature-gating items.
2938#[proc_macro]
2939pub fn supervisor_fragment(input: TokenStream) -> TokenStream {
2940    fragment_expand(input.into())
2941        .unwrap_or_else(syn::Error::into_compile_error)
2942        .into()
2943}
2944
2945fn fragment_expand(input: TokenStream2) -> SynResult<TokenStream2> {
2946    struct FragmentSpec {
2947        name: Ident,
2948        items: TokenStream2,
2949    }
2950    impl Parse for FragmentSpec {
2951        fn parse(input: ParseStream) -> SynResult<Self> {
2952            input.parse::<kw::name>()?;
2953            input.parse::<Token![:]>()?;
2954            let name: Ident = input.parse()?;
2955            input.parse::<Token![;]>()?;
2956            let items: TokenStream2 = input.parse()?;
2957            Ok(FragmentSpec { name, items })
2958        }
2959    }
2960    let spec: FragmentSpec = syn::parse2(input)?;
2961    let name = &spec.name;
2962
2963    // Only `$crate` may appear (it resolves to the fragment's own crate in the
2964    // emitted macro_rules RHS); any other `$` would be interpreted as a
2965    // metavariable by the relay and mangle the forwarded tokens.
2966    validate_dollars(spec.items.clone())?;
2967
2968    // Syntax validation with fragment-site spans: parse the items as a graph,
2969    // with `$crate` substituted by a placeholder ident so paths parse. Name
2970    // RESOLUTION (deps, executors) is deliberately skipped — targets may live
2971    // in other fragments and resolve at the compose site.
2972    let substituted = substitute_dollar_crate(spec.items.clone());
2973    syn::parse2::<GraphSpec>(substituted)?;
2974
2975    let items = &spec.items;
2976    let dollar = proc_macro2::Punct::new('$', proc_macro2::Spacing::Alone);
2977    let doc = format!(
2978        "A `supervisor_fragment!` relay (generated). Use from a compose site:\n\
2979         `embassy_supervisor::compose_graph! {{ fragments: [{name}], graph: {{ .. }} }}`\n\
2980         Not for direct invocation."
2981    );
2982    Ok(quote! {
2983        #[doc = #doc]
2984        #[macro_export]
2985        macro_rules! #name {
2986            (@emit #dollar cb:path, [#dollar(#dollar rest:tt)*], {#dollar(#dollar acc:tt)*}, {#dollar(#dollar g:tt)*}) => {
2987                #dollar cb! { @next [#dollar(#dollar rest)*],
2988                    {#dollar(#dollar acc)* @fragment #name; #items @endfragment;},
2989                    {#dollar(#dollar g)*} }
2990            };
2991        }
2992    })
2993}
2994
2995/// Reject any `$` not immediately followed by `crate`, recursively through
2996/// groups. `$crate` is the one dollar token with meaning in the emitted
2997/// macro_rules RHS (fragment-crate paths); anything else would be read as a
2998/// metavariable.
2999fn validate_dollars(stream: TokenStream2) -> SynResult<()> {
3000    use proc_macro2::TokenTree;
3001    let mut iter = stream.into_iter().peekable();
3002    while let Some(tt) = iter.next() {
3003        match tt {
3004            TokenTree::Group(g) => validate_dollars(g.stream())?,
3005            TokenTree::Punct(p) if p.as_char() == '$' => match iter.peek() {
3006                Some(TokenTree::Ident(i)) if i == "crate" => {}
3007                _ => {
3008                    return Err(syn::Error::new(
3009                        p.span(),
3010                        "only `$crate` is permitted in a fragment — any other `$` \
3011                         would be read as a metavariable by the relay macro",
3012                    ));
3013                }
3014            },
3015            _ => {}
3016        }
3017    }
3018    Ok(())
3019}
3020
3021/// Replace every `$crate` pair with a placeholder ident so the items parse as a
3022/// `GraphSpec` for validation. The ORIGINAL tokens (with `$crate` intact) are
3023/// what get forwarded.
3024fn substitute_dollar_crate(stream: TokenStream2) -> TokenStream2 {
3025    use proc_macro2::{TokenStream as TS, TokenTree};
3026    let mut out = TS::new();
3027    let mut iter = stream.into_iter().peekable();
3028    while let Some(tt) = iter.next() {
3029        match tt {
3030            TokenTree::Group(g) => {
3031                let inner = substitute_dollar_crate(g.stream());
3032                let mut ng = proc_macro2::Group::new(g.delimiter(), inner);
3033                ng.set_span(g.span());
3034                out.extend([TokenTree::Group(ng)]);
3035            }
3036            TokenTree::Punct(p) if p.as_char() == '$' => {
3037                if let Some(TokenTree::Ident(i)) = iter.peek()
3038                    && i == "crate"
3039                {
3040                    let span = iter.next().map(|t| t.span()).unwrap_or_else(|| p.span());
3041                    out.extend([TokenTree::Ident(Ident::new("__sv_fragment_crate", span))]);
3042                } else {
3043                    out.extend([TokenTree::Punct(p)]);
3044                }
3045            }
3046            other => out.extend([other]),
3047        }
3048    }
3049    out
3050}
3051
3052#[cfg(test)]
3053mod tests {
3054    use super::*;
3055
3056    /// The `ready` dep marker's feature rejection. Not UI-testable: the
3057    /// dev-dependency supervisor carries `readiness` for the trybuild pass
3058    /// cases, and the scratch project's feature unification re-enables this
3059    /// crate's feature through the supervisor's weak forward — so the
3060    /// no-feature path can only be exercised here, on the parser directly.
3061    #[test]
3062    fn ready_marker_requires_feature() {
3063        let res = syn::parse_str::<GraphSpec>(
3064            "node NET = Terminate, deps: [];\n\
3065             node HTTP = Terminate, deps: [NET ready];",
3066        );
3067        if cfg!(feature = "readiness") {
3068            assert!(res.is_ok(), "marker accepted with the feature");
3069        } else {
3070            match res {
3071                Ok(_) => panic!("marker accepted without the feature"),
3072                Err(err) => assert!(
3073                    err.to_string().contains("requires the `readiness` feature"),
3074                    "unexpected error: {err}"
3075                ),
3076            }
3077        }
3078    }
3079
3080    /// A stray ident after a dep name is rejected as an unknown marker in both
3081    /// feature states.
3082    #[test]
3083    fn unknown_dep_marker_rejected() {
3084        match syn::parse_str::<GraphSpec>("node A = Terminate, deps: [B rdy];") {
3085            Ok(_) => panic!("unknown marker accepted"),
3086            Err(err) => assert!(err.to_string().contains("`ready` marker"), "got: {err}"),
3087        }
3088    }
3089}