use runa_core::{ActorRef, MAX_VALUE_BYTES, MAX_VALUE_DEPTH, MAX_VALUE_NODES, Value}; use rune::runtime::{Object, Vec as RuneVec}; use rune::{Any, TypeHash, Value as RuneValue}; use crate::ExperimentResult; #[derive(Any, Clone)] #[rune(item = ::runa)] pub(crate) struct ActorReference(pub ActorRef); /// Allocate a fresh Rune object graph for each invocation input. pub fn value_to_rune(value: &Value) -> ExperimentResult { value.validate()?; fn convert(value: &Value) -> ExperimentResult { Ok(match value { Value::Null => RuneValue::from(()), Value::Bool(value) => rune::to_value(*value)?, Value::Integer(value) => rune::to_value(*value)?, Value::String(value) => rune::to_value(value.as_str())?, Value::ActorRef(value) => RuneValue::new(ActorReference(*value))?, Value::List(values) => { let mut result = RuneVec::with_capacity(values.len())?; for value in values { result.push(convert(value)?)?; } RuneValue::new(result)? } Value::Record(values) => { let mut result = Object::with_capacity(values.len())?; for (key, value) in values { result.insert( rune::alloc::String::try_from(key.as_str())?, convert(value)?, )?; } RuneValue::new(result)? } }) } convert(value) } /// Copy only portable data out of Rune; cycles and unsupported objects fail. pub fn value_from_rune(value: &RuneValue) -> ExperimentResult { fn convert( value: &RuneValue, depth: usize, nodes: &mut usize, bytes: &mut usize, ) -> ExperimentResult { *nodes += 1; if depth > MAX_VALUE_DEPTH || *nodes > MAX_VALUE_NODES { return Err("Rune actor value exceeds depth or node limit (possibly cyclic)".into()); } let hash = value.type_hash(); let result = if value.into_unit().is_ok() { Value::Null } else if hash == bool::HASH { Value::Bool(value.as_bool()?) } else if hash == i64::HASH { Value::Integer(value.as_integer()?) } else if hash == rune::alloc::String::HASH { let value = value.borrow_string_ref()?; *bytes += value.len(); if *bytes > MAX_VALUE_BYTES { return Err("Rune actor value exceeds string byte limit".into()); } Value::String(value.to_string()) } else if hash == ActorReference::HASH { Value::ActorRef(value.borrow_ref::()?.0) } else if hash == RuneVec::HASH { let values = value.borrow_ref::()?; let mut result = Vec::new(); for value in values.iter() { result.push(convert(value, depth + 1, nodes, bytes)?); } Value::List(result) } else if hash == Object::HASH { let values = value.borrow_ref::()?; let mut result = std::collections::BTreeMap::new(); for (key, value) in values.iter() { *bytes += key.len(); if *bytes > MAX_VALUE_BYTES { return Err("Rune actor value exceeds string byte limit".into()); } result.insert(key.to_string(), convert(value, depth + 1, nodes, bytes)?); } Value::Record(result) } else { return Err(format!("unsupported Rune actor value: {}", value.type_info()).into()); }; Ok(result) } convert(value, 0, &mut 0, &mut 0) } #[cfg(test)] mod tests { use super::*; use crate::CompiledProgram; use std::collections::BTreeMap; #[test] fn portable_values_round_trip_without_turning_strings_into_references() -> ExperimentResult<()> { let actor = ActorRef::new(); let value = Value::Record(BTreeMap::from([( "values".to_owned(), Value::List(vec![ Value::Null, Value::Bool(true), Value::Integer(i64::MIN), Value::Integer(i64::MAX), Value::String("hello world".to_owned()), Value::ActorRef(actor), Value::String(actor.to_string()), Value::List(vec![]), Value::Record(BTreeMap::new()), ]), )])); assert_eq!(value_from_rune(&value_to_rune(&value)?)?, value); Ok(()) } #[test] fn rune_mutations_do_not_alias_host_values_or_other_invocation_inputs() -> ExperimentResult<()> { let original = Value::Record(BTreeMap::from([( "items".to_owned(), Value::List(vec![Value::Integer(1)]), )])); let program = CompiledProgram::compile("pub fn change(state) { state.items.push(2); state }")?; let first = value_to_rune(&original)?; let second = value_to_rune(&original)?; let result = program.vm().call(["change"], (first,))?; assert_eq!(value_from_rune(&second)?, original); assert_ne!(value_from_rune(&result)?, original); Ok(()) } #[test] fn references_can_be_compared_cloned_and_retained_in_rune_records() -> ExperimentResult<()> { let actor = ActorRef::new(); let other = ActorRef::new(); let program = CompiledProgram::compile( "pub fn check(left, right) { #{same: left == right, reference: left.clone()} }", )?; for (right, expected) in [(actor, true), (other, false)] { let result = program.vm().call( ["check"], ( value_to_rune(&Value::ActorRef(actor))?, value_to_rune(&Value::ActorRef(right))?, ), )?; assert_eq!( value_from_rune(&result)?, Value::Record(BTreeMap::from([ ("same".to_owned(), Value::Bool(expected)), ("reference".to_owned(), Value::ActorRef(actor)), ])) ); } Ok(()) } #[test] fn cyclic_and_unsupported_rune_values_are_explicit_errors() -> ExperimentResult<()> { for source in [ "pub fn make() { 1.5 }", "pub fn make() { (1, 2) }", "pub fn make() { Some(1) }", "pub fn make() { let x = []; x.push(x); x }", ] { let program = CompiledProgram::compile(source)?; let value = program.vm().call(["make"], ())?; assert!(value_from_rune(&value).is_err(), "{source}"); } Ok(()) } }