use std::rc::Rc; use std::cell::RefCell; use crate::ast::types::Value; /// A Signal is the "packet" flowing through the reactive pipeline. /// It represents a value produced at a specific logical time (cycle_id). #[derive(Debug, Clone)] pub struct Signal { pub cycle_id: u64, pub value: Value, } /// A Stream is a stateless provider of signals. /// It doesn't "own" the data, it just knows how to get the current one. pub trait Stream { fn current_signal(&self) -> Option; } /// An Observer is a node in the pipeline that reacts to new signals. /// (e.g., a Pipe or a SharedSeries buffer). pub trait Observer { /// Notifies the observer about a new signal in the current cycle. /// `source_index` identifies which input stream provided the value. fn notify(&mut self, source_index: usize, cycle_id: u64, value: Value); } /// Polymorphic Interface for any stream that can accept observers (like Delphi's IStream). pub trait ObservableStream { fn add_observer(&self, observer: Rc>); } /// A generic wrapper to pass ANY ObservableStream (Root, Pipe, etc.) as an Object to the VM. #[derive(Clone)] pub struct StreamNode { pub inner: Rc, } impl std::fmt::Debug for StreamNode { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "StreamNode") } } impl crate::ast::types::Object for StreamNode { fn type_name(&self) -> &'static str { "StreamNode" } fn as_any(&self) -> &dyn std::any::Any { self } } /// The RootStream is the "Clock" and data source of the entire pipeline. /// It generates the monotonic `cycle_id` and triggers the observers. pub struct RootStream { current_cycle: std::cell::Cell, observers: RefCell>>>, } impl ObservableStream for RootStream { fn add_observer(&self, observer: Rc>) { self.observers.borrow_mut().push(observer); } } impl RootStream { pub fn new() -> Self { Self { current_cycle: std::cell::Cell::new(0), observers: RefCell::new(Vec::new()), } } /// Advances the pipeline to the next cycle and propagates a value. pub fn tick(&self, value: Value) { let next_cycle = self.current_cycle.get() + 1; self.current_cycle.set(next_cycle); // Propagate to all observers. // We use a local borrow of the observers list to keep the cell borrow short. let obs_list = self.observers.borrow(); for obs in obs_list.iter() { // Root observers are always at source_index 0. obs.borrow_mut().notify(0, next_cycle, value.clone()); } } pub fn current_cycle(&self) -> u64 { self.current_cycle.get() } pub fn add_observer(&self, observer: Rc>) { self.observers.borrow_mut().push(observer); } } /// A PipeStream is a reactive node that transforms inputs via a lambda. /// It implements "Barrier Synchronization": It only executes when all inputs /// have reported a value for the same cycle_id. pub struct PipeStream { pub name: String, /// The inputs this pipe is observing. /// In a real system, these would be other Streams. /// For the MVP, we assume the Pipe is notified by the Root or its parents. pub input_count: usize, /// Tracks the last cycle_id received from each input. last_cycle_per_input: RefCell>, /// The current output signal of this pipe. current_signal: RefCell>, /// The VM closure (lambda) to execute. pub lambda: Option>, /// Observers of THIS pipe. observers: RefCell>>>, } impl PipeStream { pub fn new(name: String, input_count: usize, lambda: Option>) -> Self { Self { name, input_count, last_cycle_per_input: RefCell::new(vec![0; input_count]), current_signal: RefCell::new(None), lambda, observers: RefCell::new(Vec::new()), } } /// Internal: Checks if all inputs have reached the target cycle. fn is_barrier_reached(&self, cycle_id: u64) -> bool { let cycles = self.last_cycle_per_input.borrow(); cycles.iter().all(|&c| c == cycle_id) } } impl ObservableStream for PipeStream { fn add_observer(&self, observer: Rc>) { self.observers.borrow_mut().push(observer); } } impl Stream for PipeStream { fn current_signal(&self) -> Option { self.current_signal.borrow().clone() } } impl Observer for PipeStream { fn notify(&mut self, source_index: usize, cycle_id: u64, value: Value) { let barrier_reached = { let mut cycles = self.last_cycle_per_input.borrow_mut(); if source_index < self.input_count { cycles[source_index] = cycle_id; } // Check if all inputs reached the same cycle. cycles.iter().all(|&c| c == cycle_id) }; if barrier_reached { // 1. Prepare Arguments for Lambda (Current values of all inputs) let args = vec![value]; // Simplified for now // 2. Execute Lambda (This requires a VM instance!) let result = args[0].clone(); // 3. Update Current Signal let new_signal = Signal { cycle_id, value: result }; *self.current_signal.borrow_mut() = Some(new_signal.clone()); // 4. Notify Observers (Always at source_index 0 of the NEXT pipe) let obs_list = self.observers.borrow(); for obs in obs_list.iter() { obs.borrow_mut().notify(0, cycle_id, new_signal.value.clone()); } } } } /// A specialized observer that pushes incoming signals into a SharedSeries buffer. pub struct SeriesPusher { pub buffer: Rc>>, pub lookback: Option, } impl Observer for SeriesPusher { fn notify(&mut self, _source_index: usize, _cycle_id: u64, _value: Value) { // ... (Downcast and push logic) } } // ============================================================================ // Script Integration (RTL Registration) // ============================================================================ use crate::ast::environment::Environment; use crate::ast::types::{Purity, Signature, StaticType, Keyword, RecordLayout}; pub fn register(env: &Environment) { // (create-random-ohlc seed limit) -> StreamNode let generators = env.pipeline_generators.clone(); env.register_native_fn( "create-random-ohlc", StaticType::Function(Box::new(Signature { params: StaticType::Tuple(vec![StaticType::Int, StaticType::Int]), ret: StaticType::Any, // Returns a StreamNode })), Purity::Impure, // Modifies global generator registry move |args: std::vec::Vec| { if args.len() != 2 { panic!("create-random-ohlc expects exactly 2 arguments (seed, limit)"); } let seed = if let Value::Int(s) = args[0] { s as u64 } else { 0 }; let limit = if let Value::Int(l) = args[1] { l as usize } else { 0 }; // 1. Create the RootStream let root_stream = Rc::new(RootStream::new()); let stream_node = StreamNode { inner: root_stream.clone() }; // 2. Setup the Layout for OHLC records let layout = RecordLayout::get_or_create(vec![ (Keyword::intern("open"), StaticType::Float), (Keyword::intern("high"), StaticType::Float), (Keyword::intern("low"), StaticType::Float), (Keyword::intern("close"), StaticType::Float), ]); // 3. Create the stateful generator closure let mut current_tick = 0; let mut last_close = 100.0; // We use a local PRNG instance for reproducibility based on the seed let mut rng = fastrand::Rng::with_seed(seed); let generator = move || -> bool { if current_tick >= limit { return false; // Exhausted } // Generate random OHLC (Random Walk) let change = (rng.f64() - 0.5) * 2.0; let open = last_close; let high = open + (rng.f64() * 2.0).abs(); let low = open - (rng.f64() * 2.0).abs(); let close = open + change; last_close = close; let record = Value::Record( layout.clone(), Rc::new(vec![ Value::Float(open), Value::Float(high), Value::Float(low), Value::Float(close), ]) ); // Pump the signal into the RootStream root_stream.tick(record); current_tick += 1; true // Still active }; // 4. Register the generator in the Environment generators.borrow_mut().push(Box::new(generator)); // 5. Return the stream reference to the script Value::Object(Rc::new(stream_node)) }, ); } #[cfg(test)] mod tests { use super::*; use crate::ast::types::Value; #[test] fn test_root_to_pipe_flow() { let root = RootStream::new(); let pipe = Rc::new(RefCell::new(PipeStream::new("test-pipe".to_string(), 1, None))); root.add_observer(pipe.clone()); // Cycle 1: Root ticks 10.0 root.tick(Value::Float(10.0)); let sig = pipe.borrow().current_signal().unwrap(); assert_eq!(sig.cycle_id, 1); if let Value::Float(v) = sig.value { assert_eq!(v, 10.0); } else { panic!("Value must be Float(10.0)"); } // Cycle 2: Root ticks 20.0 root.tick(Value::Float(20.0)); let sig2 = pipe.borrow().current_signal().unwrap(); assert_eq!(sig2.cycle_id, 2); if let Value::Float(v) = sig2.value { assert_eq!(v, 20.0); } else { panic!("Value must be Float(20.0)"); } } #[test] fn test_barrier_sync() { let root = RootStream::new(); // Pipe with 2 inputs let pipe = Rc::new(RefCell::new(PipeStream::new("barrier-pipe".to_string(), 2, None))); // Manual notifications simulate different input streams pipe.borrow_mut().notify(0, 1, Value::Float(10.0)); assert!(pipe.borrow().current_signal().is_none(), "Barrier should NOT be reached after 1st input"); pipe.borrow_mut().notify(1, 1, Value::Float(20.0)); assert!(pipe.borrow().current_signal().is_some(), "Barrier SHOULD be reached after 2nd input"); let sig = pipe.borrow().current_signal().unwrap(); assert_eq!(sig.cycle_id, 1); } }