Introduce unified Series trait

This commit introduces the `Series` trait to provide a unified interface
for accessing indexed data across different series types (RecordSeries,
SeriesView, and future SharedSeries). This simplifies the VM's indexer
logic and lays the groundwork for the dual series architecture.
This commit is contained in:
Michael Schimmel
2026-03-01 21:24:42 +01:00
parent fc858de59c
commit b74ddcfd61
3 changed files with 38 additions and 24 deletions
+19
View File
@@ -285,6 +285,15 @@ impl Object for RecordSeries {
fn as_any(&self) -> &dyn Any { fn as_any(&self) -> &dyn Any {
self self
} }
fn as_series(&self) -> Option<&dyn crate::ast::types::Series> {
Some(self)
}
}
impl crate::ast::types::Series for RecordSeries {
fn get_item(&self, index: usize) -> Option<Value> {
self.get_record(index)
}
} }
// ============================================================================ // ============================================================================
@@ -328,6 +337,16 @@ impl Object for SeriesView {
fn as_any(&self) -> &dyn Any { fn as_any(&self) -> &dyn Any {
self self
} }
fn as_series(&self) -> Option<&dyn crate::ast::types::Series> {
Some(self)
}
}
impl crate::ast::types::Series for SeriesView {
fn get_item(&self, index: usize) -> Option<Value> {
self.inner.borrow().get_value(index)
}
} }
// ============================================================================ // ============================================================================
+12
View File
@@ -180,6 +180,18 @@ impl RecordLayout {
pub trait Object: fmt::Debug { pub trait Object: fmt::Debug {
fn type_name(&self) -> &'static str; fn type_name(&self) -> &'static str;
fn as_any(&self) -> &dyn Any; fn as_any(&self) -> &dyn Any;
/// Optional optimization: If this object behaves like a time series (indexable lookbacks),
/// it can return a reference to its Series trait implementation.
fn as_series(&self) -> Option<&dyn Series> {
None
}
}
/// Unified interface for all series types (Local RecordSeries, SeriesViews, and future SharedSeries).
pub trait Series {
/// Gets the value at the specified lookback index (0 = newest).
fn get_item(&self, index: usize) -> Option<Value>;
} }
/// A shared sequence of values, used by both Tuples and Records. /// A shared sequence of values, used by both Tuples and Records.
+7 -24
View File
@@ -529,41 +529,24 @@ impl VM {
} }
res => break res, res => break res,
} }
} else if let Some(view) = obj.as_any().downcast_ref::<crate::ast::rtl::series::SeriesView>() { } else if let Some(series) = obj.as_series() {
// Support for `(prices 0)` or `prices[0]` - reading from the 0-copy view! // Unified Series Access (Step 1 of Dual Series Architecture)
// This handles RecordSeries, SeriesView, and future SharedSeries polymorphically.
if arg_vals.len() != 1 { if arg_vals.len() != 1 {
break Err("Series indexer expects exactly 1 argument (the lookback index)".to_string()); break Err(format!("{} indexer expects exactly 1 argument (the lookback index)", obj.type_name()));
} }
if let Value::Int(idx) = arg_vals[0] { if let Value::Int(idx) = arg_vals[0] {
if idx < 0 { if idx < 0 {
break Err("Series lookback index cannot be negative".to_string()); break Err(format!("{} lookback index cannot be negative", obj.type_name()));
} }
if let Some(val) = view.inner.borrow().get_value(idx as usize) { if let Some(val) = series.get_item(idx as usize) {
break Ok(val); break Ok(val);
} else { } else {
// Out of bounds / not enough data yet // Out of bounds / not enough data yet
break Ok(Value::Void); break Ok(Value::Void);
} }
} else { } else {
break Err("Series index must be an integer".to_string()); break Err(format!("{} index must be an integer", obj.type_name()));
}
} else if let Some(rs) = obj.as_any().downcast_ref::<crate::ast::rtl::series::RecordSeries>() {
// Support for `(my_ticks 0)` - reconstructs the full Record from SoA!
if arg_vals.len() != 1 {
break Err("RecordSeries indexer expects exactly 1 argument (the lookback index)".to_string());
}
if let Value::Int(idx) = arg_vals[0] {
if idx < 0 {
break Err("RecordSeries lookback index cannot be negative".to_string());
}
if let Some(rec) = rs.get_record(idx as usize) {
break Ok(rec);
} else {
// Out of bounds / not enough data yet
break Ok(Value::Void);
}
} else {
break Err("RecordSeries index must be an integer".to_string());
} }
} else { } else {
break Err(format!("Object is not callable: {}", obj.type_name())); break Err(format!("Object is not callable: {}", obj.type_name()));