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