3628802fc8
This commit reorganizes the stream-related logic by splitting the `streams.rs` file into smaller, more manageable modules: `nodes.rs`, `hooks.rs`, and `register.rs`. The `nodes.rs` module now contains the core stream implementations like `RootStream`, `PipeStream`, and the associated observer patterns. The `hooks.rs` module encapsulates the compiler hook logic for `pipe` and `pipe-series`, including type resolution and argument hinting. It also includes helper functions for building pipe executors and extracting stream information. The `register.rs` module handles the registration of stream-related built-in functions (`create-random-ohlc`, `create-ticker`, `pipe`, `pipe-series`) within the environment. This refactoring improves code organization, maintainability, and testability by separating concerns into dedicated modules.
150 lines
4.5 KiB
Rust
150 lines
4.5 KiB
Rust
use super::*;
|
|
use super::nodes::{PipeStream, RootStream};
|
|
use crate::ast::types::{PipeFn, 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() {
|
|
// 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);
|
|
}
|
|
|
|
/// Validates the wrapper-executor pattern for `pipe-series`:
|
|
/// A closure wraps push + fill gate + user lambda, reusing standard PipeStream.
|
|
#[test]
|
|
fn test_buffered_pipe_fill_gate() {
|
|
use crate::ast::rtl::series::data::ScalarSeries;
|
|
use crate::ast::types::SeriesStorage;
|
|
|
|
let root = RootStream::new();
|
|
|
|
// Internal series with lookback 3 (simulates what pipe-series creates)
|
|
let series: Rc<dyn SeriesStorage> =
|
|
Rc::new(ScalarSeries::<f64>::new("FloatSeries", 3));
|
|
let series_clone = Rc::clone(&series);
|
|
|
|
// Wrapper-executor: push → fill gate → user lambda (s[0] + s[1])
|
|
let mut fill_gate_open = false;
|
|
let lookback: usize = 3;
|
|
let wrapper: Box<PipeFn> = Box::new(move |args: &[Value]| {
|
|
// 1. Push incoming value into internal series
|
|
series_clone
|
|
.as_pushable()
|
|
.unwrap()
|
|
.push_value(args[0].clone());
|
|
|
|
// 2. Fill gate: wait until series has enough data
|
|
if !fill_gate_open {
|
|
if series_clone.len() >= lookback {
|
|
fill_gate_open = true;
|
|
} else {
|
|
return Value::Void; // Filtered by PipeStream::notify
|
|
}
|
|
}
|
|
|
|
// 3. User lambda: s[0] + s[1] (two most recent values)
|
|
let v0 = series_clone.get_item(0).unwrap();
|
|
let v1 = series_clone.get_item(1).unwrap();
|
|
if let (Value::Float(a), Value::Float(b)) = (&v0, &v1) {
|
|
Value::Float(a + b)
|
|
} else {
|
|
Value::Void
|
|
}
|
|
});
|
|
|
|
// Wire up: RootStream → PipeStream with wrapper executor (manual wiring)
|
|
let pipe = Rc::new(RefCell::new(PipeStream::new_typed(
|
|
"buffered-test".to_string(),
|
|
1,
|
|
Some(wrapper),
|
|
StaticType::Float,
|
|
)));
|
|
root.add_observer(pipe.clone());
|
|
|
|
// Tick 1: series has 1 element → fill gate closed → Void → no signal
|
|
root.tick(Value::Float(10.0));
|
|
assert!(
|
|
pipe.borrow().current_signal().is_none(),
|
|
"Tick 1: fill gate should block (1 < 3)"
|
|
);
|
|
|
|
// Tick 2: series has 2 elements → still blocked
|
|
root.tick(Value::Float(20.0));
|
|
assert!(
|
|
pipe.borrow().current_signal().is_none(),
|
|
"Tick 2: fill gate should block (2 < 3)"
|
|
);
|
|
|
|
// Tick 3: series has 3 elements → fill gate opens → s[0]+s[1] = 30+20 = 50
|
|
root.tick(Value::Float(30.0));
|
|
let sig3 = pipe.borrow().current_signal().unwrap();
|
|
assert_eq!(sig3.cycle_id, 3);
|
|
assert!(
|
|
matches!(sig3.value, Value::Float(v) if (v - 50.0).abs() < f64::EPSILON),
|
|
"Tick 3: expected 50.0 (30+20), got {:?}",
|
|
sig3.value
|
|
);
|
|
|
|
// Tick 4: fill gate stays open → s[0]+s[1] = 40+30 = 70
|
|
root.tick(Value::Float(40.0));
|
|
let sig4 = pipe.borrow().current_signal().unwrap();
|
|
assert_eq!(sig4.cycle_id, 4);
|
|
assert!(
|
|
matches!(sig4.value, Value::Float(v) if (v - 70.0).abs() < f64::EPSILON),
|
|
"Tick 4: expected 70.0 (40+30), got {:?}",
|
|
sig4.value
|
|
);
|
|
}
|