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
+90 -24
View File
@@ -301,14 +301,51 @@ impl VM {
BoundKind::GetField { rec, field } => {
let rec_val = self.eval_internal(obs, rec)?;
if let Value::Record(layout, values) = rec_val {
if let Some(idx) = layout.index_of(*field) {
Ok(values[idx].clone())
} else {
Err(format!("Record does not have field :{}", field.name()))
// In Rust, pattern matching (`match`) is the idiomatic way to handle variants safely.
// Previously, this only handled `Value::Record`. Now, we handle objects (like `RecordSeries`) polymorphically.
match rec_val {
// Case 1: The classic Record.
// This is a struct-like tuple containing an Arc<RecordLayout> and a Vec<Value>.
Value::Record(layout, values) => {
if let Some(idx) = layout.index_of(*field) {
Ok(values[idx].clone())
} else {
Err(format!("Record does not have field :{}", field.name()))
}
}
} else {
Err(format!("Attempt to access field on non-record: {}", rec_val))
// Case 2: A dynamic Object (our SoA / Struct-of-Arrays optimization).
// `Value::Object` holds an `Rc<dyn Object>` - a reference-counted trait object (type-erased).
Value::Object(obj) => {
// 1. We get the raw `&dyn Any` reference (Rust's standard mechanism for runtime type reflection).
let any_ptr = obj.as_any();
// 2. Downcast! We check at runtime if the pointer actually points to a `RecordSeries`.
// `downcast_ref` is very fast (essentially an O(1) type ID comparison under the hood).
if let Some(record_series) = any_ptr.downcast_ref::<crate::ast::rtl::series::RecordSeries>() {
// 3. We call our highly performant 0-copy method on the series.
// It returns an `Rc<RefCell<dyn SeriesMember>>`, which is a shared pointer
// to the concrete column array (e.g., a `ScalarSeries<f64>`).
if let Some(field_series) = record_series.field(*field) {
// 4. We wrap this RefCell in our `SeriesView` struct.
// The `SeriesView` acts as a pure `Object` for the VM, holding the reference.
// CRITICAL: No array elements are copied here! This is pure, fast pointer juggling.
// This single operation turns a SoA `RecordSeries` into a high-speed `FloatSeries` view.
let view = crate::ast::rtl::series::SeriesView::new(field_series, *field);
return Ok(Value::Object(std::rc::Rc::new(view)));
} else {
return Err(format!("RecordSeries does not have field :{}", field.name()));
}
}
// Fallback if it's another type of object that is not a RecordSeries.
Err(format!("Attempt to access field on non-record object: {}", obj.type_name()))
}
// Error handling for primitives (Int, Float, etc.).
_ => Err(format!("Attempt to access field on non-record: {}", rec_val)),
}
}
@@ -401,15 +438,21 @@ impl VM {
} else {
return Err(format!("Record does not have field :{}", k.name()));
}
} else if let Value::Object(obj) = rec {
// Polymorphic Field Access: Allow `.field` on a RecordSeries
if let Some(rs) = obj.as_any().downcast_ref::<crate::ast::rtl::series::RecordSeries>() {
if let Some(field_series) = rs.field(k) {
let view = crate::ast::rtl::series::SeriesView::new(field_series, k);
return Ok(Value::Object(std::rc::Rc::new(view)));
} else {
return Err(format!("RecordSeries does not have field :{}", k.name()));
}
}
return Err(format!("Field accessor .{} expects a record or RecordSeries, got {}", k.name(), obj.type_name()));
} else {
return Err(format!(
"Field accessor .{} expects a record, got {}",
k.name(),
rec
));
return Err(format!("Field accessor .{} expects a record or RecordSeries, got {}", k.name(), rec));
}
}
_ => {
} _ => {
return Err(format!(
"Tail call target is not a function: {}",
func_val
@@ -436,15 +479,21 @@ impl VM {
} else {
break Err(format!("Record does not have field :{}", k.name()));
}
} else if let Value::Object(obj) = rec {
// Polymorphic Field Access: Allow `.field` on a RecordSeries
if let Some(rs) = obj.as_any().downcast_ref::<crate::ast::rtl::series::RecordSeries>() {
if let Some(field_series) = rs.field(k) {
let view = crate::ast::rtl::series::SeriesView::new(field_series, k);
break Ok(Value::Object(std::rc::Rc::new(view)));
} else {
break Err(format!("RecordSeries does not have field :{}", k.name()));
}
}
break Err(format!("Field accessor .{} expects a record or RecordSeries, got {}", k.name(), obj.type_name()));
} else {
break Err(format!(
"Field accessor .{} expects a record, got {}",
k.name(),
rec
));
break Err(format!("Field accessor .{} expects a record or RecordSeries, got {}", k.name(), rec));
}
}
Value::Object(obj) => {
} Value::Object(obj) => {
if let Some(closure) = obj.as_any().downcast_ref::<Closure>() {
let old_stack_top = self.stack.len();
self.frames.push(CallFrame {
@@ -480,11 +529,28 @@ impl VM {
}
res => break res,
}
} else if let Some(view) = obj.as_any().downcast_ref::<crate::ast::rtl::series::SeriesView>() {
// Support for `(prices 0)` or `prices[0]` - reading from the 0-copy view!
if arg_vals.len() != 1 {
break Err("Series indexer expects exactly 1 argument (the lookback index)".to_string());
}
if let Value::Int(idx) = arg_vals[0] {
if idx < 0 {
break Err("Series lookback index cannot be negative".to_string());
}
if let Some(val) = view.inner.borrow().get_value(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 {
break Err(format!("Object is not a closure: {}", obj.type_name()));
break Err(format!("Object is not callable: {}", obj.type_name()));
}
}
_ => break Err(format!("Attempt to call non-function: {}", func_val)),
} _ => break Err(format!("Attempt to call non-function: {}", func_val)),
}
}
}