Files
RustAst/src/ast/rtl/series.rs
T
Brummel ae1d94e507 Refactor PushableSeries trait
Introduce a new trait `PushableSeries` to distinguish objects that can
have values pushed into them from those that are only readable series.
This clarifies the API and allows for more precise type checking.

The `Object` trait no longer has a default `push_value` implementation.
Instead, `PushableSeries` now provides this functionality.
`SeriesMember` has also been updated to require `PushableSeries` for
RecordSeries field buffers.
2026-03-22 19:45:50 +01:00

671 lines
21 KiB
Rust

use std::any::Any;
use std::collections::VecDeque;
use std::fmt::{self, Debug};
use std::rc::Rc;
use std::sync::Arc;
use crate::ast::environment::Environment;
use crate::ast::types::{Keyword, Object, PushableSeries, RecordLayout, Series, StaticType, Value};
use crate::ast::types::{Purity, Signature};
// ============================================================================
// 1. Core Storage Engine
// ============================================================================
/// A generic, efficient ring buffer for time series data.
/// (Replaces Delphi's TChunkArray for now with VecDeque for O(1) push/pop)
#[derive(Clone)]
pub struct RingBuffer<T> {
buffer: VecDeque<T>,
total_count: u64,
lookback_limit: usize,
}
impl<T> RingBuffer<T> {
pub fn new(lookback_limit: usize) -> Self {
// Pre-allocate up to 500 elements to minimize heap fragmentation
let initial_capacity = std::cmp::min(lookback_limit, 500);
Self {
buffer: VecDeque::with_capacity(initial_capacity),
total_count: 0,
lookback_limit,
}
}
/// Adds an element. Removes the oldest if the lookback limit is reached.
#[inline]
pub fn push(&mut self, item: T) {
self.buffer.push_back(item);
self.total_count += 1;
while self.buffer.len() > self.lookback_limit {
self.buffer.pop_front();
}
}
/// Access in "Financial Style": Index 0 is the newest element.
#[inline]
pub fn get(&self, index: usize) -> Option<&T> {
let len = self.buffer.len();
if index < len {
self.buffer.get(len - 1 - index)
} else {
None
}
}
#[inline]
pub fn total_count(&self) -> u64 {
self.total_count
}
#[inline]
pub fn len(&self) -> usize {
self.buffer.len()
}
pub fn is_empty(&self) -> bool {
self.buffer.is_empty()
}
}
// ============================================================================
// 2. Trait & Primitives for Fast-Paths
// ============================================================================
/// Marker trait ensuring our fast arrays only store true, flat scalars
/// (No pointers/heaps like String or Record).
pub trait ScalarValue: Copy + Debug + 'static {
fn to_value(self) -> Value;
/// Converts a dynamic `Value` back to this scalar type, returning `None` on mismatch.
fn from_value(v: Value) -> Option<Self>;
}
impl ScalarValue for f64 {
fn to_value(self) -> Value {
Value::Float(self)
}
fn from_value(v: Value) -> Option<Self> {
v.as_float()
}
}
impl ScalarValue for i64 {
fn to_value(self) -> Value {
Value::Int(self)
}
fn from_value(v: Value) -> Option<Self> {
v.as_int()
}
}
impl ScalarValue for bool {
fn to_value(self) -> Value {
Value::Bool(self)
}
fn from_value(v: Value) -> Option<Self> {
if let Value::Bool(b) = v { Some(b) } else { None }
}
}
/// A highly optimized, type-safe series for primitive values (Float, Int, Bool).
#[derive(Clone)]
pub struct ScalarSeries<T: ScalarValue> {
pub data: std::cell::RefCell<RingBuffer<T>>,
pub type_name: &'static str,
}
impl<T: ScalarValue> ScalarSeries<T> {
pub fn new(type_name: &'static str, lookback_limit: usize) -> Self {
Self {
data: std::cell::RefCell::new(RingBuffer::new(lookback_limit)),
type_name,
}
}
}
impl<T: ScalarValue> Debug for ScalarSeries<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"ScalarSeries<{}>[len: {}]",
self.type_name,
self.data.borrow().len()
)
}
}
// Makes the series usable as a dynamic Object for the VM.
impl<T: ScalarValue> Object for ScalarSeries<T> {
fn type_name(&self) -> &'static str {
self.type_name
}
fn as_any(&self) -> &dyn Any {
self
}
fn into_rc_any(self: Rc<Self>) -> Rc<dyn Any> {
self
}
fn as_series(&self) -> Option<&dyn Series> {
Some(self)
}
fn as_pushable_series(&self) -> Option<&dyn PushableSeries> {
Some(self)
}
}
impl<T: ScalarValue> PushableSeries for ScalarSeries<T> {
fn push_value(&self, value: Value) {
if let Some(v) = T::from_value(value) {
self.data.borrow_mut().push(v);
}
}
}
impl<T: ScalarValue> Series for ScalarSeries<T> {
fn get_item(&self, index: usize) -> Option<Value> {
self.data.borrow().get(index).map(|&v| v.to_value())
}
fn len(&self) -> usize {
self.data.borrow().len()
}
fn total_count(&self) -> u64 {
self.data.borrow().total_count()
}
}
// ============================================================================
// 2b. Fallback Series for non-scalar types
// ============================================================================
/// A generic series for all non-scalar types (Strings, Tuples, etc.) or mixed data.
#[derive(Clone)]
pub struct ValueSeries {
pub data: std::cell::RefCell<RingBuffer<Value>>,
}
impl ValueSeries {
pub fn new(lookback_limit: usize) -> Self {
Self {
data: std::cell::RefCell::new(RingBuffer::new(lookback_limit)),
}
}
}
impl Debug for ValueSeries {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "ValueSeries[len: {}]", self.data.borrow().len())
}
}
impl Object for ValueSeries {
fn type_name(&self) -> &'static str {
"ValueSeries"
}
fn as_any(&self) -> &dyn Any {
self
}
fn into_rc_any(self: Rc<Self>) -> Rc<dyn Any> {
self
}
fn as_series(&self) -> Option<&dyn Series> {
Some(self)
}
fn as_pushable_series(&self) -> Option<&dyn PushableSeries> {
Some(self)
}
}
impl PushableSeries for ValueSeries {
fn push_value(&self, value: Value) {
self.data.borrow_mut().push(value);
}
}
impl Series for ValueSeries {
fn get_item(&self, index: usize) -> Option<Value> {
self.data.borrow().get(index).cloned()
}
fn len(&self) -> usize {
self.data.borrow().len()
}
fn total_count(&self) -> u64 {
self.data.borrow().total_count()
}
}
// ============================================================================
// 3. RecordSeries (SoA - Struct of Arrays)
// ============================================================================
/// Marker supertrait for RecordSeries field buffers.
/// Ensures each field buffer supports indexed access (Series), dynamic dispatch (Object),
/// and value insertion (PushableSeries).
pub trait SeriesMember: PushableSeries {}
impl SeriesMember for ScalarSeries<f64> {}
impl SeriesMember for ScalarSeries<i64> {}
impl SeriesMember for ScalarSeries<bool> {}
impl SeriesMember for ValueSeries {}
/// The "Struct of Arrays" implementation for Records (e.g., Ticks).
/// Instead of holding a large array of Records (AoS), this series
/// splits the record and stores each field in a separate (parallel) series.
#[derive(Clone)]
pub struct RecordSeries {
layout: Arc<RecordLayout>,
/// Each field of the record gets its own series (SoA).
/// We use Rc<RefCell> so we can iterate fields individually,
/// but also pass them as 0-Copy references to indicators!
fields: Vec<Rc<std::cell::RefCell<dyn SeriesMember>>>,
}
impl RecordSeries {
/// Creates a new RecordSeries matching the given layout.
pub fn new(layout: Arc<RecordLayout>, lookback_limit: usize) -> Self {
let mut fields: Vec<Rc<std::cell::RefCell<dyn SeriesMember>>> =
Vec::with_capacity(layout.fields.len());
for (_, static_type) in &layout.fields {
// Automatically select the optimal storage representation based on the field's static type.
let member: Rc<std::cell::RefCell<dyn SeriesMember>> = match static_type {
StaticType::Float => Rc::new(std::cell::RefCell::new(ScalarSeries::<f64>::new(
"FloatSeries",
lookback_limit,
))),
StaticType::Int | StaticType::DateTime => Rc::new(std::cell::RefCell::new(
ScalarSeries::<i64>::new("IntSeries", lookback_limit),
)),
StaticType::Bool => Rc::new(std::cell::RefCell::new(ScalarSeries::<bool>::new(
"BoolSeries",
lookback_limit,
))),
// Fallback for everything else (Text, Lists, nested Records without SoA, etc.)
_ => Rc::new(std::cell::RefCell::new(ValueSeries::new(lookback_limit))),
};
fields.push(member);
}
Self { layout, fields }
}
/// The magical 0-Copy Field Mapper!
pub fn field(&self, key: Keyword) -> Option<Rc<std::cell::RefCell<dyn SeriesMember>>> {
self.layout
.index_of(key)
.map(|idx| self.fields[idx].clone())
}
/// Dynamically reconstructs the full Record at the given lookback index.
pub fn get_record(&self, index: usize) -> Option<Value> {
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_item(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 {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"RecordSeries[fields: {}, len: {}]",
self.fields.len(),
Series::len(self)
)
}
}
impl Object for RecordSeries {
fn type_name(&self) -> &'static str {
"RecordSeries"
}
fn as_any(&self) -> &dyn Any {
self
}
fn into_rc_any(self: Rc<Self>) -> Rc<dyn Any> {
self
}
fn as_series(&self) -> Option<&dyn Series> {
Some(self)
}
fn as_pushable_series(&self) -> Option<&dyn PushableSeries> {
Some(self)
}
}
impl PushableSeries for RecordSeries {
fn push_value(&self, value: Value) {
if let Value::Record(_, values) = value {
for (i, v) in values.iter().enumerate() {
if let Some(f) = self.fields.get(i) {
f.borrow().push_value(v.clone());
}
}
} else {
panic!("push to RecordSeries expects a record");
}
}
}
impl Series for RecordSeries {
fn get_item(&self, index: usize) -> Option<Value> {
self.get_record(index)
}
fn len(&self) -> usize {
self.fields.first().map(|f| f.borrow().len()).unwrap_or(0)
}
fn total_count(&self) -> u64 {
self.fields
.first()
.map(|f| f.borrow().total_count())
.unwrap_or(0)
}
}
// ============================================================================
// 4. Series View (Safe, 0-Copy access bridge for the VM)
// ============================================================================
#[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: {}, len: {}]",
self.field_name.name(),
Series::len(self)
)
}
}
impl Object for SeriesView {
fn type_name(&self) -> &'static str {
"SeriesView"
}
fn as_any(&self) -> &dyn Any {
self
}
fn into_rc_any(self: Rc<Self>) -> Rc<dyn Any> {
self
}
fn as_series(&self) -> Option<&dyn Series> {
Some(self)
}
}
impl Series for SeriesView {
fn get_item(&self, index: usize) -> Option<Value> {
self.inner.borrow().get_item(index)
}
fn len(&self) -> usize {
self.inner.borrow().len()
}
fn total_count(&self) -> u64 {
self.inner.borrow().total_count()
}
}
// ============================================================================
// 4b. Shared Series (Read-Only Reactive Storage)
// ============================================================================
#[derive(Clone)]
pub struct SharedSeries<T: ScalarValue> {
pub buffer: Rc<std::cell::RefCell<RingBuffer<T>>>,
pub type_name: &'static str,
pub converter: fn(T) -> Value,
}
impl<T: ScalarValue> Debug for SharedSeries<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"SharedSeries<{}>[len: {}]",
self.type_name,
self.buffer.borrow().len()
)
}
}
impl<T: ScalarValue> Object for SharedSeries<T> {
fn type_name(&self) -> &'static str {
self.type_name
}
fn as_any(&self) -> &dyn Any {
self
}
fn into_rc_any(self: Rc<Self>) -> Rc<dyn Any> {
self
}
fn as_series(&self) -> Option<&dyn Series> {
Some(self)
}
}
impl<T: ScalarValue> Series for SharedSeries<T> {
fn get_item(&self, index: usize) -> Option<Value> {
self.buffer
.borrow()
.get(index)
.map(|&v| (self.converter)(v))
}
fn len(&self) -> usize {
self.buffer.borrow().len()
}
fn total_count(&self) -> u64 {
self.buffer.borrow().total_count()
}
}
#[derive(Clone)]
pub struct SharedValueSeries {
pub buffer: Rc<std::cell::RefCell<RingBuffer<Value>>>,
}
impl Debug for SharedValueSeries {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "SharedValueSeries[len: {}]", self.buffer.borrow().len())
}
}
impl Object for SharedValueSeries {
fn type_name(&self) -> &'static str {
"SharedValueSeries"
}
fn as_any(&self) -> &dyn Any {
self
}
fn into_rc_any(self: Rc<Self>) -> Rc<dyn Any> {
self
}
fn as_series(&self) -> Option<&dyn Series> {
Some(self)
}
}
impl Series for SharedValueSeries {
fn get_item(&self, index: usize) -> Option<Value> {
self.buffer.borrow().get(index).cloned()
}
fn len(&self) -> usize {
self.buffer.borrow().len()
}
fn total_count(&self) -> u64 {
self.buffer.borrow().total_count()
}
}
#[derive(Clone)]
pub struct SharedRecordSeries {
pub layout: Arc<RecordLayout>,
pub field_buffers: Vec<Rc<std::cell::RefCell<dyn SeriesMember>>>,
}
impl Debug for SharedRecordSeries {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"SharedRecordSeries[fields: {}, len: {}]",
self.field_buffers.len(),
Series::len(self)
)
}
}
impl Object for SharedRecordSeries {
fn type_name(&self) -> &'static str {
"SharedRecordSeries"
}
fn as_any(&self) -> &dyn Any {
self
}
fn into_rc_any(self: Rc<Self>) -> Rc<dyn Any> {
self
}
fn as_series(&self) -> Option<&dyn Series> {
Some(self)
}
}
impl Series for SharedRecordSeries {
fn get_item(&self, index: usize) -> Option<Value> {
if self.field_buffers.is_empty() {
return None;
}
let mut vals = Vec::with_capacity(self.field_buffers.len());
for buffer in &self.field_buffers {
match buffer.borrow().get_item(index) {
Some(v) => vals.push(v),
None => return None,
}
}
Some(Value::Record(self.layout.clone(), Rc::new(vals)))
}
fn len(&self) -> usize {
self.field_buffers
.first()
.map(|f| f.borrow().len())
.unwrap_or(0)
}
fn total_count(&self) -> u64 {
self.field_buffers
.first()
.map(|f| f.borrow().total_count())
.unwrap_or(0)
}
}
// ============================================================================
// 5. Script Integration (RTL Registration)
// ============================================================================
pub fn register(env: &Environment) {
// (series lookback_limit template_or_type_keyword) -> Series
env.register_native_fn(
"series",
StaticType::Function(Box::new(Signature {
params: StaticType::Tuple(vec![StaticType::Int, StaticType::Any]),
ret: StaticType::Any,
})),
Purity::Impure,
|args: &[Value]| {
let lookback_limit = args[0].as_int().unwrap_or(0) as usize;
let arg = &args[1];
match arg {
Value::Keyword(k) => {
match k.name().to_lowercase().as_str() {
"float" => Value::Object(Rc::new(ScalarSeries::<f64>::new("FloatSeries", lookback_limit))),
"int" | "datetime" => Value::Object(Rc::new(ScalarSeries::<i64>::new("IntSeries", lookback_limit))),
"bool" => Value::Object(Rc::new(ScalarSeries::<bool>::new("BoolSeries", lookback_limit))),
"text" => Value::Object(Rc::new(ValueSeries::new(lookback_limit))),
_ => panic!("Unknown or unsupported series type keyword: :{}", k.name()),
}
}
Value::Record(schema_layout, schema_values) => {
// Interpret the record as a schema: { :field :type_keyword }
let mut fields = Vec::with_capacity(schema_layout.fields.len());
for (i, (name, _)) in schema_layout.fields.iter().enumerate() {
let type_keyword = match &schema_values[i] {
Value::Keyword(tk) => tk.name().to_lowercase(),
_ => panic!("Field :{} in series schema must be a type keyword (e.g., :float, :int)", name.name()),
};
let ty = match type_keyword.as_str() {
"float" => StaticType::Float,
"int" => StaticType::Int,
"bool" => StaticType::Bool,
"datetime" => StaticType::DateTime,
"text" => StaticType::Text,
_ => panic!("Unsupported type keyword :{} for series field :{}", type_keyword, name.name()),
};
fields.push((*name, ty));
}
let final_layout = RecordLayout::get_or_create(fields);
Value::Object(Rc::new(RecordSeries::new(final_layout, lookback_limit)))
}
_ => panic!("series expects a lookback_limit and a type keyword (:float, :int...) or a schema record (e.g., {{:price :float}})"),
}
},
);
// (push series value) -> Void
env.register_native_fn(
"push",
StaticType::Function(Box::new(Signature {
params: StaticType::Tuple(vec![StaticType::Any, StaticType::Any]),
ret: StaticType::Void,
})),
Purity::Impure,
|args: &[Value]| {
let (series_val, val) = (&args[0], &args[1]);
if let Value::Object(obj) = series_val {
if let Some(p) = obj.as_pushable_series() {
p.push_value(val.clone());
return Value::Void;
}
panic!("push expects a pushable series, got: {}", obj.type_name());
}
panic!("push expects a series object as the first argument")
},
);
// (len series) -> Int
env.register_native_fn(
"len",
StaticType::Function(Box::new(Signature {
params: StaticType::Tuple(vec![StaticType::Any]),
ret: StaticType::Int,
})),
Purity::Pure,
|args: &[Value]| {
if let Value::Object(obj) = &args[0]
&& let Some(s) = obj.as_series()
{
return Value::Int(s.len() as i64);
}
Value::Int(0)
},
);
}