Skip to main content

embassy_supervisor_macros/
lib.rs

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