gmt_dos_actors_dsl/
lib.rs

1/*!
2# actorscript
3
4A scripting micro-language for [gmt_dos-actors].
5
6The `actorscript` procedural macro is a [Domain Specific Language] to write [gmt_dos-actors] models.
7
8`actorscript` parses **flows**.
9A **flow** consists in a sampling rate followed by a **chain**.
10A **chain** is a series of pairs of actor's client and an actor output separated by the token `->`.
11
12As an example:
13```ignore
14actorscript! {
15    1: a[A2B] -> b
16};
17```
18is a **flow** connecting the output `A2B` of client `a` to an input of client `b` at the nominal sampling rate.
19This example will be expanded by the compiler to
20```ignore
21let mut a: Actor<_,1,1> = a.into();
22let mut b: Actor<_,1,1> = b.into();
23a.add_output().build::<A2B>().into_input(&mut b)?;
24let model = model!(a,b).name("model").flowchart().check()?;
25```
26For the code above to compile successfully, the traits [`Write<A2B>`] and [`Read<A2B>`]
27must have been implemented for the clients `a` and `b`, respectively.
28
29The [gmt_dos-actors] model is written in the `completed` state meaning that the model is automatically run to completion
30
31The state the model is written into can be altered with the `state` parameter of the `model` attribute.
32Beside `completed`, two other states can be specified:
33 * `ready`
34```ignore
35actorscript! {
36    #[model(state = ready)]
37    1: a[A2B] -> b
38};
39```
40will build and check the model but running the model and  waiting for completion of the model
41 is left to the user by calling
42```ignore
43model.run().await?;
44```
45 * `running`
46```ignore
47actorscript! {
48    #[model(state = running)]
49    1: a[A2B] -> b
50};
51```
52will execute the model and waiting for completion of the model is left to the user by calling
53```ignore
54model.await?;
55```
56
57Clients are wrapped into an [`Arc`]`<`[`Mutex`]`<_>>` container, cloned and passed to the associated actor.
58A reference to the clients can then be retrieved latter with the `lock` and `await` methods.
59For example, a reference to the client `b` is obtained with:
60```ignore
61let b_ref = *b.lock().await;
62```
63
64## Model growth
65
66A model grows by expanding **chains** with new links and adding new **flows**.
67
68A **chain** grows by adding new clients and ouputs e.g.
69```ignore
70actorscript! {
71    1: a[A2B] -> b[B2C] -> c
72};
73```
74where the output `B2C` is added to `b` and connected to the client `c`.
75
76A new **flow** is added with
77```ignore
78actorscript! {
79    1: a[A2B] -> b[B2C] -> c
80    10: c[C2D] -> d
81};
82```
83Here the new **flow**  is down sampled with a sampling rate that is 1/10th of the nominal sampling rate.
84
85Up sampling can be obtained similarly:
86```ignore
87actorscript! {
88    1: a[A2B] -> b[B2C] -> c
89    10: c[C2D] -> d
90    5: d[D2E] -> e
91};
92```
93In the model above, `C2D` is sent to `d` from `c` every 10 samples
94and `D2E` is sent consecutively twice to `e` from `d` within intervals of 10 samples.
95
96The table below gives the sampling rate for the inputs and outputs of each client:
97
98|        | `a` | `b` | `c` | `d` | `e` |
99|--------|:---:|:---:|:---:|:---:|:---:|
100| inputs | 0   | 1   | 1   | 10  | 5   |
101| outputs| 1   | 1   | 10  | 5   | 0   |
102
103## Rate transitions
104
105The former example illustrates how rate transitions can happen "naturally" between client by
106relying on the up and down sampling implementations within the actors.
107However, this works only if the inputs and/or outputs of a client are only used once per **flow**.
108
109Considering the following example:
110```ignore
111actorscript! {
112    1: a[A2B] -> b[B2C] -> d
113    10: c[C2D] -> b
114};
115```
116The table of inputs and outputs sampling rate is in this case
117
118|        | `a` | `b` | `c` | `d` |
119|--------|:---:|:---:|:---:|:---:|
120| inputs | 0   | 1   | 0   | 1   |
121| outputs| 1   | 1   | 10  | 0   |
122
123Here there is a mismatch between the `C2D` output with a 1/10th sampling rate
124and `b` inputs that have inherited a sampling rate of 1 from the 1st **flow**.
125
126`actorscript` is capable of detecting such mismatch, and it will introduce a rate transition client
127between `c` and `b`, effectively rewriting the model as
128```ignore
129actorscript! {
130    1: a[A2B] -> b[B2C] -> d
131    10: c[C2D] -> r
132    1: r[C2D] -> b
133};
134```
135where `r` is the up sampling rate transition client [Sampler].
136
137## Feedback loop
138
139An example of a feedback loop is a closed **chain** within a **flow** e.g.:
140```ignore
141actorscript! {
142    1: a[A2B] -> b[B2C] -> c[C2B]! -> b
143};
144```
145The flow of data is initiated by the leftmost client (`a`)
146and `b` is blocking until it receives `A2B` and `C2B` but `c` cannot send `C2B` until he has received `B2C` from `b`,
147so both `b` and `c` are waiting for each other.
148To break this kind of stalemate, one can instruct a client to send the data of a given output immediately by appending
149the output with the token `!`.
150
151In the above example, `c` is sending `C2B` at the same time as `a` is sending `A2B` hence allowing `b` to proceed.
152
153Another example of a feedback loop across 2 **flows**:
154```ignore
155actorscript! {
156    1: a[A2B] -> b[B2C] -> c
157    10: c[C2D]! -> d[D2B] -> b
158};
159```
160This version would work as well:
161```ignore
162actorscript! {
163    1: a[A2B] -> b[B2C] -> c
164    10: c[C2D] -> d[D2B]! -> b
165};
166```
167
168## Output data logging
169
170Logging the data of and output is triggered by appending the token `$` after the output like so
171
172```ignore
173actorscript! {
174    1: a[A2B]$ -> b[B2C]$ -> c
175    10: c[C2D]$ -> d[DD]$
176};
177```
178where `A2B` and `B2C` output data are logged into the [parquet] file `model-data_1.parquet` and
179`C2D` and `DD` output data are logged into the [parquet] file `model-data_10.parquet`.
180For logging purposes, `actorscript` rewrites the model as
181```ignore
182actorscript! {
183    1: a[A2B] -> b[B2C] -> c
184    10: c[C2D] -> d[DD]
185    1: a[A2B] -> logging_1
186    1: b[B2C] -> logging_1
187    10: c[C2D] -> logging_10
188    10: d[DD] -> logging_10
189};
190```
191where `logging_1` and `logging_10` are two [Arrow] logging clients.
192References to both clients is available after `actorscript` with
193```ignore
194*logging_1.lock().await
195```
196and
197```ignore
198*logging_10.lock().await
199```
200
201[gmt_dos-actors]: https://docs.rs/gmt_dos-actors
202[Domain Specific Language]: https://en.wikipedia.org/wiki/Domain-specific_language
203[`Write<A2B>`]: https://docs.rs/gmt_dos-clients/latest/gmt_dos_clients/interface/trait.Write.html
204[`Read<A2B>`]: https://docs.rs/gmt_dos-clients/latest/gmt_dos_clients/interface/trait.Read.html
205[`Arc`]: https://doc.rust-lang.org/std/sync/struct.Arc.html
206[`Mutex`]: https://docs.rs/tokio/latest/tokio/sync/struct.Mutex.html#
207[Sampler]: https://docs.rs/gmt_dos-clients/latest/gmt_dos_clients/struct.Sampler.html
208[parquet]: https://parquet.apache.org/
209[Arrow]: https://docs.rs/gmt_dos-clients_arrow/
210*/
211
212use proc_macro::TokenStream;
213use quote::quote;
214use syn::{
215    Attribute,
216    parse::{Parse, ParseStream},
217    parse_macro_input,
218};
219
220/**
221Interpreter for the scripting language of [gmt_dos-actors] models.
222
223Generates all the boilerplate code to build [gmt_dos-actors] models.
224
225See also the [crate](self) documentation for details about building [gmt_dos-actors] models with [actorscript](actorscript!).
226
227## Syntax
228
229### Flow
230
231```ignore
2321: ...
233```
234
235A **flow** always starts with an integer literal following by colon, then a **chain**.
236
237### Chain
238
239```ignore
240pair|client -> another_pair -> ... -> pair|client
241```
242
243The start and end of a chain can be either a client or a pair of a client and an ouput.
244
245### Client-Output Pair
246
247The syntax for a client-output pair is (optional parameters are preceded by `?`)
248```ignore
249?{ client ?::GatewayClient ?} [Output] ?suffix
250```
251
252* `client`: is the name of the client identifier that is the variable declared in the main scope.
253If the client is surrounded by braces i.e `{sys}`, it is assumed to be a [gmt_dos-actors] [system].
254The type of the client that receives an input or sends an output within a [system] acts as a gateway between the [system]
255and the other clients and the gateway client can be specified by appending its type `GatewayClient`
256to the [system] variable like so`{sys::GatewayClient}`.
257* `Output`: is the type of one of the outputs of the actor associated with the client,
258the client must implement the trait `Write<Output>`,
259if it preceded by another client-output pair it must also implement the `Read<PreviousOutput>` trait.
260* `?suffix`: optional operators applied to the ouput (suffix can be combined in any order (e.g `S!..` or `!..$` are both valid)):
261  * `!`: output bootstrapping
262  * `$`: data logging: creates clients variables `<model name>_logging_<flow rate>` and data file `<model name>_data_<flow rate>.parquet`,
263  * `${n}`: same as above but also specifies the data size,
264  * `..`: unbounded output
265  * `~`: stream the output to a [gmt_dos-clients_scope] client
266
267### Attributes
268
269#### `model`
270
271```ignore
272#[model(key = param, ...)]
273```
274#####  keys
275 * `name`: model variable identifier (default: `model`), this is also the name given to the flowchart
276 * `state`: model state identifier: `ready`, `running` or `completed` (default: `completed`)
277 * `flowchart`: flowchart string literal name (default: `"model"`)
278
279#### `labels`
280
281```ignore
282#[model(<client> = "client label", ...)]
283```
284#####  keys
285The key `<client>` is the name of the client identifier that is the variable declared in the main scope.
286The label associated to the key will be display in the flowchart instead of the `<client>` type.
287
288#### `images`
289
290```ignore
291#[model(<client> = "<png file>", ...)]
292```
293#####  keys
294The key `<client>` is the name of the client identifier that is the variable declared in the main scope.
295The image associated to the key will be display in the flowchart instead of the `<client>` type.
296If a label is also given for the same key, it will written over the image.
297
298
299[gmt_dos-actors]: https://docs.rs/gmt_dos-actors
300[system]: https://docs.rs/gmt_dos-actors/latest/gmt_dos_actors/system
301[gmt_dos-clients_scope]: https://docs.rs/gmt_dos-clients_scope
302*/
303#[proc_macro]
304pub fn actorscript(input: TokenStream) -> TokenStream {
305    let script = parse_macro_input!(input as Script);
306    script
307        .try_expand()
308        .unwrap_or_else(syn::Error::into_compile_error)
309        .into()
310}
311
312mod model;
313use model::Model;
314mod client;
315
316pub(crate) type Expanded = proc_macro2::TokenStream;
317
318/// Source code expansion
319pub(crate) trait Expand {
320    fn expand(&self) -> Expanded;
321}
322/// Faillible source code expansion
323pub(crate) trait TryExpand {
324    fn try_expand(&self) -> syn::Result<Expanded>;
325}
326
327/// Script parser
328///
329/// The script parser holds the code of the actors model
330#[derive(Debug, Clone)]
331struct Script {
332    model: Model,
333}
334
335impl Parse for Script {
336    fn parse(input: ParseStream) -> syn::Result<Self> {
337        let attrs = input.call(Attribute::parse_outer).ok();
338        let model = input.parse::<Model>()?.attributes(attrs)?.build();
339        println!("/*\n{model} */");
340        Ok(Script { model })
341    }
342}
343
344impl TryExpand for Script {
345    fn try_expand(&self) -> syn::Result<Expanded> {
346        let model = self.model.try_expand()?;
347        Ok(quote!(#model))
348    }
349}