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:
Michael Schimmel
2026-02-28 21:47:14 +01:00
parent 85d21943dd
commit b612df6e10
5 changed files with 268 additions and 28 deletions
+156 -4
View File
@@ -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")
},
);
}