phoxal_macros/lib.rs
1//! Proc-macros for the phoxal framework.
2//!
3//! Three macro families make up the authoring surface:
4//!
5//! - [`phoxal_api_tree!`] - declares concrete API-revision modules
6//! (`phoxal_api::v0_1`, …), their revision-local body types, the
7//! `ContractBody`/`ApiVersion` impls, and the api-local topic builders.
8//! - [`derive@Api`] / [`derive@Config`] - read an `Api` handle struct's typed
9//! fields (the role-gated publishers / `Subscriber<T>` / `Latest<T>` / `Querier<Req,
10//! Resp>` / `Server<Req, Resp>`) and a `Config` struct respectively, and emit
11//! the static metadata (`ParticipantApi`/`ParticipantConfig`) the runner
12//! consumes.
13//! - [`macro@service`] / [`macro@driver`] / [`macro@simulator`] / [`macro@tool`] -
14//! link a participant state struct to its `Config`/`Api` types and record
15//! its identity (`Participant`).
16//! - [`macro@behavior`] - the bare `#[phoxal::behavior]` attribute on the inherent
17//! impl; it reads `#[setup]`/`#[step(hz = N)]`/`#[shutdown]` plus the query-side
18//! `#[server]`/`#[server_snapshot]`/`#[snapshot]` and emits the lifecycle and
19//! server dispatch (`ParticipantLifecycle`).
20//!
21//! The struct/impl macros are paired: `#[phoxal::service|driver|simulator|tool]`
22//! links the participant to its `Config`/`Api` types and records the artifact
23//! kind, while `#[phoxal::behavior]` adds the lifecycle methods and the
24//! server-side contracts, threading `Self::Api` through every callback (D3).
25//!
26//! The participant authoring macros (`macro@service` / `macro@driver` /
27//! `macro@tool` / `macro@simulator` / `macro@behavior`) reference the framework
28//! through `::phoxal::…`; the engine crate makes that path resolve to itself with
29//! `extern crate self as phoxal;`. The
30//! `phoxal_api_tree!` output instead targets the bus ABI floor directly as
31//! `::phoxal_bus`, since it is invoked in the `phoxal-api` crate, which does not
32//! depend on the engine.
33
34mod api_tree;
35mod authoring;
36mod behavior;
37mod util;
38
39use proc_macro::TokenStream;
40
41/// Declare a versioned API tree of version-local wire bodies + topics.
42///
43/// One invocation owns one or more `version vM_N { … }` blocks and exactly one
44/// final `latest vM_N;` declaration. A child may `extends` one earlier parent;
45/// inherited definitions are fully materialized with the child's concrete
46/// identity. Additions are direct and same-path changes require explicit
47/// `replace` or `remove`. The generated tree references the bus ABI floor as
48/// `::phoxal_bus`.
49///
50/// # Node grammar
51///
52/// A version body is a tree of **nodes**. A node is either static (`name { … }`)
53/// or dynamic (`name(var) { … }`, binding exactly one variable), and may nest to
54/// any depth. Inside a node block, in any order:
55///
56/// - `struct …` / `enum …` - a version-local wire body. Macro-declared structs
57/// get public fields; every body gets the standard derive set (`Clone`,
58/// `Debug`, `PartialEq`, `serde::Serialize`/`Deserialize`).
59/// - `topic <leaf>: command <Body>;` - a pub/sub topic the owning service
60/// subscribes (a control input).
61/// - `topic <leaf>: state <Body>;` - a pub/sub topic the owning service publishes
62/// (telemetry/output). Same wire shape as `command`, but the side-branded
63/// builders give it the inverse brand (see *Generated topic builders* below).
64/// - `topic <leaf>: query <Req> => <Resp>;` - a request/response topic.
65/// - a child node (`name { … }` / `name(var) { … }`).
66///
67/// Doc-comments and attributes attach to the next `struct`/`enum`; `topic`
68/// declarations and child nodes take none.
69///
70/// # What each topic derives from its node path
71///
72/// A topic carries no per-topic params; its identity is derived from the path of
73/// nodes enclosing it:
74///
75/// - **`TOPIC`** (the wire key) - the version, then the `/`-joined node
76/// segments plus the leaf, where a static node contributes `name` and a
77/// dynamic node contributes `name/{var}` (e.g.
78/// `v0.1/component/{instance}/motor/{capability}/command`). Folding the
79/// version into the key (D1) is what makes two differently-versioned
80/// contracts physically distinct Zenoh keys - there is no separate
81/// `FAMILY`/`SCHEMA_ID` axis.
82/// - **body type path** - `phoxal_api::vM_N::<node>::…::<Body>`; variables never
83/// appear in the module path.
84///
85/// A topic is dynamic when its node path contains at least one `(var)` node, and
86/// static otherwise.
87///
88/// # Generated topic builders
89///
90/// Each version also gets an api-local `topic` module emitted with BOTH side trees
91/// (L1, plan #00). `topic::client()` returns a `Root` for the PUBLIC **client** side;
92/// `topic::owner()` returns a `Root` for the OWNER side. Both
93/// have a method per node that walks the identical
94/// tree (a dynamic node's method takes its variable as `impl Display`) and a leaf
95/// method that returns a typed `bus::Topic<Kind>` with the key formatted from the
96/// carried variables. The leaf brand is side-specific: on the client side a
97/// `command` leaf is `Publish<Body>`, a `state` leaf is `Subscribe<Body>`, and a
98/// `query` leaf is `AskQuery<Req, Resp>`; on the owner side those flip to
99/// `Subscribe<Body>` / `Publish<Body>` / `ServeQuery<Req, Resp>`.
100#[proc_macro]
101pub fn phoxal_api_tree(input: TokenStream) -> TokenStream {
102 api_tree::expand(input.into())
103 .unwrap_or_else(syn::Error::into_compile_error)
104 .into()
105}
106
107/// The bare `#[phoxal::behavior]` attribute on a participant's inherent impl.
108///
109/// Takes no arguments (configure the participant on the struct via
110/// `#[phoxal::service]`, `#[phoxal::driver]`, `#[phoxal::tool]`, or
111/// `#[phoxal::simulator]`, and its bus-facing handles via `#[derive(phoxal::Api)]`
112/// on a companion `Api` struct). Reads the lifecycle/server helper attributes on
113/// the impl's methods, emits a `ParticipantLifecycle` impl that the runner
114/// drives, and re-emits the original methods verbatim with the helper
115/// attributes stripped. A method may carry at most one helper attribute.
116///
117/// # Lifecycle / server attributes and their required signatures
118///
119/// - `#[setup]` - **mandatory, exactly once**. An `async` associated function
120/// named `setup` taking `ctx: &mut SetupContext<Self>` and, optionally, the
121/// participant config; returns `Result<(Self, Self::Api)>`.
122/// - `#[step(hz = N)]` - at most once. `async fn (&mut self, api: &mut Self::Api,
123/// step: StepContext) -> Result<()>`; the scheduled control loop runs at the
124/// positive, finite frequency `N`.
125/// - `#[shutdown]` - at most once. An `async` method named `shutdown` taking
126/// `&mut self`, `api: &mut Self::Api`, and, optionally, `ctx: ShutdownContext`;
127/// returns `Result<()>`.
128/// - `#[server(api = field)]` - an exclusive query server: `async fn (&mut self,
129/// api: &mut Self::Api, request: Req) -> ServerResult<Resp>`. Serialized with
130/// `#[step]`.
131/// - `#[server_snapshot(api = field)]` - a concurrent, read-only query server: an
132/// `async` associated function taking `state: Snapshot<State>`, `api:
133/// &Self::Api`, and `request: Req`, returning `ServerResult<Resp>`. Requires a
134/// `#[snapshot]` provider.
135/// - `#[snapshot]` - at most once. The committed-snapshot provider: a synchronous
136/// `fn (&self) -> State` returning the committed state.
137///
138/// For `#[server]`/`#[server_snapshot]` the `api = field` names the `Api` struct's
139/// `Server<Req, Resp>` field being implemented; both request and response bodies
140/// must be `ContractBody` (checked at compile time; a query only ever reaches the
141/// handler on its own version-qualified topic key, D1, so there is no
142/// separate decode-time identity check left).
143///
144/// # A `tool` is a thin runner
145///
146/// A `#[phoxal::tool]` participant may use `#[setup]` and `#[shutdown]` (plus
147/// `#[snapshot]`, which is inert without a server), but `#[step]`,
148/// `#[server(...)]`, and `#[server_snapshot(...)]` are the typed-graph surface and
149/// are a compile error on a tool: tools are privileged, out-of-band, thin
150/// raw-bus runners (lifecycle + `participant_id` + `ctx.robot()` +
151/// `phoxal::raw`), not checked participants. A tool that needs a recurring loop
152/// spawns and owns its own task from `#[setup]`.
153#[proc_macro_attribute]
154pub fn behavior(attr: TokenStream, item: TokenStream) -> TokenStream {
155 behavior::expand(attr.into(), item.into())
156 .unwrap_or_else(syn::Error::into_compile_error)
157 .into()
158}
159
160/// Derive the bus-facing contract surface from an `Api` handle struct. See
161/// `phoxal::participant::api` for the trait shape.
162///
163/// Scans fields by canonical syntactic form: a role-gated publisher / `Subscriber<T>` /
164/// `Latest<T>` are pub/sub handles, `Querier<Req, Resp>` is the asking side of a
165/// query, and `Server<Req, Resp>` is a served query contract - no live
166/// connection, declared for `#[phoxal::behavior]`'s `#[server(api = …)]` /
167/// `#[server_snapshot(api = …)]` to implement. A `Vec`/`BTreeMap`/`HashMap` of a
168/// handle carries the inner handle's declaration. Official participants name
169/// the train-selected complete revision through `phoxal::api`.
170#[proc_macro_derive(Api)]
171pub fn derive_api(input: TokenStream) -> TokenStream {
172 authoring::expand_api(input.into())
173 .unwrap_or_else(syn::Error::into_compile_error)
174 .into()
175}
176
177/// Derive a compile-time Draft 2020-12 JSON Schema from the same supported
178/// `#[serde(...)]` attributes used by `Deserialize`: `rename`, `rename_all`,
179/// `default`, and `deny_unknown_fields`. Unsupported Serde attributes are a
180/// compile error rather than an approximate schema.
181#[proc_macro_derive(Config, attributes(serde))]
182pub fn derive_config(input: TokenStream) -> TokenStream {
183 authoring::expand_config(input.into())
184 .unwrap_or_else(syn::Error::into_compile_error)
185 .into()
186}
187
188/// Link a participant state struct to its `Config`/`Api` types as a checked
189/// service. Defaults to the local `Config`/`Api` type names, and to the
190/// crate's `CARGO_PKG_NAME` (a leading `phoxal-<kind>-` stripped when
191/// present) for `id`; override any of them with
192/// `#[phoxal::service(id = "…", config = Type, api = Type)]`.
193///
194/// An explicit `id` is still required whenever a crate defines more than one
195/// participant - they cannot all default to the one package name - and
196/// remains available any time the package name isn't the id you want.
197///
198/// For user runtimes, `Config` is the user-authored `robot.yaml` surface. A
199/// framework runtime may use this same slot for a CLI-synthesized launch
200/// payload (for example a cross-robot staging product); ordinary framework
201/// knobs belong in the robot model received through `ctx.robot()`.
202#[proc_macro_attribute]
203pub fn service(attr: TokenStream, item: TokenStream) -> TokenStream {
204 authoring::expand_participant(
205 attr.into(),
206 item.into(),
207 authoring::ParticipantKind::Service,
208 )
209 .unwrap_or_else(syn::Error::into_compile_error)
210 .into()
211}
212
213/// The driver-shaped counterpart to [`service`].
214#[proc_macro_attribute]
215pub fn driver(attr: TokenStream, item: TokenStream) -> TokenStream {
216 authoring::expand_participant(attr.into(), item.into(), authoring::ParticipantKind::Driver)
217 .unwrap_or_else(syn::Error::into_compile_error)
218 .into()
219}
220
221/// The simulator-shaped counterpart to [`service`].
222#[proc_macro_attribute]
223pub fn simulator(attr: TokenStream, item: TokenStream) -> TokenStream {
224 authoring::expand_participant(
225 attr.into(),
226 item.into(),
227 authoring::ParticipantKind::Simulator,
228 )
229 .unwrap_or_else(syn::Error::into_compile_error)
230 .into()
231}
232
233/// The tool-shaped counterpart to [`service`]. `Api` and `Config` default to
234/// `()` - tools stay raw-bus only (decided 2026-07-09), and a configless tool
235/// can launch without `PHOXAL_CONFIG`. An explicit `config = Type` remains
236/// required at launch unless that type itself accepts `null` (for example,
237/// `Option<T>`).
238#[proc_macro_attribute]
239pub fn tool(attr: TokenStream, item: TokenStream) -> TokenStream {
240 authoring::expand_participant(attr.into(), item.into(), authoring::ParticipantKind::Tool)
241 .unwrap_or_else(syn::Error::into_compile_error)
242 .into()
243}