From b74ddcfd6176d31e89cf9c8a5e9dc458504e4c92 Mon Sep 17 00:00:00 2001 From: Michael Schimmel Date: Sun, 1 Mar 2026 21:24:42 +0100 Subject: [PATCH] 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. --- src/ast/rtl/series.rs | 19 +++++++++++++++++++ src/ast/types.rs | 12 ++++++++++++ src/ast/vm.rs | 31 +++++++------------------------ 3 files changed, 38 insertions(+), 24 deletions(-) diff --git a/src/ast/rtl/series.rs b/src/ast/rtl/series.rs index efe5ece..8b4d9d8 100644 --- a/src/ast/rtl/series.rs +++ b/src/ast/rtl/series.rs @@ -285,6 +285,15 @@ impl Object for RecordSeries { fn as_any(&self) -> &dyn Any { 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 { + self.get_record(index) + } } // ============================================================================ @@ -328,6 +337,16 @@ impl Object for SeriesView { fn as_any(&self) -> &dyn Any { 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 { + self.inner.borrow().get_value(index) + } } // ============================================================================ diff --git a/src/ast/types.rs b/src/ast/types.rs index d810b7b..4eb49ce 100644 --- a/src/ast/types.rs +++ b/src/ast/types.rs @@ -180,6 +180,18 @@ impl RecordLayout { pub trait Object: fmt::Debug { fn type_name(&self) -> &'static str; 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; } /// A shared sequence of values, used by both Tuples and Records. diff --git a/src/ast/vm.rs b/src/ast/vm.rs index 0b242c8..4f8bc3c 100644 --- a/src/ast/vm.rs +++ b/src/ast/vm.rs @@ -529,41 +529,24 @@ impl VM { } res => break res, } - } else if let Some(view) = obj.as_any().downcast_ref::() { - // Support for `(prices 0)` or `prices[0]` - reading from the 0-copy view! + } else if let Some(series) = obj.as_series() { + // Unified Series Access (Step 1 of Dual Series Architecture) + // This handles RecordSeries, SeriesView, and future SharedSeries polymorphically. 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 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); } else { // Out of bounds / not enough data yet break Ok(Value::Void); } } else { - break Err("Series index must be an integer".to_string()); - } - } else if let Some(rs) = obj.as_any().downcast_ref::() { - // 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()); + break Err(format!("{} index must be an integer", obj.type_name())); } } else { break Err(format!("Object is not callable: {}", obj.type_name()));