Skip to main content

arora_behavior/
lib.rs

1//! Behavior interpreters: the executors the Arora runtime ticks each step.
2//!
3//! Two things share the word "behavior", and this crate is about only one of
4//! them:
5//!
6//! - A **behavior** (the noun) is an *authored, editable representation* of what
7//!   a device should do — a behavior tree, a node graph — produced in a visual
8//!   editor (Studio, the Vizij Workspace) and shipped as data.
9//! - A [`BehaviorInterpreter`] is the *runtime-level executor* that runs one of
10//!   those: a behavior-tree interpreter, a node-graph interpreter. It is the
11//!   thing the runtime actually ticks.
12//!
13//! The runtime holds one `Box<dyn BehaviorInterpreter>` and ticks it — swapping
14//! the interpreter replaces the behavior. The behavior tree is one interpreter
15//! (`arora-behavior-tree`'s [`BehaviorTreeInterpreter`]); a Vizij node graph is
16//! another. Adding a new *kind of authored behavior* means adding a new
17//! interpreter here. Hand-implementing the trait to hard-code a single behavior
18//! in Rust is possible, but it is a corner case — the promoted path is to author
19//! a behavior in an editor and let an interpreter run it.
20//!
21//! An authored behavior is a [`graph::Graph`] — nodes bound to functions, with
22//! typed I/Os and links — and interpreters are edited by
23//! [`apply`](BehaviorInterpreter::apply)ing a [`graph::GraphDiff`]. Loading a
24//! behavior is applying a diff onto an empty graph. See [`graph`] for the model.
25//!
26//! Each tick an interpreter gets a [`BehaviorContext`]: the shared
27//! [`DataStore`](arora_types::data::DataStore) (read inputs, write intent /
28//! outputs) and a [`CallBridge`](arora_types::call::CallBridge) (so a
29//! module-calling interpreter like the behavior tree can reach the engine). An
30//! interpreter uses whichever it needs — a graph reads/writes the store; the
31//! tree drives the caller.
32//!
33//! Timing is not a tick argument. The runtime publishes the frame's clock into
34//! the store under the [`built_in`] keys before it ticks, so an interpreter that
35//! needs `dt` or elapsed time reads it from the store like any other slot.
36
37use arora_types::call::{Call, CallBridge};
38use arora_types::data::DataStore;
39
40pub mod built_in;
41pub mod graph;
42pub mod interpreter_module;
43pub mod task;
44
45pub use graph::{Graph, GraphDiff};
46pub use task::{RunPolicy, TaskHandle, TaskId};
47
48/// Whether an interpreter wants to be ticked again — and, when it does not, how
49/// it ended.
50///
51/// Liveness is one bit: [`Running`](Self::Running) or terminal
52/// ([`Done`](Self::Done)). The terminal case carries a `Result` — `Ok(())` for a
53/// clean stop, `Err` for a fault the run could not recover from.
54///
55/// This is the interpreter's *scheduling* status, not an application outcome. A
56/// behavior does not *return* a value — it writes its outputs to store keys — so
57/// the terminal `Ok` is `()`, never a payload. A run's success / failure / errno
58/// is likewise an application value on its status key, not here: one [`tick`] may
59/// drive many runs, so a single return cannot speak for each. What the runtime
60/// must act on is only liveness and, at termination, whether the run stopped
61/// cleanly or faulted.
62///
63/// [`tick`]: BehaviorInterpreter::tick
64#[derive(Debug, Clone, PartialEq, Eq)]
65pub enum BehaviorStatus {
66    /// Not terminal: tick me again next step (a node graph; a tree still
67    /// running).
68    Running,
69    /// Terminal: I reached a terminal state and the runtime drops me. `Ok(())`
70    /// is a clean stop; `Err(_)` is a fault the run could not recover from — its
71    /// reason is surfaced (like an `Err` from [`tick`](BehaviorInterpreter::tick))
72    /// but, unlike a transient `Err`, a run that ends `Done(Err(_))` is dropped,
73    /// *not* retried.
74    Done(Result<(), BehaviorError>),
75}
76
77/// What a [`BehaviorInterpreter`] receives each
78/// [`tick`](BehaviorInterpreter::tick).
79pub struct BehaviorContext<'a> {
80    /// The shared blackboard: read inputs, write intent / outputs.
81    pub store: &'a dyn DataStore,
82    /// The module-call bridge (the engine), for interpreters that call modules.
83    pub call_bridge: &'a mut dyn CallBridge,
84}
85
86/// An interpreter failed to tick.
87#[derive(Debug, Clone, PartialEq, Eq)]
88pub struct BehaviorError {
89    /// Human-readable description.
90    pub message: String,
91}
92
93impl std::fmt::Display for BehaviorError {
94    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
95        f.write_str(&self.message)
96    }
97}
98
99impl std::error::Error for BehaviorError {}
100
101/// A behavior *executor*: something the Arora runtime ticks once per step to run
102/// an authored behavior.
103///
104/// Implemented by the behavior tree (`arora-behavior-tree`'s
105/// [`BehaviorTreeInterpreter`]) and by other interpreters such as a Vizij
106/// node-graph interpreter. The runtime holds one `Box<dyn BehaviorInterpreter>`
107/// and ticks it without knowing which is which.
108///
109/// Implement this to add a new *kind of executor* (a new authored-behavior
110/// representation the runtime can run), not to hand-code one particular
111/// behavior — authored behaviors come from the visual editors, run by an
112/// existing interpreter.
113///
114/// Not `Send`: the runtime is a single-owner, single-thread step loop.
115pub trait BehaviorInterpreter {
116    /// Advance one step. Return [`BehaviorStatus::Running`] to be ticked again,
117    /// or [`BehaviorStatus::Done`] to be dropped — `Done(Ok(()))` for a clean
118    /// stop, `Done(Err(e))` for a terminal fault whose reason `e` is surfaced.
119    ///
120    /// An `Err` from `tick` is different from `Ok(Done(Err(..)))`: an `Err` is a
121    /// *transient* failure — the runtime keeps the interpreter installed and
122    /// ticks it again next step (a momentary lowering or call-bridge hiccup),
123    /// only raising the reason on the standing error watch. `Done(Err(..))` is
124    /// *terminal*: the run has ended and will not be retried.
125    fn tick(&mut self, ctx: &mut BehaviorContext) -> Result<BehaviorStatus, BehaviorError>;
126
127    /// Edit the running behavior by applying a [`GraphDiff`](graph::GraphDiff):
128    /// add/remove nodes and links, set/override predetermined keys. **Loading** a
129    /// behavior is applying a diff onto an empty graph.
130    ///
131    /// The default rejects edition — override it in interpreters that carry an
132    /// editable [`graph::Graph`] (the behavior tree does). A hand-coded,
133    /// non-graph interpreter (the corner case above) can leave this as-is.
134    ///
135    /// `apply` validates and stores the edit; an implementation may defer
136    /// re-lowering its runtime form to the next [`tick`](Self::tick) (which
137    /// carries the store in its context), so a lowering problem can surface
138    /// there rather than here. This keeps `apply` callable from anywhere a
139    /// [`GraphDiff`](graph::GraphDiff) arrives — notably the interpreter's
140    /// engine-registered edit function, which holds no store.
141    fn apply(&mut self, diff: graph::GraphDiff) -> Result<(), BehaviorError> {
142        let _ = diff;
143        Err(BehaviorError {
144            message: "this interpreter does not support graph edition".to_string(),
145        })
146    }
147
148    /// Replace the running behavior with `graph`, whole. Like
149    /// [`apply`](Self::apply) it is store-less — an implementation may defer
150    /// lowering to the next [`tick`](Self::tick) — so it is callable from the
151    /// interpreter's engine-registered load function. The default rejects
152    /// loading, for hand-coded interpreters that carry no graph.
153    fn load(&mut self, graph: graph::Graph) -> Result<(), BehaviorError> {
154        let _ = graph;
155        Err(BehaviorError {
156            message: "this interpreter does not support loading a graph".to_string(),
157        })
158    }
159
160    /// Spawn `call` as a concurrent task run under `policy`, returning a
161    /// [`TaskHandle`] to follow and stop it. Non-blocking: the run advances with
162    /// the normal [`tick`](Self::tick), and its lifecycle surfaces on the
163    /// handle's keys.
164    ///
165    /// The default rejects spawning — override it in interpreters that host
166    /// concurrent runs (a run is realised however the interpreter hosts
167    /// behavior: a parallel behavior-tree subtree, a node-graph subgraph). A
168    /// hand-coded interpreter can leave this as-is.
169    fn spawn(&mut self, call: Call, policy: RunPolicy) -> Result<TaskHandle, BehaviorError> {
170        let _ = (call, policy);
171        Err(BehaviorError {
172            message: "this interpreter does not support spawning task runs".to_string(),
173        })
174    }
175
176    /// Halt the run identified by `task` (idempotent): a clean stop that may run
177    /// compensation, not a hard kill. On reaching a terminal state the run is
178    /// dropped and its status key reflects the outcome.
179    ///
180    /// The default rejects halting — override it alongside [`spawn`](Self::spawn).
181    fn halt(&mut self, task: TaskId) -> Result<(), BehaviorError> {
182        let _ = task;
183        Err(BehaviorError {
184            message: "this interpreter does not support halting task runs".to_string(),
185        })
186    }
187}
188
189#[cfg(test)]
190mod tests {
191    use super::*;
192
193    /// A minimal interpreter that only ticks, so it keeps the default `spawn`
194    /// and `halt` — the ones under test.
195    struct Idle;
196    impl BehaviorInterpreter for Idle {
197        fn tick(&mut self, _ctx: &mut BehaviorContext) -> Result<BehaviorStatus, BehaviorError> {
198            Ok(BehaviorStatus::Running)
199        }
200    }
201
202    #[test]
203    fn spawn_and_halt_reject_by_default() {
204        let mut idle = Idle;
205        let call = Call {
206            module_id: None,
207            id: uuid::Uuid::nil(),
208            args: Vec::new(),
209        };
210        assert!(idle.spawn(call, RunPolicy::Concurrent).is_err());
211        assert!(idle.halt(TaskId(uuid::Uuid::nil())).is_err());
212    }
213}