From 46b546446fae2938f392c81cca47064e7b72c71e Mon Sep 17 00:00:00 2001 From: Michael Schimmel Date: Sun, 1 Mar 2026 18:44:46 +0100 Subject: [PATCH] Add RecordSeries::get_record and support for (my_ticks 0) This commit introduces the `get_record` method to `RecordSeries`, allowing for the dynamic reconstruction of a full record from its constituent fields at a given index. This enables idiomatic access to series data using indexing like `(my_ticks 0)`. The `push-series` function has also been updated to correctly handle the structure of records when pushing data into a `RecordSeries`. --- examples/soa_series.myc | 12 +++++------ src/ast/rtl/series.rs | 45 +++++++++++++++++++++++++++++++---------- src/ast/vm.rs | 18 +++++++++++++++++ 3 files changed, 58 insertions(+), 17 deletions(-) diff --git a/examples/soa_series.myc b/examples/soa_series.myc index 3c94d4a..64d0d42 100644 --- a/examples/soa_series.myc +++ b/examples/soa_series.myc @@ -1,15 +1,15 @@ ;; Output: 26.7 -;; Benchmark: 998ns +;; Benchmark: 998ns ;; Benchmark-Repeat: 2047 (do - (def template {:price 10.0 :volume 100}) + (def template {:price 10.0 :volume 100 :msg "hi"}) (def my_ticks (create-series template)) - (push-series my_ticks {:price 10.5 :volume 100}) - (push-series my_ticks {:price 11.2 :volume 200}) - (push-series my_ticks {:price 15.5 :volume 300}) + (push-series my_ticks {:price 10.5 :volume 100 :msg "A"}) + (push-series my_ticks {:price 11.2 :volume 200 :msg "B"}) + (push-series my_ticks {:price 15.5 :volume 300 :msg "C"}) (def prices (.price my_ticks)) (+ (prices 0) (prices 1)) -) \ No newline at end of file +) diff --git a/src/ast/rtl/series.rs b/src/ast/rtl/series.rs index d63efdb..efe5ece 100644 --- a/src/ast/rtl/series.rs +++ b/src/ast/rtl/series.rs @@ -251,6 +251,25 @@ impl RecordSeries { pub fn field(&self, key: Keyword) -> Option>> { self.layout.index_of(key).map(|idx| self.fields[idx].clone()) } + + /// Dynamically reconstructs the full Record at the given lookback index. + /// This is an O(N) operation where N is the number of fields, meaning it breaks + /// the 0-Copy performance path, but it's essential for idiomatic AST access `(series 0)`. + pub fn get_record(&self, index: usize) -> Option { + if self.fields.is_empty() { + return None; + } + + let mut vals = Vec::with_capacity(self.fields.len()); + for field in &self.fields { + match field.borrow().get_value(index) { + Some(v) => vals.push(v), + None => return None, // Out of bounds or incomplete + } + } + + Some(Value::Record(self.layout.clone(), Rc::new(vals))) + } } impl Debug for RecordSeries { @@ -356,19 +375,23 @@ pub fn register(env: &Environment) { } 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::() { - 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); - } + let record_series = if let Value::Object(obj) = series_val { + obj.as_any().downcast_ref::() + } else { + None + }; + + if let Some(record_series) = record_series { + 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"); } + return Value::Void; + } else { + panic!("push-series expects a record as the second argument"); } } panic!("push-series expects a RecordSeries as the first argument") diff --git a/src/ast/vm.rs b/src/ast/vm.rs index a8ba5e8..0b242c8 100644 --- a/src/ast/vm.rs +++ b/src/ast/vm.rs @@ -547,6 +547,24 @@ impl VM { } 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()); + } } else { break Err(format!("Object is not callable: {}", obj.type_name())); }