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