Skip to main content

arora_behavior/
interpreter_module.rs

1//! The behavior interpreter as a module: the well-known ids and call
2//! conventions under which the runtime exposes its one
3//! [`BehaviorInterpreter`](crate::BehaviorInterpreter) on the engine.
4//!
5//! The runtime assembles a host module under [`ID`] (the engine's generic
6//! module builder) with functions attached to the interpreter's entry points:
7//!
8//! - [`LOAD`] → [`BehaviorInterpreter::load`](crate::BehaviorInterpreter::load):
9//!   replace the running behavior with a whole [`Graph`];
10//! - [`EDIT`] → [`BehaviorInterpreter::apply`](crate::BehaviorInterpreter::apply):
11//!   apply a [`GraphDiff`] to it;
12//! - [`SPAWN`] → [`BehaviorInterpreter::spawn`](crate::BehaviorInterpreter::spawn):
13//!   start a task run from a [`Call`] under a [`RunPolicy`], returning a
14//!   [`TaskHandle`];
15//! - [`HALT`] → [`BehaviorInterpreter::halt`](crate::BehaviorInterpreter::halt):
16//!   stop the run named by a [`TaskId`].
17//!
18//! Payloads travel as structured [`Value`] arguments, converted generically
19//! through [`arora_types::value_serde`] — any serde type flows, no bespoke
20//! encoding — and [`SPAWN`]'s [`TaskHandle`] returns the same way. A
21//! `Call{module_id: ID, id: LOAD|EDIT|SPAWN|HALT}` — a remote's
22//! `BridgeOp::Call`, or a behavior's own call bridge — reaches the interpreter
23//! through the engine's normal dispatch, like any module function.
24
25use arora_types::call::Call;
26use arora_types::value::{StructureField, Value};
27use arora_types::value_serde;
28use serde::de::DeserializeOwned;
29use serde::Serialize;
30use uuid::{uuid, Uuid};
31
32use crate::graph::{Graph, GraphDiff};
33use crate::task::{RunPolicy, TaskHandle, TaskId};
34
35/// Module id under which the runtime registers the behavior interpreter on
36/// the engine. Self-identifying like the vizij type ids: the ASCII bytes of
37/// "arora" lead the UUID, a small offset tails it.
38pub const ID: Uuid = uuid!("61726f72-6100-0000-0000-000000000001");
39
40/// Function id of **edit**: apply a [`GraphDiff`] to the running behavior.
41pub const EDIT: Uuid = uuid!("61726f72-6100-0000-0000-000000000002");
42
43/// Argument id of [`EDIT`]'s one argument: the [`GraphDiff`], as a structured
44/// [`Value`](arora_types::value::Value).
45pub const EDIT_ARG: Uuid = uuid!("61726f72-6100-0000-0000-000000000003");
46
47/// Function id of **load**: replace the running behavior with a [`Graph`].
48pub const LOAD: Uuid = uuid!("61726f72-6100-0000-0000-000000000004");
49
50/// Argument id of [`LOAD`]'s one argument: the [`Graph`], as a structured
51/// [`Value`](arora_types::value::Value).
52pub const LOAD_ARG: Uuid = uuid!("61726f72-6100-0000-0000-000000000005");
53
54/// Function id of **spawn**: start a task run from a [`Call`] under a
55/// [`RunPolicy`], returning a [`TaskHandle`].
56pub const SPAWN: Uuid = uuid!("61726f72-6100-0000-0000-000000000006");
57
58/// Argument id of [`SPAWN`]'s first argument: the [`Call`] to run, as a
59/// structured [`Value`].
60pub const SPAWN_CALL_ARG: Uuid = uuid!("61726f72-6100-0000-0000-000000000007");
61
62/// Argument id of [`SPAWN`]'s second argument: the [`RunPolicy`].
63pub const SPAWN_POLICY_ARG: Uuid = uuid!("61726f72-6100-0000-0000-000000000008");
64
65/// Function id of **halt**: stop the run named by a [`TaskId`].
66pub const HALT: Uuid = uuid!("61726f72-6100-0000-0000-000000000009");
67
68/// Argument id of [`HALT`]'s one argument: the [`TaskId`] of the run to stop.
69pub const HALT_ARG: Uuid = uuid!("61726f72-6100-0000-0000-00000000000a");
70
71/// Build the [`Call`] that applies `diff` to the running behavior.
72pub fn encode_edit(diff: &GraphDiff) -> Call {
73    encode(EDIT, EDIT_ARG, diff)
74}
75
76/// Read the [`GraphDiff`] out of an [`EDIT`] call.
77pub fn decode_edit(call: &Call) -> Result<GraphDiff, String> {
78    decode(call, EDIT, EDIT_ARG, "GraphDiff")
79}
80
81/// Build the [`Call`] that loads `graph` as the running behavior.
82pub fn encode_load(graph: &Graph) -> Call {
83    encode(LOAD, LOAD_ARG, graph)
84}
85
86/// Read the [`Graph`] out of a [`LOAD`] call.
87pub fn decode_load(call: &Call) -> Result<Graph, String> {
88    decode(call, LOAD, LOAD_ARG, "Graph")
89}
90
91/// Build the [`Call`] that spawns `call` as a task run under `policy`.
92pub fn encode_spawn(call: &Call, policy: RunPolicy) -> Call {
93    Call {
94        module_id: Some(ID),
95        id: SPAWN,
96        args: vec![
97            StructureField {
98                id: SPAWN_CALL_ARG,
99                value: Box::new(value_serde::to_value(call).expect("a Call converts to a Value")),
100            },
101            StructureField {
102                id: SPAWN_POLICY_ARG,
103                value: Box::new(
104                    value_serde::to_value(&policy).expect("a RunPolicy converts to a Value"),
105                ),
106            },
107        ],
108    }
109}
110
111/// Read the [`Call`] to run and its [`RunPolicy`] out of a [`SPAWN`] call.
112pub fn decode_spawn(call: &Call) -> Result<(Call, RunPolicy), String> {
113    if call.id != SPAWN {
114        return Err(format!(
115            "not the expected interpreter function: {}",
116            call.id
117        ));
118    }
119    let inner = decode_arg(call, SPAWN_CALL_ARG, "Call")?;
120    let policy = decode_arg(call, SPAWN_POLICY_ARG, "RunPolicy")?;
121    Ok((inner, policy))
122}
123
124/// Build the [`Call`] that halts the run named by `task`.
125pub fn encode_halt(task: TaskId) -> Call {
126    encode(HALT, HALT_ARG, &task)
127}
128
129/// Read the [`TaskId`] out of a [`HALT`] call.
130pub fn decode_halt(call: &Call) -> Result<TaskId, String> {
131    decode(call, HALT, HALT_ARG, "TaskId")
132}
133
134/// Convert the [`TaskHandle`] that [`SPAWN`] returns to a [`Value`], to travel
135/// back as the call's return.
136pub fn encode_spawn_result(handle: &TaskHandle) -> Value {
137    value_serde::to_value(handle).expect("a TaskHandle converts to a Value")
138}
139
140/// Read the [`TaskHandle`] out of a [`SPAWN`] call's return [`Value`].
141pub fn decode_spawn_result(value: &Value) -> Result<TaskHandle, String> {
142    value_serde::from_value(value.clone()).map_err(|e| format!("malformed TaskHandle: {e}"))
143}
144
145fn encode<T: Serialize>(function: Uuid, arg: Uuid, payload: &T) -> Call {
146    let value = value_serde::to_value(payload).expect("a graph payload converts to a Value");
147    Call {
148        module_id: Some(ID),
149        id: function,
150        args: vec![StructureField {
151            id: arg,
152            value: Box::new(value),
153        }],
154    }
155}
156
157fn decode<T: DeserializeOwned>(
158    call: &Call,
159    function: Uuid,
160    arg: Uuid,
161    what: &str,
162) -> Result<T, String> {
163    if call.id != function {
164        return Err(format!(
165            "not the expected interpreter function: {}",
166            call.id
167        ));
168    }
169    decode_arg(call, arg, what)
170}
171
172/// Read one argument value out of `call` by id and convert it to `T` — the arg
173/// half of [`decode`], reused by multi-argument calls like [`SPAWN`].
174fn decode_arg<T: DeserializeOwned>(call: &Call, arg: Uuid, what: &str) -> Result<T, String> {
175    let field = call
176        .args
177        .iter()
178        .find(|field| field.id == arg)
179        .ok_or_else(|| format!("the call is missing its {what} argument"))?;
180    value_serde::from_value(field.value.as_ref().clone())
181        .map_err(|e| format!("malformed {what}: {e}"))
182}
183
184#[cfg(test)]
185mod tests {
186    use super::*;
187
188    #[test]
189    fn edit_call_round_trips_the_diff() {
190        let diff = GraphDiff {
191            remove_nodes: vec![Uuid::from_u128(7)],
192            set_root: Some(Uuid::from_u128(9)),
193            ..Default::default()
194        };
195        let call = encode_edit(&diff);
196        assert_eq!(call.module_id, Some(ID));
197        assert_eq!(call.id, EDIT);
198        assert_eq!(decode_edit(&call).unwrap(), diff);
199    }
200
201    #[test]
202    fn load_call_round_trips_the_graph() {
203        let mut graph = Graph::empty();
204        graph.root = Some(Uuid::from_u128(3));
205        let call = encode_load(&graph);
206        assert_eq!(call.module_id, Some(ID));
207        assert_eq!(call.id, LOAD);
208        assert_eq!(decode_load(&call).unwrap(), graph);
209    }
210
211    #[test]
212    fn spawn_call_round_trips_the_call_and_policy() {
213        let inner = Call {
214            module_id: Some(Uuid::from_u128(1)),
215            id: Uuid::from_u128(2),
216            args: vec![],
217        };
218        let call = encode_spawn(&inner, RunPolicy::Concurrent);
219        assert_eq!(call.module_id, Some(ID));
220        assert_eq!(call.id, SPAWN);
221        assert_eq!(decode_spawn(&call).unwrap(), (inner, RunPolicy::Concurrent));
222
223        // A spawn call under the wrong function id is rejected.
224        let mut wrong = call;
225        wrong.id = Uuid::from_u128(1);
226        assert!(decode_spawn(&wrong).is_err());
227    }
228
229    #[test]
230    fn halt_call_round_trips_the_task_id() {
231        let task = TaskId(Uuid::from_u128(5));
232        let call = encode_halt(task);
233        assert_eq!(call.module_id, Some(ID));
234        assert_eq!(call.id, HALT);
235        assert_eq!(decode_halt(&call).unwrap(), task);
236    }
237
238    #[test]
239    fn spawn_result_round_trips_the_handle() {
240        use arora_types::data::Key;
241        let id = TaskId(Uuid::from_u128(5));
242        let handle = TaskHandle {
243            id,
244            stop: encode_halt(id),
245            status: Key::new("arora/tasks/m/f/5/status"),
246            feedback: vec![Key::new("arora/tasks/m/f/5/feedback")],
247            result: vec![Key::new("arora/tasks/m/f/5/result")],
248            update: vec![],
249        };
250        let value = encode_spawn_result(&handle);
251        assert_eq!(decode_spawn_result(&value).unwrap(), handle);
252    }
253
254    #[test]
255    fn decode_rejects_malformed_calls() {
256        // Wrong function id.
257        let mut call = encode_edit(&GraphDiff::default());
258        call.id = Uuid::from_u128(1);
259        assert!(decode_edit(&call).is_err());
260
261        // Missing the payload argument.
262        let mut call = encode_edit(&GraphDiff::default());
263        call.args.clear();
264        assert!(decode_edit(&call).is_err());
265
266        // The argument does not decode into the expected payload.
267        let mut call = encode_edit(&GraphDiff::default());
268        *call.args[0].value = arora_types::value::Value::Boolean(true);
269        assert!(decode_edit(&call).is_err());
270        let mut call = encode_load(&Graph::empty());
271        *call.args[0].value = arora_types::value::Value::String("not a graph".to_string());
272        assert!(decode_load(&call).is_err());
273    }
274}