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