# AIR ## Overview This crate defines the core of the AIR interpreter intended to execute scripts to control execution flow in the [Fluence](https://fluence.network) network. From a high level point of view, the interpreter can be considered as a state transition function that takes two states, merges them, and then produces a new state. ## Interpreter interface This interpreter has only one export function called `invoke` and no import functions. The export function has the following signature: ```rust pub fn executed_air( /// AIR script to execute. air: String, /// Previous data that should be equal to the last returned by the interpreter. prev_data: Vec, /// So-called current data that was sent with a particle from the interpreter on some other peer. data: Vec, /// Running parameters that includes different settings. params: RunParameters, /// Results of calling services. call_results: Vec, ) -> InterpreterOutcome {...} pub struct InterpreterOutcome { /// A return code, where 0 means success. pub ret_code: i32, /// Contains error message if ret_code != 0. pub error_message: String, /// Contains so-called new data that should be preserved in an executor of this interpreter /// regardless of ret_code value. pub data: Vec, /// Public keys of peers that should receive data. pub next_peer_pks: Vec, /// Collected parameters of all met call instructions that could be executed on a current peer. pub call_requests: Vec, } ``` As it was already mentioned in the previous section, `invoke` takes two states (`prev_data` and `current_data`) and returns a new state (`new_data`). Additionally, it takes AIR script that should be executed, some run parameters (such as `init_peer_id` and `current_peer_id`), and `call_results`, results of services calling. As a result it provides the `IntepreterOutcome` structure described in the code snippet above. ## Main properties Let's consider the interpreter with respect to data first, because previous, current and resulted data are the most interesting parts of arguments and the outcome. Assuming `X` is a set of all possible values that data could have, we'll denote `executed_air` export function as `f: X * X -> X`. It could be seen that with respect to data `f` forms a magma. Even more, `f` is an idempotent non-commutative monoid, because: 1. `f` is associative: `forall a, b, c from X: f(f(a,b), c) = f(a, f(b,c))` 2. `f` has a neutral element: `exists e, forall a from X: f(e, a) = f(a, e) = a`, where `e` is a data with an empty trace 3. `f` is a non-commutative function: `exists a, b from X: f(a, b) != f(b, a)` 4. `X` could be constructed from a four based elements that form the `ExecutedState` enum (that why this monoid is free) 5. `f` satisfies these idempotence properties: 1. `forall x from X: f(x, x) = x` 2. `forall a, b from X: f(a, b) = c, f(c, b) = c, f(c, a) = c` ## Interaction with the interpreter The interpreter allows a peer (either a node or a browser) to call service asynchronously by collecting all arguments and other necessary stuff from each `call` instruction that could be called during the execution and return them in `InterpreterOutcome`. A host should then execute them at any time and call back the interpreter providing executed service results as the `call_results` argument. A scheme of interacting with the interpreter should look as follows: 1. For each new `current_data` received from a network, a host should call the interpreter with corresponding `prev_data` and `current_data` and empty `call_results`. `prev_data` here is last `new_data` returned from the interpreter. 2. Having obtained a result of the interpreter, there could be non-empty `next_peer_ids` and non-empty `call_requests` in `InterpreterOutcome`: 1. re `next_peer_pks`: it's a peer duty to decide whether it should send particle after each interpreter call or after the whole particle execution, i.e. after completing all `call_requests`. 2. re `call_requests`: `call_requests` is a `HashMap` and it's important for host to keep that correspondence between `u32` and call `CallRequestParams`, because they should be used when results are passed back on step 3. 3. If `call_requests` was non-empty on step 2, a peer must call the interpreter again with supplied call results (`HashMap`) following next rules: - current_data here shouldn't be supplied (actually, it could be supplied because of `f` is idempotent, but it's unnecessary and slow down a bit an interpreter execution) - it's not necessary to supply `call_results` before handling the next particle, actually a peer could supply it in any moment - a peer must preserve `new_data` after each execution step. It's important, because `f` is non-commutative and the interpreter save an additional info in `data` expecting to see the resulted data back as `prev_data` on the next launch.

Then this flow should be repeated starting from point 2. 4. If `call_requests` was empty, the whole execution is completed, `new_data` must be preserved and particle send for all `new_peer_pks` as usual. An example of interaction can be found in [tests](https://github.com/fluencelabs/aquavm/blob/async/crates/test-utils/src/test_runner.rs).