Skip to main content

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@Config`] - derives the config schema embedded in participant
9//!   metadata.
10//! - [`macro@service`] / [`macro@driver`] / [`macro@simulator`] / [`macro@tool`] -
11//!   declare a unit marker's `Config`/`State`/`Api` types and identity
12//!   (`ParticipantSpec`).
13//! - [`macro@step`] - records cadence on the ordinary `Participant::step`
14//!   override. Setup, reset, shutdown, and query handlers are plain Rust.
15//!
16//! The participant authoring macros (`macro@service` / `macro@driver` /
17//! `macro@tool` / `macro@simulator` / `macro@step`) reference the framework
18//! through `::phoxal::…`; the engine crate makes that path resolve to itself with
19//! `extern crate self as phoxal;`. The
20//! `phoxal_api_tree!` output instead targets the bus ABI floor directly as
21//! `::phoxal_bus`, since it is invoked in the `phoxal-api` crate, which does not
22//! depend on the engine.
23
24mod api_tree;
25mod authoring;
26mod step;
27mod util;
28
29use proc_macro::TokenStream;
30
31/// Declare a versioned API tree of version-local wire bodies + topics.
32///
33/// One invocation owns one or more `version vM_N { … }` blocks and exactly one
34/// final `latest vM_N;` declaration. A child may `extends` one earlier parent;
35/// inherited definitions are fully materialized with the child's concrete
36/// identity. Additions are direct and same-path changes require explicit
37/// `replace` or `remove`. The generated tree references the bus ABI floor as
38/// `::phoxal_bus`.
39///
40/// # Node grammar
41///
42/// A version body is a tree of **nodes**. A node is either static (`name { … }`)
43/// or dynamic (`name(var) { … }`, binding exactly one variable), and may nest to
44/// any depth. Inside a node block, in any order:
45///
46/// - `struct …` / `enum …` - a version-local wire body. Macro-declared structs
47///   get public fields; every body gets the standard derive set (`Clone`,
48///   `Debug`, `PartialEq`, `serde::Serialize`/`Deserialize`).
49/// - `topic <leaf>: command <Body>;` - a pub/sub topic the owning service
50///   subscribes (a control input).
51/// - `topic <leaf>: state <Body>;` - a pub/sub topic the owning service publishes
52///   (telemetry/output). Same wire shape as `command`, but the side-branded
53///   builders give it the inverse brand (see *Generated topic builders* below).
54/// - `topic <leaf>: query <Req> => <Resp>;` - a request/response topic.
55/// - a child node (`name { … }` / `name(var) { … }`).
56///
57/// Doc-comments and attributes attach to the next `struct`/`enum`; `topic`
58/// declarations and child nodes take none.
59///
60/// # What each topic derives from its node path
61///
62/// A topic carries no per-topic params; its identity is derived from the path of
63/// nodes enclosing it:
64///
65/// - **`TOPIC`** (the wire key) - the version, then the `/`-joined node
66///   segments plus the leaf, where a static node contributes `name` and a
67///   dynamic node contributes `name/{var}` (e.g.
68///   `v0.1/component/{instance}/motor/{capability}/command`). Folding the
69///   version into the key (D1) makes differently-versioned contracts
70///   physically distinct Zenoh keys.
71/// - **body type path** - `phoxal_api::vM_N::<node>::…::<Body>`; variables never
72///   appear in the module path.
73///
74/// A topic is dynamic when its node path contains at least one `(var)` node, and
75/// static otherwise.
76///
77/// # Generated topic builders
78///
79/// Each version also gets an api-local `topic` module emitted with BOTH side trees
80/// (L1, plan #00). `topic::client()` returns a `Root` for the PUBLIC **client** side;
81/// `topic::owner()` returns a `Root` for the OWNER side. Both
82/// have a method per node that walks the identical
83/// tree (a dynamic node's method takes its variable as `impl Display`) and a leaf
84/// method that returns a typed `bus::Topic<Kind>` with the key formatted from the
85/// carried variables. The leaf brand is side-specific: on the client side a
86/// `command` leaf is `Publish<Body>`, a `state` leaf is `Subscribe<Body>`, and a
87/// `query` leaf is `AskQuery<Req, Resp>`; on the owner side those flip to
88/// `Subscribe<Body>` / `Publish<Body>` / `ServeQuery<Req, Resp>`.
89#[proc_macro]
90pub fn phoxal_api_tree(input: TokenStream) -> TokenStream {
91    api_tree::expand(input.into())
92        .unwrap_or_else(syn::Error::into_compile_error)
93        .into()
94}
95
96/// Attach a positive, finite frequency to the ordinary
97/// [`Participant::step`](https://docs.rs/phoxal) override.
98#[proc_macro_attribute]
99pub fn step(attr: TokenStream, item: TokenStream) -> TokenStream {
100    step::expand(attr.into(), item.into())
101        .unwrap_or_else(syn::Error::into_compile_error)
102        .into()
103}
104
105/// Derive a compile-time Draft 2020-12 JSON Schema from the same supported
106/// `#[serde(...)]` attributes used by `Deserialize`: `rename`, `rename_all`,
107/// `default`, and `deny_unknown_fields`. Unsupported Serde attributes are a
108/// compile error rather than an approximate schema.
109#[proc_macro_derive(Config, attributes(serde))]
110pub fn derive_config(input: TokenStream) -> TokenStream {
111    authoring::expand_config(input.into())
112        .unwrap_or_else(syn::Error::into_compile_error)
113        .into()
114}
115
116/// Declare a service marker's `Config`/`State`/`Api` types. Each omitted type
117/// defaults to `()`; identity defaults from `CARGO_PKG_NAME`.
118///
119/// An explicit `id` is still required whenever a crate defines more than one
120/// participant - they cannot all default to the one package name - and
121/// remains available any time the package name isn't the id you want.
122///
123/// For user runtimes, `Config` is the user-authored `robot.yaml` surface. A
124/// framework runtime may use this same slot for a CLI-synthesized launch
125/// payload (for example a cross-robot staging product); ordinary framework
126/// knobs belong in the robot model received through `ctx.robot()`.
127#[proc_macro_attribute]
128pub fn service(attr: TokenStream, item: TokenStream) -> TokenStream {
129    authoring::expand_participant(
130        attr.into(),
131        item.into(),
132        authoring::ParticipantKind::Service,
133    )
134    .unwrap_or_else(syn::Error::into_compile_error)
135    .into()
136}
137
138/// The driver-shaped counterpart to [`service`].
139#[proc_macro_attribute]
140pub fn driver(attr: TokenStream, item: TokenStream) -> TokenStream {
141    authoring::expand_participant(attr.into(), item.into(), authoring::ParticipantKind::Driver)
142        .unwrap_or_else(syn::Error::into_compile_error)
143        .into()
144}
145
146/// The simulator-shaped counterpart to [`service`].
147#[proc_macro_attribute]
148pub fn simulator(attr: TokenStream, item: TokenStream) -> TokenStream {
149    authoring::expand_participant(
150        attr.into(),
151        item.into(),
152        authoring::ParticipantKind::Simulator,
153    )
154    .unwrap_or_else(syn::Error::into_compile_error)
155    .into()
156}
157
158/// The tool-shaped counterpart to [`service`]. `Api` and `Config` default to
159/// `()` - tools stay raw-bus only (decided 2026-07-09), and a configless tool
160/// can launch without `PHOXAL_CONFIG`. An explicit `config = Type` remains
161/// required at launch unless that type itself accepts `null` (for example,
162/// `Option<T>`).
163#[proc_macro_attribute]
164pub fn tool(attr: TokenStream, item: TokenStream) -> TokenStream {
165    authoring::expand_participant(attr.into(), item.into(), authoring::ParticipantKind::Tool)
166        .unwrap_or_else(syn::Error::into_compile_error)
167        .into()
168}