Add Series RTL and SeriesView
Introduces a new RTL module for handling series data, enabling efficient storage and retrieval of time-series data in a Struct-of-Arrays (SoA) format. Includes `SeriesView`, a zero-copy wrapper that provides a fast, read-only interface to individual columns within a `RecordSeries`, optimizing performance for data access in the VM. Registers new native functions `create-series` and `push-series` for managing series data.
This commit is contained in:
@@ -11,4 +11,5 @@ pub fn register(env: &Environment) {
|
||||
core::register(env);
|
||||
datetime::register(env);
|
||||
math::register(env);
|
||||
series::register(env);
|
||||
}
|
||||
+156
-4
@@ -161,12 +161,56 @@ impl Object for ValueSeries {
|
||||
/// An interface allowing iteration over fields of a RecordSeries independently
|
||||
/// of the exact type (Type Erasure for member arrays).
|
||||
pub trait SeriesMember: Object {
|
||||
// We could add type-safe `push_any(&mut self, Value)` here later,
|
||||
// if the fallback path of the VM needs to insert values dynamically.
|
||||
/// Gets the value at the specified lookback index as a dynamic Value.
|
||||
fn get_value(&self, index: usize) -> Option<Value>;
|
||||
|
||||
/// Pushes a dynamic Value into the series, performing the necessary downcast.
|
||||
fn push_value(&mut self, value: Value, limit: Option<usize>);
|
||||
}
|
||||
|
||||
impl<T: ScalarValue> SeriesMember for ScalarSeries<T> {}
|
||||
impl SeriesMember for ValueSeries {}
|
||||
impl SeriesMember for ScalarSeries<f64> {
|
||||
fn get_value(&self, index: usize) -> Option<Value> {
|
||||
self.data.get(index).map(|&v| Value::Float(v))
|
||||
}
|
||||
fn push_value(&mut self, value: Value, limit: Option<usize>) {
|
||||
if let Value::Float(v) = value {
|
||||
self.data.push(v, limit);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SeriesMember for ScalarSeries<i64> {
|
||||
fn get_value(&self, index: usize) -> Option<Value> {
|
||||
self.data.get(index).map(|&v| Value::Int(v))
|
||||
}
|
||||
fn push_value(&mut self, value: Value, limit: Option<usize>) {
|
||||
if let Value::Int(v) = value {
|
||||
self.data.push(v, limit);
|
||||
} else if let Value::DateTime(v) = value {
|
||||
self.data.push(v, limit);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SeriesMember for ScalarSeries<bool> {
|
||||
fn get_value(&self, index: usize) -> Option<Value> {
|
||||
self.data.get(index).map(|&v| Value::Bool(v))
|
||||
}
|
||||
fn push_value(&mut self, value: Value, limit: Option<usize>) {
|
||||
if let Value::Bool(v) = value {
|
||||
self.data.push(v, limit);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SeriesMember for ValueSeries {
|
||||
fn get_value(&self, index: usize) -> Option<Value> {
|
||||
self.data.get(index).cloned()
|
||||
}
|
||||
fn push_value(&mut self, value: Value, limit: Option<usize>) {
|
||||
self.data.push(value, limit);
|
||||
}
|
||||
}
|
||||
|
||||
/// The "Struct of Arrays" implementation for Records (e.g., Ticks).
|
||||
/// Instead of holding a large array of Records (AoS), this series
|
||||
@@ -223,3 +267,111 @@ impl Object for RecordSeries {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 4. Series View (Safe, 0-Copy access bridge for the VM)
|
||||
// ============================================================================
|
||||
|
||||
/// A lightweight, 0-copy wrapper around a specific column (field) of a RecordSeries.
|
||||
///
|
||||
/// Why is this needed?
|
||||
/// In a SoA (Struct of Arrays) architecture, columns like `open` or `close`
|
||||
/// are stored as `Rc<RefCell<dyn SeriesMember>>` inside the `RecordSeries`.
|
||||
/// The `RefCell` is crucial for runtime mutability (so the engine can push new ticks).
|
||||
/// However, the VM's `Value::Object` expects a pure `Rc<dyn Object>`.
|
||||
///
|
||||
/// `SeriesView` bridges this gap safely: It holds the shared reference and implements `Object` itself,
|
||||
/// acting as a fast, read-only "window" into the underlying column data without copying any arrays.
|
||||
#[derive(Clone)]
|
||||
pub struct SeriesView {
|
||||
pub inner: Rc<std::cell::RefCell<dyn SeriesMember>>,
|
||||
pub field_name: Keyword,
|
||||
}
|
||||
|
||||
impl SeriesView {
|
||||
pub fn new(inner: Rc<std::cell::RefCell<dyn SeriesMember>>, field_name: Keyword) -> Self {
|
||||
Self { inner, field_name }
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for SeriesView {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "SeriesView[field: {}]", self.field_name.name())
|
||||
}
|
||||
}
|
||||
|
||||
impl Object for SeriesView {
|
||||
fn type_name(&self) -> &'static str {
|
||||
"SeriesView"
|
||||
}
|
||||
|
||||
// This allows the VM or specialized extractors to downcast the View back to its concrete type if needed.
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 5. Script Integration (RTL Registration)
|
||||
// ============================================================================
|
||||
|
||||
use crate::ast::environment::Environment;
|
||||
use crate::ast::types::{Purity, Signature};
|
||||
|
||||
pub fn register(env: &Environment) {
|
||||
// (create-series layout_record) -> RecordSeries
|
||||
// Extracts the layout from a sample record and creates an empty SoA RecordSeries.
|
||||
env.register_native_fn(
|
||||
"create-series",
|
||||
StaticType::Function(Box::new(Signature {
|
||||
params: StaticType::Tuple(vec![StaticType::Any]), // In reality: Record
|
||||
ret: StaticType::Any, // In reality: Object(RecordSeries)
|
||||
})),
|
||||
Purity::Impure,
|
||||
|args: std::vec::Vec<Value>| {
|
||||
if args.len() != 1 {
|
||||
panic!("create-series expects exactly 1 argument (a record)");
|
||||
}
|
||||
if let Value::Record(layout, _) = &args[0] {
|
||||
let series = RecordSeries::new(layout.clone());
|
||||
Value::Object(Rc::new(series))
|
||||
} else {
|
||||
panic!("create-series expects a record to extract the layout from")
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// (push-series series record) -> Void
|
||||
// Pushes the values of a record into the parallel arrays of the RecordSeries.
|
||||
env.register_native_fn(
|
||||
"push-series",
|
||||
StaticType::Function(Box::new(Signature {
|
||||
params: StaticType::Tuple(vec![StaticType::Any, StaticType::Any]),
|
||||
ret: StaticType::Void,
|
||||
})),
|
||||
Purity::Impure,
|
||||
|args: std::vec::Vec<Value>| {
|
||||
if args.len() != 2 {
|
||||
panic!("push-series expects exactly 2 arguments (series, record)");
|
||||
}
|
||||
let (series_val, record_val) = (&args[0], &args[1]);
|
||||
|
||||
if let Value::Object(obj) = series_val {
|
||||
if let Some(record_series) = obj.as_any().downcast_ref::<RecordSeries>() {
|
||||
if let Value::Record(_, values) = record_val {
|
||||
// Push each value into its corresponding column (SoA push)
|
||||
for (i, field_val) in values.iter().enumerate() {
|
||||
if let Some(field_series) = record_series.fields.get(i) {
|
||||
field_series.borrow_mut().push_value(field_val.clone(), None);
|
||||
}
|
||||
}
|
||||
return Value::Void;
|
||||
} else {
|
||||
panic!("push-series expects a record as the second argument");
|
||||
}
|
||||
}
|
||||
}
|
||||
panic!("push-series expects a RecordSeries as the first argument")
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user