use std::any::Any; use std::mem::MaybeUninit; use std::ops::Add; use core_types::arena::{Arena, ArenaCell}; use core_types::context::{ContextImpl, Ctx, EvalScope, ExtractArena, InjectIndex}; use core_types::gnode::{BatchStatus, GNode, StatusCell}; use core_types::gpoll::{ErrorKind, Finality, GPoll, Interrupt}; fn add, B, C: Ctx>(_ctx: &C, augend: A, addend: B) -> >::Output { augend + addend } struct AddNode { augend: Node0, addend: Node1, } impl AddNode { fn new(augend: Node0, addend: Node1) -> Self { Self { augend, addend } } } impl GNode for AddNode where A: Add, Input: Ctx, Node0: GNode, Node1: GNode, { type Output = >::Output; fn eval(&self, input: &Input) -> GPoll { let cell = StatusCell::new(); let augend = match cell.eval_input(0, &self.augend, input) { Ok(value) => value, Err(interrupt) => return interrupt.into(), }; let addend = match cell.eval_input(1, &self.addend, input) { Ok(value) => value, Err(interrupt) => return interrupt.into(), }; cell.finish(add(input, augend, addend)) } } struct ValueNode(T); impl GNode for ValueNode { type Output = T; fn eval(&self, _input: &Input) -> GPoll { GPoll::Final(self.0.clone()) } } struct ReadIndexNode; impl GNode for ReadIndexNode { type Output = f64; fn eval(&self, input: &Input) -> GPoll { GPoll::Final(input.index_value() as f64) } } trait ExtractIndexValue { fn index_value(&self) -> u64; } impl ExtractIndexValue for ContextImpl<'_> { fn index_value(&self) -> u64 { self.index_head().index } } fn string_length(_ctx: &C, value: &String) -> f64 { value.len() as f64 } struct LendStringNode { value: String, cell: ArenaCell, } impl LendStringNode { fn new(value: String) -> Self { Self { value, cell: ArenaCell::new(), } } } impl<'e, Input> GNode for LendStringNode where Input: Ctx + ExtractArena, { type Output = &'e String; fn eval(&self, input: &Input) -> GPoll<&'e String> { let arena = input.arena(); if let Some(value) = self.cell.load(arena) { return GPoll::Final(value); } match arena.alloc(self.value.clone()) { Some((value, weak)) => { self.cell.store(weak); GPoll::Final(value) } None => GPoll::arena_exhausted(), } } } struct StringLengthNode { value: Node0, } impl StringLengthNode { fn new(value: Node0) -> Self { Self { value } } } impl<'e, Input, Node0> GNode for StringLengthNode where Input: Ctx, Node0: GNode, { type Output = f64; fn eval(&self, input: &Input) -> GPoll { let cell = StatusCell::new(); let value = match cell.eval_input(0, &self.value, input) { Ok(value) => value, Err(interrupt) => return interrupt.into(), }; cell.finish(string_length(input, value)) } } type ErasedGNode = dyn for<'c> GNode, Output = T>; type ErasedLendEdge = dyn for<'c> GNode, Output = &'c String>; fn string_length_constructor(args: Vec>) -> Result>, &'static str> { let mut args = args.into_iter(); let value = *args.next().ok_or("arity")?.downcast::>().map_err(|_| "type")?; Ok(Box::new(StringLengthNode::new(value))) } fn add_constructor_f64(args: Vec>) -> Result>, &'static str> { let mut args = args.into_iter(); let augend = *args.next().ok_or("arity")?.downcast::>>().map_err(|_| "type")?; let addend = *args.next().ok_or("arity")?.downcast::>>().map_err(|_| "type")?; Ok(Box::new(AddNode::new(augend, addend))) } fn scope_fixture<'a>(generations: &'a [(u64, u64)], arena: &'a Arena) -> EvalScope<'a> { EvalScope::new(Some(0.5), None, None, generations, arena) } #[test] fn hand_expansion_evaluates_through_typed_erased_edges() { let arena = Arena::new(1024); let generations = []; let scope = scope_fixture(&generations, &arena); let ctx = ContextImpl::root(&scope); let augend: Box = Box::new(Box::new(ValueNode(1.0f64)) as Box>); let addend: Box = Box::new(Box::new(ValueNode(2.0f64)) as Box>); let wired = add_constructor_f64(vec![augend, addend]).unwrap(); assert_eq!(wired.eval(&ctx), GPoll::Final(3.0)); } #[test] fn wiring_rejects_type_and_arity_mismatches() { let augend: Box = Box::new(Box::new(ValueNode(1.0f64)) as Box>); let addend: Box = Box::new(Box::new(ValueNode(2u32)) as Box>); assert_eq!(add_constructor_f64(vec![augend, addend]).map(|_| ()), Err("type")); let augend: Box = Box::new(Box::new(ValueNode(1.0f64)) as Box>); assert_eq!(add_constructor_f64(vec![augend]).map(|_| ()), Err("arity")); } #[test] fn spec_loop_batches_through_the_erased_edge() { let arena = Arena::new(1024); let generations = []; let scope = scope_fixture(&generations, &arena); let ctx = ContextImpl::root(&scope); let graph: Box> = Box::new(AddNode::new(ReadIndexNode, ValueNode(10.0f64))); let mut scratch = [const { MaybeUninit::uninit() }; 4]; let status = graph.eval_batch(&ctx, 2..6, Some(&mut scratch)); let BatchStatus::Filled(lanes, finality) = status else { panic!("expected filled, got {status:?}"); }; assert_eq!(lanes, &[12.0, 13.0, 14.0, 15.0]); assert_eq!(finality, Finality::AllFinal); } #[test] fn lending_kernel_clones_once_per_generation_and_lends_after() { let arena = Arena::new(1024); let generations = []; let scope = scope_fixture(&generations, &arena); let ctx = ContextImpl::root(&scope); let node = LendStringNode::new("lend me".to_string()); let GPoll::Final(first) = node.eval(&ctx) else { panic!("first eval must clone into the arena and lend"); }; let GPoll::Final(second) = node.eval(&ctx) else { panic!("second eval must hit the cell"); }; assert_eq!(first, "lend me"); assert!(std::ptr::eq(first, second)); } #[test] fn exhausted_arena_reports_the_operational_error() { let arena = Arena::new(0); let generations = []; let scope = scope_fixture(&generations, &arena); let ctx = ContextImpl::root(&scope); let node = LendStringNode::new("too big".to_string()); let GPoll::Error(error) = node.eval(&ctx) else { panic!("exhaustion must surface as an operational error"); }; assert_eq!(error.kind, ErrorKind::ArenaExhausted); } #[test] fn lending_edges_erase_and_wire_like_owned_edges() { let arena = Arena::new(1024); let generations = []; let scope = scope_fixture(&generations, &arena); let ctx = ContextImpl::root(&scope); let value: Box = Box::new(Box::new(LendStringNode::new("across the boundary".to_string())) as Box); let wired = string_length_constructor(vec![value]).unwrap(); assert_eq!(wired.eval(&ctx), GPoll::Final(19.0)); } #[test] fn spec_loop_batches_through_the_erased_lending_edge() { let arena = Arena::new(1024); let generations = []; let scope = scope_fixture(&generations, &arena); let ctx = ContextImpl::root(&scope); let graph: Box = Box::new(LendStringNode::new("batched".to_string())); let mut scratch = [const { MaybeUninit::uninit() }; 3]; let status = graph.eval_batch(&ctx, 0..3, Some(&mut scratch)); let BatchStatus::Filled(lanes, finality) = status else { panic!("expected filled, got {status:?}"); }; assert_eq!(lanes.len(), 3); assert!(lanes.iter().all(|lane| std::ptr::eq(*lane, lanes[0]))); assert_eq!(*lanes[0], "batched"); assert_eq!(finality, Finality::AllFinal); } #[test] fn fallback_input_records_partiality_invisibly() { struct FallbackNode; impl GNode for FallbackNode { type Output = f64; fn eval(&self, _input: &Input) -> GPoll { GPoll::fallback(0.0, "upstream failed") } } let arena = Arena::new(1024); let generations = []; let scope = scope_fixture(&generations, &arena); let ctx = ContextImpl::root(&scope); let graph = AddNode::new(FallbackNode, ValueNode(5.0f64)); let GPoll::Fallback(boxed) = graph.eval(&ctx) else { panic!("fallback must propagate with the computed stand-in"); }; assert_eq!(boxed.0, 5.0); assert!(boxed.1.kind == "upstream failed"); assert_eq!(boxed.1.trace, vec![0]); }