embassy-supervisor-syntax 0.4.0

Parser for the embassy-supervisor graph DSL, shared by its proc-macro and its tooling
Documentation
# embassy-supervisor-syntax

The parser and AST for the `supervisor_graph!` DSL of
[`embassy-supervisor`](https://docs.rs/embassy-supervisor): turn graph
declarations into [`GraphSpec`] values and reject malformed ones with
span-attached errors.

## Not a stable API

The AST is an internal contract with `embassy-supervisor-macros`, which pins
this crate by **exact version**. Fields and variants change whenever the graph
syntax does. Depend on it directly only if you are willing to track it.

## What parsing does not decide

**Feature gating.** This crate carries no features and accepts the grammar in
full: `ready`/`bound` dep markers, `observed`/`beat`/`via`, `discover`,
`dataflow:`, `local`, `state:`, `beat_timeout:`, `ready_on_write` always parse.
Whether a build permits a construct is policy, applied by the caller:
`embassy-supervisor-macros` rejects gated constructs in its `gate` pass. Every
gated construct keeps the `Ident` or literal its rejection points at, so those
errors carry the span the author wrote.

**Name resolution.** Unknown deps, duplicate names, executor slot references,
and the 256-node-slot cap (pool members included) are properties of a whole
graph, checked by `embassy-supervisor-macros` while expanding. A
`supervisor_fragment!` legitimately names nodes it does not contain; they
resolve at the compose site, so parsing it in isolation must succeed.

## What parsing does check

Shape, which is part of the grammar rather than any build's policy. All errors
are span-attached:

- Empty clause lists (`reads:`, `writes:`, `resources:`, `dataflow:`,
  `provides:` must each declare at least one entry; omit the clause instead).
- A repeated signal path, or one path declared both bare and indexed (`&ARR`
  and `&ARR[0]` share an address, so nothing downstream could tell them apart).
- Marker shape: `bound` without `ready`, `beat`/`via` without `observed`, a
  bare `beat` entry, `beat` on a `reads:` entry.
- Numeric ranges: `slot_timeout:`/`ack_timeout:`/`beat_timeout:`/`pool_size:`
  at least 1, `beat_window:` in 1..=255.
- Clause combinations: `task:` vs `spawn:`, clauses that require `task:`
  (`pool_size:`, `resources:`, `state:`, `exit:`, `cancel`), `cancel` with
  `Mode::Pause`, `exit:` on a `pool`,
  `ready_on_write` without `beat_timeout:` and an `observed beat` write.
- `default executor` shape: one per graph, never `#[cfg]`-gated, never inside
  `@fragment` markers (it resolves across them at the compose site). Whether a
  `local` slot's declarations share one executor is checked by
  `embassy-supervisor-macros`, not here.

## Usage

```rust
use embassy_supervisor_syntax::{GraphSpec, Item};

let spec: GraphSpec = syn::parse_str(
    "node NET = Terminate, task: net_task;\n\
     node HTTP = Terminate, deps: [NET ready], task: http_task;",
)?;

for item in &spec.items {
    if let Item::Node(n) = item {
        println!("{} depends on {} node(s)", n.ident, n.deps.len());
    }
}
# Ok::<(), syn::Error>(())
```

## Fragment helpers

`supervisor_fragment!` items reach a compose site as tokens. Two helpers move
them between spellings:

- [`normalize_fragment_crate`] rewrites every bare `crate` to `$crate`, so a
  fragment forwarded into another crate still names its own items.
- [`substitute_dollar_crate`] replaces every `$crate` with a caller-chosen
  token stream, so fragment items parse as a [`GraphSpec`] the macro
  substitutes a placeholder; a source reader substitutes the compose crate.