pub mod behavior;
#[cfg(test)]
mod call_expression_tests;
pub mod core_nodes;
pub mod detached;
#[cfg(test)]
mod detached_tests;
pub mod display;
pub mod error;
pub mod graph;
pub mod nodes;
mod scatter;
pub mod schema;
pub mod schema_groot;
#[cfg(test)]
mod tests;
pub mod tree_node;
pub mod variable;
pub use arora_behavior::{FunctionIndex, ModuleFunction};
use detached::Detached;
pub use detached::DetachedPlan;
use crate::display::{stated_id, Names};
use crate::variable::{is_tree_local, VariableCell, VariableResolver};
use arora_behavior::Status;
use arora_behavior_tree_types::TickId;
use arora_types::call::{CallBridge, CallError, Callable, CallableId};
use arora_types::record::module::frozen::Function;
use arora_types::AroraType;
use arora_types::{
call::Call,
value::{ConversionError, StructureField, Value},
};
use error::BehaviorTreeError;
use schema::{CallExpression, Expression, Node, NodeParameterId, _RET_PARAM_ID};
use std::{
cell::RefCell,
collections::{HashMap, HashSet},
rc::Rc,
};
use uuid::Uuid;
use crate::nodes::{
EQUAL_A_PARAM_ID, EQUAL_B_PARAM_ID, EQUAL_FUNCTION_ID, FAIL_FUNCTION_ID, FALLBACK_FUNCTION_ID,
PARALLEL_FUNCTION_ID, RUN_CALL_FUNCTION_ID, RUN_CALL_PARAM_ID, RUN_FUNCTION_ID,
RUN_STATUS_FUNCTION_ID, RUN_STATUS_LATCH_PARAM_ID, RUN_STATUS_OUT_PARAM_ID,
SCATTER_FUNCTION_ID, SCATTER_KEYS_PARAM_ID, SCATTER_VALUES_PARAM_ID, SCATTER_WITHIN_PARAM_ID,
SEQ_FUNCTION_ID, SEQ_STAR_CURRENT_INDEX_PARAM_ID, SEQ_STAR_FUNCTION_ID, SUCCEED_FUNCTION_ID,
UNPACK_FUNCTION_ID, UNPACK_OUT_PARAM_IDS, UNPACK_VALUES_PARAM_ID,
};
pub struct BehaviorTree {
root: Rc<Node>,
node_index: HashMap<Uuid, Rc<Node>>,
variables: Rc<RefCell<HashMap<Uuid, VariableCell>>>,
node_arg_variables: Rc<HashMap<NodeParameterId, VariableCell>>,
outputs: HashMap<Uuid, Outputs>,
detached: Rc<Detached>,
}
#[derive(Clone)]
enum Outputs {
Ports(Rc<[Option<VariableCell>]>),
Keys(Rc<scatter::Scatter>),
}
impl BehaviorTree {
pub fn detached_plan(&self) -> DetachedPlan {
self.detached.plan()
}
pub fn detached_calls(&self, id: Uuid) -> Option<u64> {
self.detached.calls(id)
}
pub fn display_detached(&self, names: Names) -> String {
self.detached
.nodes()
.iter()
.map(|node| format!("{}\n", node.display(names)))
.collect()
}
fn lower_detached(&self, functions: &FunctionIndex) -> Result<(), BehaviorTreeError> {
let mut attached: Vec<Rc<Node>> = Vec::new();
let mut seen = HashSet::new();
let mut held = vec![self.root.clone()];
while let Some(node) = held.pop() {
if seen.insert(node.id) {
held.extend(
node.children
.iter()
.flatten()
.filter_map(|child| self.node_index.get(child).cloned()),
);
attached.push(node);
}
}
self.detached.lower(
functions,
&attached,
&self.variables.borrow(),
&self.node_arg_variables,
)
}
fn tick_root(
&self,
root: &TickId,
caller: &mut dyn CallBridge,
) -> Result<Status, BehaviorTreeError> {
self.detached.begin_tick();
let status = root.tick(caller);
let variables = self.variables.borrow();
let calls = FunctionIndex::new();
let sinks = self.detached.tick_sinks(
Scope {
variables: &variables,
node_parameters_variables: &self.node_arg_variables,
calls: &calls,
},
caller,
);
let status = status?;
sinks?;
Ok(status)
}
pub(crate) fn keep_state(&self, previous: &BehaviorTree, fresh: &HashSet<Uuid>) {
let mut read = HashSet::new();
for node in self.node_index.values() {
if fresh.contains(&node.id) {
continue;
}
for (parameter, expression) in &node.arguments {
variable_ids_in(expression, &mut read);
match selected_source(expression) {
Expression::VariableId(_) => {}
Expression::Value(_) | Expression::Uuid(_) | Expression::Call(_) => {
let at = NodeParameterId {
node: node.id,
parameter: *parameter,
};
if let Some(held) = previous
.node_index
.get(&node.id)
.and_then(|previous| previous.arguments.get(parameter))
{
take_call_site_cells(expression, held);
}
if let (Some(cell), Some(held)) = (
self.node_arg_variables.get(&at),
previous.node_arg_variables.get(&at),
) {
take_local(cell, held);
}
}
Expression::NodeArgument(_)
| Expression::Variable(_)
| Expression::Select { .. } => {}
}
}
}
let (cells, held) = (self.variables.borrow(), previous.variables.borrow());
for variable in read {
if let (Some(cell), Some(held)) = (cells.get(&variable), held.get(&variable)) {
take_local(cell, held);
}
}
}
}
fn variable_ids_in(expression: &Expression, found: &mut HashSet<Uuid>) {
match expression {
Expression::VariableId(id) => {
found.insert(*id);
}
Expression::Select { source, .. } => variable_ids_in(source, found),
Expression::Call(call) => {
let parts = [&call.module, &call.function].into_iter().chain(
call.arguments
.iter()
.flat_map(|(parameter, argument)| [parameter, argument]),
);
for part in parts {
variable_ids_in(part, found);
}
}
_ => {}
}
}
fn selected_source(expression: &Expression) -> &Expression {
match expression {
Expression::Select { source, .. } => selected_source(source),
expression => expression,
}
}
fn take_local(cell: &VariableCell, held: &VariableCell) {
if let (VariableCell::Local(cell), VariableCell::Local(held)) = (cell, held) {
cell.swap(held);
}
}
pub struct BehaviorTreeRuntime<'a> {
caller: &'a mut dyn CallBridge,
tick: TickId,
tree: &'a BehaviorTree,
}
impl<'a> BehaviorTreeRuntime<'a> {
pub fn setup(
tree: &'a BehaviorTree,
functions: &FunctionIndex,
caller: &'a mut dyn CallBridge,
trace: bool,
) -> Result<Self, BehaviorTreeError> {
let (tick, _) = register_ticks(tree, functions, caller, trace)?;
Ok(Self { caller, tick, tree })
}
pub fn tick(&mut self) -> Result<Status, BehaviorTreeError> {
self.tree.tick_root(&self.tick, self.caller)
}
}
pub struct LoweredTree {
root: TickId,
callables: Vec<CallableId>,
tree: BehaviorTree,
}
pub fn lower_behavior_tree(
tree: BehaviorTree,
functions: &FunctionIndex,
caller: &mut dyn CallBridge,
trace: bool,
) -> Result<LoweredTree, BehaviorTreeError> {
let (root, callables) = register_ticks(&tree, functions, caller, trace)?;
Ok(LoweredTree {
root,
callables,
tree,
})
}
impl LoweredTree {
pub fn tick(&self, caller: &mut dyn CallBridge) -> Result<Status, BehaviorTreeError> {
self.tree.tick_root(&self.root, caller)
}
pub fn tree(&self) -> &BehaviorTree {
&self.tree
}
pub fn unregister(&self, caller: &mut dyn CallBridge) {
for callable in &self.callables {
caller.arora_unregister_callable(callable);
}
}
}
fn register_ticks(
tree: &BehaviorTree,
functions: &FunctionIndex,
caller: &mut dyn CallBridge,
trace: bool,
) -> Result<(TickId, Vec<CallableId>), BehaviorTreeError> {
tree.lower_detached(functions)?;
let mut callables = Vec::new();
let root = setup_tick_function(
tree.root.clone(),
&tree.node_index,
functions,
tree.variables.clone(),
tree.node_arg_variables.clone(),
tree.detached.clone(),
&tree.outputs,
caller,
if trace {
TraceTick::YesAll
} else {
TraceTick::No
},
&mut callables,
);
match root {
Ok(root) => Ok((root, callables)),
Err(error) => {
for callable in &callables {
caller.arora_unregister_callable(callable);
}
Err(error)
}
}
}
pub fn run_behavior_tree(
behavior: &BehaviorTree,
functions: &FunctionIndex,
caller: &mut dyn CallBridge,
trace: bool,
) -> Result<Status, BehaviorTreeError> {
let mut runtime = BehaviorTreeRuntime::setup(behavior, functions, caller, trace)?;
let mut status = Status::Running;
while status == Status::Running {
status = runtime.tick()?;
}
Ok(status)
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
enum TraceTick {
YesAll,
No,
}
#[allow(clippy::too_many_arguments)]
fn setup_tick_function(
node: Rc<Node>,
node_index: &HashMap<Uuid, Rc<Node>>,
functions: &FunctionIndex,
variables: Rc<RefCell<HashMap<Uuid, VariableCell>>>,
node_arg_variables: Rc<HashMap<NodeParameterId, VariableCell>>,
detached: Rc<Detached>,
outputs: &HashMap<Uuid, Outputs>,
caller: &mut dyn CallBridge,
trace: TraceTick,
callables: &mut Vec<CallableId>,
) -> Result<TickId, BehaviorTreeError> {
let nof_children = node
.children
.as_ref()
.map(|children| children.len())
.unwrap_or(0);
let mut children_ticks: Vec<TickId> = Vec::with_capacity(nof_children);
if let Some(children) = &node.children {
for child_id in children {
let child_node = node_index
.get(child_id)
.ok_or(BehaviorTreeError::ChildNodeNotFound {
node: node.id,
child: *child_id,
})?
.clone();
let tick_function_with_id = setup_tick_function(
child_node.clone(),
node_index,
functions,
variables.clone(),
node_arg_variables.clone(),
detached.clone(),
outputs,
caller,
trace,
callables,
)?;
children_ticks.push(tick_function_with_id);
}
}
let tick_function: Rc<dyn Callable> = Rc::new(TickFunction {
node: node.clone(),
function: functions.get(&node.function).cloned(),
calls: Rc::new(call_signatures(&node, functions)?),
argument_order: crate::display::ordered(&node.arguments, functions.get(&node.function))
.into_iter()
.map(|(parameter, _)| parameter)
.collect(),
locals: variables.to_owned(),
node_arg_variables: node_arg_variables.to_owned(),
pulls: detached.pulls(&node.id),
detached,
outputs: outputs.get(&node.id).cloned(),
children: children_ticks,
trace,
});
let callable_id = caller.arora_register_callable(tick_function);
let tick = TickId {
callable_id: callable_id.id,
};
callables.push(callable_id);
Ok(tick)
}
pub(crate) fn is_native(function: Uuid) -> bool {
matches!(
function,
SUCCEED_FUNCTION_ID
| FAIL_FUNCTION_ID
| RUN_FUNCTION_ID
| SEQ_FUNCTION_ID
| SEQ_STAR_FUNCTION_ID
| FALLBACK_FUNCTION_ID
| PARALLEL_FUNCTION_ID
| RUN_CALL_FUNCTION_ID
| RUN_STATUS_FUNCTION_ID
| EQUAL_FUNCTION_ID
| UNPACK_FUNCTION_ID
| SCATTER_FUNCTION_ID
) || core_nodes::core_node_parameters(function).is_some()
}
fn tick_builtin(
caller: &mut dyn CallBridge,
scope: Scope,
outputs: Option<&Outputs>,
child_tick_ids: &[TickId],
node: &Node,
) -> Result<Option<Status>, BehaviorTreeError> {
let argument = |caller: &mut dyn CallBridge, parameter: Uuid| {
let node_parameter = NodeParameterId {
node: node.id,
parameter,
};
let expression = node.arguments.get(¶meter).ok_or_else(|| {
BehaviorTreeError::InconsistentTreeError {
message: format!("{node_parameter} has no argument"),
}
})?;
let cell = scope.node_parameters_variables.get(&node_parameter);
let value = match (expression, cell) {
(Expression::Call(_) | Expression::Select { .. }, _) | (_, None) => {
compute_expression(scope, expression, caller, &node_parameter)?
}
(_, Some(cell)) => cell.get_or_unit(),
};
if let (Expression::Call(_), Some(cell)) = (expression, cell) {
cell.set_opt(Some(value.clone()).filter(|value| *value != Value::Unit));
}
Ok::<_, BehaviorTreeError>(value)
};
let status = match node.function {
EQUAL_FUNCTION_ID => {
if argument(caller, EQUAL_A_PARAM_ID)? == argument(caller, EQUAL_B_PARAM_ID)? {
Status::Success
} else {
Status::Failure
}
}
UNPACK_FUNCTION_ID => {
let Some(Outputs::Ports(outputs)) = outputs else {
return Err(BehaviorTreeError::InconsistentTreeError {
message: format!("Unpack node {} has no bound outputs", node.id),
});
};
match argument(caller, UNPACK_VALUES_PARAM_ID)?.into_elements() {
Ok(values) if values.len() >= outputs.len() => {
for (cell, value) in outputs.iter().zip(values) {
if let Some(cell) = cell.as_ref().filter(|_| value != Value::Unit) {
cell.set(value);
}
}
Status::Success
}
_ => Status::Failure,
}
}
SCATTER_FUNCTION_ID => {
let Some(Outputs::Keys(scatter)) = outputs else {
return Err(BehaviorTreeError::InconsistentTreeError {
message: format!("Scatter node {} is not bound", node.id),
});
};
scatter.write(
argument(caller, SCATTER_KEYS_PARAM_ID)?,
argument(caller, SCATTER_VALUES_PARAM_ID)?,
)?
}
SUCCEED_FUNCTION_ID => Status::Success,
FAIL_FUNCTION_ID => Status::Failure,
RUN_FUNCTION_ID => Status::Running,
SEQ_FUNCTION_ID => {
let mut status = Status::Success;
for child in child_tick_ids {
match child.tick(caller)? {
Status::Success => continue,
Status::Failure => {
status = Status::Failure;
break;
}
Status::Running => {
status = Status::Running;
break;
}
}
}
status
}
FALLBACK_FUNCTION_ID => {
if child_tick_ids.is_empty() {
Status::Success
} else {
let mut status = Status::Failure;
for child in child_tick_ids {
match child.tick(caller)? {
Status::Success => {
status = Status::Success;
break;
}
Status::Failure => continue,
Status::Running => {
status = Status::Running;
break;
}
}
}
status
}
}
PARALLEL_FUNCTION_ID => {
let mut success = true;
let mut failure = false;
for child in child_tick_ids {
match child.tick(caller)? {
Status::Success => continue,
Status::Failure => {
success = false;
failure = true;
}
Status::Running => {
success = false;
}
}
}
if success {
Status::Success
} else if failure {
Status::Failure
} else {
Status::Running
}
}
RUN_CALL_FUNCTION_ID => {
let call_variable = get_node_parameter_variable(
&NodeParameterId {
node: node.id,
parameter: RUN_CALL_PARAM_ID,
},
scope.node_parameters_variables,
)?;
let call: Call = arora_types::value_serde::from_value(call_variable.get_or_unit())
.map_err(|e| BehaviorTreeError::InconsistentTreeError {
message: format!("run-call node {}: malformed call: {e}", node.id),
})?;
let result = caller
.arora_call(call)
.map_err(BehaviorTreeError::CallError)?;
result.ret.try_into().unwrap_or(Status::Success)
}
RUN_STATUS_FUNCTION_ID => {
let latch = get_node_parameter_variable(
&NodeParameterId {
node: node.id,
parameter: RUN_STATUS_LATCH_PARAM_ID,
},
scope.node_parameters_variables,
)?;
if let Ok(latched) = Status::try_from(latch.get_or_unit()) {
if latched != Status::Running {
return Ok(Some(latched));
}
}
let child = child_tick_ids
.first()
.ok_or(BehaviorTreeError::InconsistentTreeError {
message: format!("run-status decorator {} needs one child", node.id),
})?;
let ticked = child.tick(caller);
let out = get_node_parameter_variable(
&NodeParameterId {
node: node.id,
parameter: RUN_STATUS_OUT_PARAM_ID,
},
scope.node_parameters_variables,
)?;
let status = match ticked {
Ok(status) => status,
Err(error) => {
out.set(Status::Failure.into());
latch.set(Status::Failure.into());
return Err(error);
}
};
out.set(status.into());
if status != Status::Running {
latch.set(status.into());
}
status
}
SEQ_STAR_FUNCTION_ID => {
let index_variable = get_node_parameter_variable(
&NodeParameterId {
node: node.id,
parameter: SEQ_STAR_CURRENT_INDEX_PARAM_ID,
},
scope.node_parameters_variables,
)?;
let mut current_index = match index_variable.get_or_unit() {
Value::U16(index) => index,
_ => 0,
};
let mut status = Status::Success;
for child in child_tick_ids.iter().skip(current_index as usize) {
match child.tick(caller)? {
Status::Success => current_index += 1,
Status::Failure => {
status = Status::Failure;
break;
}
Status::Running => {
status = Status::Running;
break;
}
}
}
if status != Status::Running {
current_index = 0;
}
index_variable.set(Value::U16(current_index));
status
}
function => {
let writes = detached::native_writes(function);
let mut selected = HashMap::new();
for parameter in core_nodes::core_node_parameters(function).unwrap_or_default() {
match node.arguments.get(¶meter.id) {
Some(Expression::Call(_)) => {
argument(caller, parameter.id)?;
}
Some(Expression::Select { .. }) if !writes.contains(¶meter.id) => {
let value = argument(caller, parameter.id)?;
selected.insert(parameter.id, VariableCell::local_with(value));
}
_ => {}
}
}
return core_nodes::tick_core_node(function, node.id, |parameter| {
if let Some(cell) = selected.get(¶meter) {
return Ok(cell.clone());
}
get_node_parameter_variable(
&NodeParameterId {
node: node.id,
parameter,
},
scope.node_parameters_variables,
)
.cloned()
});
}
};
Ok(Some(status))
}
#[allow(clippy::too_many_arguments)]
fn tick(
caller: &mut dyn CallBridge,
function: Option<&ModuleFunction>,
calls: &FunctionIndex,
argument_order: &[Uuid],
variables: Rc<RefCell<HashMap<Uuid, VariableCell>>>,
node_parameters_variables: Rc<HashMap<NodeParameterId, VariableCell>>,
detached: (&Detached, Option<&[usize]>),
outputs: Option<&Outputs>,
child_tick_ids: &[TickId],
node: Rc<Node>,
trace: TraceTick,
) -> Result<Status, BehaviorTreeError> {
let trace_line = |status: Status| {
if trace != TraceTick::No {
println!(
"tick {} {} -> {:?}",
node.id,
node.display(Names::functions(calls)),
status
);
}
};
if let (detached, Some(pulls)) = detached {
detached.pull(
pulls,
Scope {
variables: &variables.borrow(),
node_parameters_variables: &node_parameters_variables,
calls,
},
caller,
)?;
}
let builtin = tick_builtin(
caller,
Scope {
variables: &variables.borrow(),
node_parameters_variables: &node_parameters_variables,
calls,
},
outputs,
child_tick_ids,
&node,
)?;
if let Some(status) = builtin {
trace_line(status);
return Ok(status);
}
let module_function =
function.ok_or(BehaviorTreeError::CallError(CallError::FunctionNotFound {
id: node.function,
}))?;
let function = &module_function.function;
let mut call = Call {
module_id: Some(module_function.module_id),
id: node.function,
args: Vec::with_capacity(
node.arguments.len() + if node.children.is_some() { 1 } else { 0 },
),
};
let nof_children = node
.children
.as_ref()
.map(|children| children.len())
.unwrap_or(0);
assert_eq!(nof_children, child_tick_ids.len());
if node.children.is_some() {
let first_parameter_id = function.parameter_ordering.first().ok_or(
BehaviorTreeError::MissingChildrenParameter {
node: node.id.to_owned(),
function: node.function.to_owned(),
},
)?;
let first_parameter = function.parameters.get(first_parameter_id).ok_or(
BehaviorTreeError::MissingChildrenParameter {
node: node.id,
function: node.function.to_owned(),
},
)?;
if first_parameter.name != "children" {
Err(BehaviorTreeError::MissingChildrenParameter {
node: node.id,
function: node.function.to_owned(),
})?;
}
let first_parameter_type_id = first_parameter
.ty
.as_array()
.ok_or(BehaviorTreeError::MissingChildrenParameter {
node: node.id,
function: node.function.to_owned(),
})?
.reference
.id;
if first_parameter_type_id != TickId::arora_type_id() {
Err(BehaviorTreeError::MissingChildrenParameter {
node: node.id,
function: node.function.to_owned(),
})?;
}
call.args.push(StructureField {
id: *first_parameter_id,
value: Box::new(Value::array_of_type::<TickId>(
child_tick_ids.iter().copied().map(Value::from).collect(),
)),
})
}
let mut locals = HashMap::<Uuid, VariableCell>::new();
let mut selected = Vec::new();
{
let variables = variables.borrow();
let scope = Scope {
variables: &variables,
node_parameters_variables: &node_parameters_variables,
calls,
};
for param_id in argument_order {
let value_expression = &node.arguments[param_id];
let node_parameter = NodeParameterId {
node: node.id.to_owned(),
parameter: param_id.to_owned(),
};
let variable =
get_node_parameter_variable(&node_parameter, &node_parameters_variables)?;
if matches!(value_expression, Expression::Call(_)) {
let value = compute_expression(scope, value_expression, caller, &node_parameter)?;
variable.set_opt(Some(value).filter(|value| *value != Value::Unit));
};
if param_id == &_RET_PARAM_ID {
locals.insert(*param_id, variable.clone());
continue;
}
let value = if let Expression::Select { .. } = value_expression {
selected.push(*param_id);
Some(compute_expression(
scope,
value_expression,
caller,
&node_parameter,
)?)
.filter(|value| *value != Value::Unit)
} else {
locals.insert(*param_id, variable.clone());
variable.get()
};
call.args.push(StructureField {
id: *param_id,
value: Box::new(argument_value(value, is_optional(function, param_id))),
});
}
}
let result = caller
.arora_call(call)
.map_err(BehaviorTreeError::CallError)?;
for mutated in result.mutated {
if selected.contains(&mutated.id) {
continue;
}
let variable = locals
.get(&mutated.id)
.ok_or(BehaviorTreeError::InternalError {
message: format!(
"mutated parameter {} does not correspond to any local variable",
mutated.id
),
})?;
write_back(variable, *mutated.value, is_optional(function, &mutated.id));
}
let status = if let Some(var) = locals.get(&_RET_PARAM_ID) {
write_back(var, result.ret.clone(), function.return_ty.is_option());
Status::Success
} else {
result.ret.try_into().unwrap_or(Status::Success)
};
trace_line(status);
Ok(status)
}
pub(crate) fn is_optional(function: &Function, parameter: &Uuid) -> bool {
function
.parameters
.get(parameter)
.is_some_and(|parameter| parameter.ty.is_option())
}
pub(crate) fn argument_value(value: Option<Value>, optional: bool) -> Value {
match (value, optional) {
(Some(value @ Value::Option(_)), _) => value,
(Some(value), true) => Value::Option(Some(Box::new(value))),
(Some(value), false) => value,
(None, true) => Value::Option(None),
(None, false) => Value::Unit,
}
}
pub(crate) fn write_back(variable: &VariableCell, value: Value, optional: bool) {
match value {
Value::Option(Some(value)) if optional => variable.set(*value),
Value::Option(None) if optional => {
if variable.get().is_some() {
variable.set_opt(None);
}
}
value => variable.set(value),
}
}
trait Tickable {
fn tick(&self, caller: &mut dyn CallBridge) -> Result<Status, BehaviorTreeError>;
}
impl Tickable for TickId {
fn tick(&self, caller: &mut dyn CallBridge) -> Result<Status, BehaviorTreeError> {
CallableId {
id: self.callable_id,
}
.tick(caller)
}
}
impl Tickable for CallableId {
fn tick(&self, caller: &mut dyn CallBridge) -> Result<Status, BehaviorTreeError> {
let value = self.call(caller).map_err(BehaviorTreeError::CallError)?;
value.try_into().map_err(|_| {
BehaviorTreeError::ConversionError(ConversionError {
message: "return value cannot be interpreted as a Status".to_string(),
})
})
}
}
struct TickFunction {
node: Rc<Node>,
function: Option<ModuleFunction>,
calls: Rc<FunctionIndex>,
argument_order: Rc<[Uuid]>,
locals: Rc<RefCell<HashMap<Uuid, VariableCell>>>,
node_arg_variables: Rc<HashMap<NodeParameterId, VariableCell>>,
detached: Rc<Detached>,
pulls: Option<Rc<[usize]>>,
outputs: Option<Outputs>,
children: Vec<TickId>,
trace: TraceTick,
}
impl Tickable for TickFunction {
fn tick(&self, caller: &mut dyn CallBridge) -> Result<Status, BehaviorTreeError> {
tick(
caller,
self.function.as_ref(),
&self.calls,
&self.argument_order,
self.locals.clone(),
self.node_arg_variables.clone(),
(&self.detached, self.pulls.as_deref()),
self.outputs.as_ref(),
&self.children,
self.node.clone(),
self.trace,
)
}
}
impl Callable for TickFunction {
fn call(&self, caller: &mut dyn CallBridge) -> Result<Value, CallError> {
self.tick(caller)
.map(Into::<Value>::into)
.map_err(Into::<CallError>::into)
}
}
pub fn load_behavior_tree_nodes(nodes: Vec<Node>) -> Result<BehaviorTree, BehaviorTreeError> {
load_behavior_tree_nodes_with(nodes, &|_| None, &HashMap::new())
}
pub fn load_behavior_tree_nodes_with(
nodes: Vec<Node>,
resolver: &VariableResolver,
names: &HashMap<Uuid, String>,
) -> Result<BehaviorTree, BehaviorTreeError> {
let mut node_index: HashMap<Uuid, Rc<Node>> = HashMap::new();
let mut listed: Vec<Rc<Node>> = Vec::new();
let mut root: Option<Rc<Node>> = None;
let mut variables = HashMap::new();
let mut node_parameters_variables = HashMap::new();
let mut arguments = Vec::new();
let mut outputs = HashMap::new();
for mut node in nodes {
node.arguments = node
.arguments
.into_iter()
.map(|(parameter, argument)| (parameter, with_call_site_cells(argument)))
.collect();
let shared_node = Rc::new(node);
if root.is_none() {
root = Some(shared_node.clone());
}
listed.push(shared_node.clone());
let existing_node = node_index.insert(shared_node.id, shared_node.clone());
if let Some(existing_node) = existing_node {
return Err(BehaviorTreeError::InconsistentTreeError {
message: format!("duplicate node {}", existing_node.id),
});
}
for (param_id, arg_expr) in &shared_node.arguments {
arguments.push((
NodeParameterId {
node: shared_node.id.to_owned(),
parameter: param_id.to_owned(),
},
arg_expr.clone(),
));
}
}
let mut attached = HashSet::new();
let mut held: Vec<Uuid> = root.iter().map(|root| root.id).collect();
while let Some(id) = held.pop() {
if attached.insert(id) {
if let Some(children) = node_index.get(&id).and_then(|node| node.children.as_ref()) {
held.extend(children.iter().copied());
}
}
}
let detached_nodes = Detached::select(&listed, &attached);
let detached_ids: HashSet<Uuid> = detached_nodes.iter().map(|node| node.id).collect();
let mut bindings = HashMap::new();
let mut fed_inputs = HashSet::new();
for node in &detached_nodes {
if !node.arguments.contains_key(&_RET_PARAM_ID) {
node_parameters_variables.insert(
NodeParameterId {
node: node.id,
parameter: _RET_PARAM_ID,
},
VariableCell::local(),
);
}
}
let mut pending = arguments;
while !pending.is_empty() {
let before = pending.len();
let mut unresolved = Vec::new();
for (node_param, arg_expr) in pending {
if read_argument(&arg_expr)
.is_some_and(|source| !node_parameters_variables.contains_key(source))
{
unresolved.push((node_param, arg_expr));
continue;
}
let cell = setup_node_parameter_variable(
&node_param,
&arg_expr,
&mut variables,
&mut node_parameters_variables,
resolver,
names,
)?;
let detached = detached_ids.contains(&node_param.node);
let fed = read_argument(&arg_expr)
.filter(|_| detached && node_param.parameter == _RET_PARAM_ID)
.map(|input| {
let mut fed = input.clone();
for _ in 0..node_index.len() {
let next = node_index
.get(&fed.node)
.and_then(|node| node.arguments.get(&fed.parameter))
.and_then(read_argument);
match next {
Some(next) => fed = next.clone(),
None => break,
}
}
fed
})
.filter(|fed| {
matches!(
node_parameters_variables.get(fed),
Some(VariableCell::Stored(_))
)
});
if let Some(fed) = fed {
let port = VariableCell::local();
node_parameters_variables.insert(node_param.clone(), port.clone());
node_parameters_variables.insert(fed.clone(), port);
fed_inputs.insert(fed);
} else if matches!(cell, VariableCell::Stored(_)) && detached {
node_parameters_variables.insert(node_param.clone(), VariableCell::local());
bindings.insert(node_param, cell);
}
}
if let Some((node_param, arg_expr)) =
unresolved.first().filter(|_| unresolved.len() == before)
{
setup_node_parameter_variable(
node_param,
arg_expr,
&mut variables,
&mut node_parameters_variables,
resolver,
names,
)?;
}
pending = unresolved;
}
if !fed_inputs.is_empty() {
let sources: HashMap<NodeParameterId, NodeParameterId> = node_index
.values()
.flat_map(|node| {
node.arguments.iter().filter_map(|(parameter, argument)| {
read_argument(argument).map(|source| {
(
NodeParameterId {
node: node.id,
parameter: *parameter,
},
source.clone(),
)
})
})
})
.collect();
for reader in sources.keys() {
let mut at = reader;
let mut fed = None;
for _ in 0..sources.len() {
let Some(source) = sources.get(at) else {
break;
};
if fed_inputs.contains(source) {
fed = Some(source);
}
at = source;
}
if let Some(cell) = fed.and_then(|fed| node_parameters_variables.get(fed).cloned()) {
node_parameters_variables.insert(reader.clone(), cell);
}
}
}
for node in node_index.values() {
if let Some(bound) = bind_data_node(node, names, &node_parameters_variables)? {
outputs.insert(node.id, bound);
}
}
let detached = Detached::new(detached_nodes, bindings);
Ok(BehaviorTree {
root: root.ok_or(BehaviorTreeError::InconsistentTreeError {
message: "behavior tree has no nodes".to_string(),
})?,
node_index,
variables: Rc::new(RefCell::new(variables)),
node_arg_variables: Rc::new(node_parameters_variables),
outputs,
detached: Rc::new(detached),
})
}
pub fn load_behavior_tree_yaml(yaml: &str) -> Result<BehaviorTree, BehaviorTreeError> {
load_behavior_tree_nodes(serde_yaml::from_str(yaml)?)
}
fn get_variable<'a>(
variables: &'a HashMap<Uuid, VariableCell>,
variable_id: &Uuid,
node_id: &Uuid,
) -> Result<&'a VariableCell, BehaviorTreeError> {
variables
.get(variable_id)
.ok_or(BehaviorTreeError::VariableNotFound {
variable: variable_id.to_owned(),
node: node_id.to_owned(),
})
}
pub(crate) fn bind_data_node(
node: &Node,
names: &HashMap<Uuid, String>,
node_parameters_variables: &HashMap<NodeParameterId, VariableCell>,
) -> Result<Option<Outputs>, BehaviorTreeError> {
let refuse = |what: String| {
Err(BehaviorTreeError::InconsistentTreeError {
message: format!("node {}: {what}", node.id),
})
};
let required: &[Uuid] = match node.function {
EQUAL_FUNCTION_ID => &[EQUAL_A_PARAM_ID, EQUAL_B_PARAM_ID],
UNPACK_FUNCTION_ID => &[UNPACK_VALUES_PARAM_ID],
SCATTER_FUNCTION_ID => &[
SCATTER_VALUES_PARAM_ID,
SCATTER_KEYS_PARAM_ID,
SCATTER_WITHIN_PARAM_ID,
],
_ => return Ok(None),
};
if let Some(missing) = required.iter().find(|id| !node.arguments.contains_key(id)) {
return refuse(format!("argument {missing} is not linked"));
}
if node.function == SCATTER_FUNCTION_ID {
return Ok(Some(Outputs::Keys(Rc::new(scatter::Scatter::bind(node)?))));
}
if node.function != UNPACK_FUNCTION_ID {
return Ok(None);
}
let mut bound = Vec::new();
let mut seen = std::collections::HashSet::new();
for (param, expression) in &node.arguments {
if *param == UNPACK_VALUES_PARAM_ID {
continue;
}
let Some(index) = crate::nodes::unpack_out_index(*param) else {
return refuse(format!("Unpack has no port {param}"));
};
let Expression::VariableId(variable) = expression else {
return refuse(format!(
"Unpack's out_{index} is written, so it binds a variable, not {expression:?}"
));
};
let target = names
.get(variable)
.map_or_else(|| variable.to_string(), |name| format!("'{name}'"));
if !seen.insert(target.clone()) {
return refuse(format!("two of Unpack's ports write the variable {target}"));
}
bound.push(index);
}
let count = bound.iter().max().map_or(0, |last| last + 1);
let mut outputs = vec![None; count];
for index in bound {
let port = NodeParameterId {
node: node.id,
parameter: UNPACK_OUT_PARAM_IDS[index],
};
outputs[index] =
Some(get_node_parameter_variable(&port, node_parameters_variables)?.clone());
}
Ok(Some(Outputs::Ports(outputs.into())))
}
fn read_argument(expression: &Expression) -> Option<&NodeParameterId> {
match expression {
Expression::NodeArgument(source) => Some(source),
Expression::Select { source, .. } => read_argument(source),
_ => None,
}
}
fn get_node_parameter_variable<'a>(
node_parameter: &NodeParameterId,
node_parameters_variables: &'a HashMap<NodeParameterId, VariableCell>,
) -> Result<&'a VariableCell, BehaviorTreeError> {
node_parameters_variables
.get(node_parameter)
.ok_or(BehaviorTreeError::InconsistentTreeError {
message: format!("node parameter {} was not found", node_parameter),
})
}
pub(crate) fn setup_node_parameter_variable(
node_parameter: &NodeParameterId,
argument_expression: &Expression,
variables: &mut HashMap<Uuid, VariableCell>,
node_parameters_variables: &mut HashMap<NodeParameterId, VariableCell>,
resolver: &VariableResolver,
names: &HashMap<Uuid, String>,
) -> Result<VariableCell, BehaviorTreeError> {
let variable = match argument_expression {
Expression::Value(value) => VariableCell::local_with(value.to_owned()),
Expression::Uuid(uuid) => {
let value = Value::ArrayU8(uuid.as_bytes().to_vec());
VariableCell::local_with(value)
}
Expression::Variable(variable) => variable.clone(),
Expression::VariableId(variable_id) => {
variable_cell(*variable_id, variables, resolver, names)
}
Expression::NodeArgument(other_node_parameter) => node_parameters_variables
.get(other_node_parameter)
.ok_or(BehaviorTreeError::InconsistentTreeError {
message: format!(
"node argument {} used by node argument {} was not found",
other_node_parameter,
node_parameter.to_owned()
),
})?
.to_owned(),
Expression::Select { source, .. } => setup_node_parameter_variable(
node_parameter,
source,
variables,
node_parameters_variables,
resolver,
names,
)?,
Expression::Call(_) => {
bind_variables(argument_expression, variables, resolver, names);
VariableCell::local()
}
};
node_parameters_variables.insert(node_parameter.to_owned(), variable.to_owned());
Ok(variable)
}
pub(crate) fn bind_variables(
expression: &Expression,
variables: &mut HashMap<Uuid, VariableCell>,
resolver: &VariableResolver,
names: &HashMap<Uuid, String>,
) {
match expression {
Expression::VariableId(id) => {
variable_cell(*id, variables, resolver, names);
}
Expression::Select { source, .. } => bind_variables(source, variables, resolver, names),
Expression::Call(call) => {
let parts = [&call.module, &call.function].into_iter().chain(
call.arguments
.iter()
.flat_map(|(parameter, argument)| [parameter, argument]),
);
for part in parts {
bind_variables(part, variables, resolver, names);
}
}
_ => {}
}
}
pub(crate) fn variable_cell(
variable_id: Uuid,
variables: &mut HashMap<Uuid, VariableCell>,
resolver: &VariableResolver,
names: &HashMap<Uuid, String>,
) -> VariableCell {
variables
.entry(variable_id)
.or_insert_with(|| {
names
.get(&variable_id)
.filter(|name| !is_tree_local(name))
.and_then(|name| resolver(name))
.map(VariableCell::stored)
.unwrap_or_else(VariableCell::local)
})
.clone()
}
#[derive(Clone, Copy)]
pub(crate) struct Scope<'a> {
pub(crate) variables: &'a HashMap<Uuid, VariableCell>,
pub(crate) node_parameters_variables: &'a HashMap<NodeParameterId, VariableCell>,
pub(crate) calls: &'a FunctionIndex,
}
pub(crate) fn compute_expression(
scope: Scope,
expression: &Expression,
caller: &mut dyn CallBridge,
node_parameter: &NodeParameterId,
) -> Result<Value, BehaviorTreeError> {
let value = match expression {
Expression::Value(value) => value.to_owned(),
Expression::Uuid(uuid) => Value::ArrayU8(uuid.as_bytes().to_vec()),
Expression::Variable(variable) => variable.get_or_unit(),
Expression::VariableId(variable_id) => {
let variable = get_variable(scope.variables, variable_id, &node_parameter.node)?;
variable.get_or_unit()
}
Expression::Call(call) => call_expression(scope, call, caller, node_parameter)?,
Expression::NodeArgument(other_node_parameter) => {
let variable =
get_node_parameter_variable(other_node_parameter, scope.node_parameters_variables)?;
variable.get_or_unit()
}
Expression::Select { source, path } => {
let value = compute_expression(scope, source, caller, node_parameter)?;
select(path, &value)?
}
};
Ok(value)
}
fn select(path: &arora_types::data::Key, value: &Value) -> Result<Value, BehaviorTreeError> {
if *value == Value::Unit {
return Ok(Value::Unit);
}
path.select(value)
.map_err(|message| BehaviorTreeError::InconsistentTreeError { message })
}
fn compute_uuid(
scope: Scope,
expression: &Expression,
caller: &mut dyn CallBridge,
node_parameter: &NodeParameterId,
) -> Result<Uuid, BehaviorTreeError> {
match expression {
Expression::Uuid(uuid) => Ok(uuid.to_owned()),
Expression::VariableId(variable_id) => {
let variable = get_variable(scope.variables, variable_id, &node_parameter.node)?;
try_into_uuid(&variable.get_or_unit(), &Some(variable_id))
}
expression => try_into_uuid(
&compute_expression(scope, expression, caller, node_parameter)?,
&None,
),
}
}
fn try_into_uuid(value: &Value, variable_id: &Option<&Uuid>) -> Result<Uuid, BehaviorTreeError> {
if let Value::ArrayU8(uuid_bytes) = value {
let uuid = Uuid::from_slice(uuid_bytes.as_slice()).map_err(|e| {
BehaviorTreeError::ConversionError(ConversionError {
message: format!(
"bytes of variable {:?} are not an uuid ({})",
variable_id, e
),
})
})?;
Ok(uuid)
} else {
Err(BehaviorTreeError::ConversionError(ConversionError {
message: format!("variable {:?} is not an uuid", variable_id),
}))
}
}
pub(crate) fn written_cell(scope: Scope, argument: &Expression) -> Option<VariableCell> {
match argument {
Expression::Variable(cell) => Some(cell.clone()),
Expression::VariableId(id) => scope.variables.get(id).cloned(),
Expression::NodeArgument(at) => scope.node_parameters_variables.get(at).cloned(),
_ => None,
}
}
fn call_expression(
scope: Scope,
call: &CallExpression,
caller: &mut dyn CallBridge,
node_parameter: &NodeParameterId,
) -> Result<Value, BehaviorTreeError> {
let module_id = compute_uuid(scope, &call.module, caller, node_parameter)?;
let function_id = compute_uuid(scope, &call.function, caller, node_parameter)?;
if is_interpreter_function_id(function_id) {
return Err(BehaviorTreeError::InconsistentTreeError {
message: format!(
"{node_parameter}: {} calls the interpreter module's function {function_id}, \
which a tree does not call",
Expression::Call(Box::new(call.clone())).display(Names::functions(scope.calls))
),
});
}
let function = scope.calls.get(&function_id).map(|f| &f.function);
let mut args = Vec::with_capacity(call.arguments.len());
let mut written = Vec::new();
let mut unwritable = Vec::new();
for (arg_id_expression, value_expression) in &call.arguments {
let arg_id = compute_uuid(scope, arg_id_expression, caller, node_parameter)?;
let value = compute_expression(scope, value_expression, caller, node_parameter)?;
let optional = function.is_some_and(|function| is_optional(function, &arg_id));
match written_cell(scope, value_expression) {
Some(cell) => written.push((arg_id, cell, optional)),
None => unwritable.push(arg_id),
}
args.push(StructureField {
id: arg_id,
value: Box::new(argument_value(
Some(value).filter(|value| *value != Value::Unit),
optional,
)),
});
}
let result = caller
.arora_call(Call {
module_id: Some(module_id),
id: function_id,
args,
})
.map_err(BehaviorTreeError::CallError)?;
for mutated in result.mutated {
if let Some((_, cell, optional)) = written.iter().find(|(id, ..)| *id == mutated.id) {
write_back(cell, *mutated.value, *optional);
} else if unwritable.contains(&mutated.id) {
return Err(BehaviorTreeError::InconsistentTreeError {
message: format!(
"{node_parameter}: {} writes back parameter {}, whose argument, a selection \
or a call, has no cell to hold it",
Expression::Call(Box::new(call.clone())).display(Names::functions(scope.calls)),
mutated.id
),
});
}
}
Ok(result.ret)
}
pub(crate) fn is_interpreter_function(function: &ModuleFunction) -> bool {
use arora_behavior::interpreter_module::{EDIT, HALT, ID, LOAD, SPAWN, SPAWN_GRAPH};
function.module_id == ID
&& [LOAD, EDIT, SPAWN, SPAWN_GRAPH, HALT].contains(&function.function_id)
}
pub(crate) fn is_interpreter_function_id(function: Uuid) -> bool {
use arora_behavior::interpreter_module::{EDIT, HALT, LOAD, SPAWN, SPAWN_GRAPH};
[LOAD, EDIT, SPAWN, SPAWN_GRAPH, HALT].contains(&function)
}
fn calls_in<'e>(expression: &'e Expression, found: &mut Vec<&'e CallExpression>) {
match expression {
Expression::Call(call) => {
found.push(call);
calls_in(&call.module, found);
calls_in(&call.function, found);
for (parameter, argument) in &call.arguments {
calls_in(parameter, found);
calls_in(argument, found);
}
}
Expression::Select { source, .. } => calls_in(source, found),
_ => {}
}
}
fn call_signatures(
node: &Node,
functions: &FunctionIndex,
) -> Result<FunctionIndex, BehaviorTreeError> {
let mut signatures = expression_call_signatures(
&format!("node {}", node.id),
node.arguments.values(),
functions,
)?;
if let Some(function) = functions.get(&node.function) {
signatures.insert(node.function, function.clone());
}
Ok(signatures)
}
pub(crate) fn expression_call_signatures<'e>(
owner: &str,
arguments: impl IntoIterator<Item = &'e Expression>,
functions: &FunctionIndex,
) -> Result<FunctionIndex, BehaviorTreeError> {
let mut signatures = FunctionIndex::new();
let mut calls = Vec::new();
for argument in arguments {
calls_in(argument, &mut calls);
}
let names = Names::functions(functions);
let refuse = |call: &CallExpression, why: String| {
Err(BehaviorTreeError::InconsistentTreeError {
message: format!(
"{owner}: {} {why}",
Expression::Call(Box::new(call.clone())).display(names)
),
})
};
for call in calls {
let Some(function_id) = stated_id(&call.function) else {
continue;
};
if is_interpreter_function_id(function_id) {
return refuse(
call,
"calls the interpreter module, which a tree does not call".to_string(),
);
}
let Some(signature) = functions.get(&function_id) else {
continue;
};
for (parameter, argument) in &call.arguments {
let Some(parameter_id) = stated_id(parameter) else {
continue;
};
let Some(declared) = signature.function.parameters.get(¶meter_id) else {
return refuse(
call,
format!(
"names the parameter {parameter_id}, which {} does not have",
signature.function_name
),
);
};
if declared.mutable
&& matches!(argument, Expression::Select { .. } | Expression::Call(_))
{
return refuse(
call,
format!(
"feeds the mutable parameter `{}` with {}: what the function writes \
back needs a literal, a variable or a node argument to hold it",
declared.name,
argument.display(names)
),
);
}
}
signatures.insert(function_id, signature.clone());
}
Ok(signatures)
}
pub(crate) fn with_call_site_cells(expression: Expression) -> Expression {
let identifier = |expression: Expression| match expression {
Expression::Call(_) | Expression::Select { .. } => with_call_site_cells(expression),
expression => expression,
};
match expression {
Expression::Call(call) => {
let CallExpression {
module,
function,
arguments,
} = *call;
Expression::Call(Box::new(CallExpression {
module: identifier(module),
function: identifier(function),
arguments: arguments
.into_iter()
.map(|(parameter, argument)| {
let parameter = identifier(parameter);
let argument = match argument {
Expression::Value(value) => {
Expression::Variable(VariableCell::local_with(value))
}
Expression::Uuid(id) => Expression::Variable(VariableCell::local_with(
Value::ArrayU8(id.as_bytes().to_vec()),
)),
argument => with_call_site_cells(argument),
};
(parameter, argument)
})
.collect(),
}))
}
Expression::Select { source, path } => Expression::Select {
source: Box::new(with_call_site_cells(*source)),
path,
},
expression => expression,
}
}
fn take_call_site_cells(expression: &Expression, held: &Expression) {
match (expression, held) {
(Expression::Call(call), Expression::Call(held)) => {
take_call_site_cells(&call.module, &held.module);
take_call_site_cells(&call.function, &held.function);
for ((parameter, argument), (held_parameter, held)) in
call.arguments.iter().zip(&held.arguments)
{
take_call_site_cells(parameter, held_parameter);
take_call_site_cells(argument, held);
}
}
(Expression::Select { source, .. }, Expression::Select { source: held, .. }) => {
take_call_site_cells(source, held)
}
(Expression::Variable(cell), Expression::Variable(held)) => take_local(cell, held),
_ => {}
}
}