Add series, SMA, and WMA indicators

Introduce new indicators for Simple Moving Average (SMA) and Weighted
Moving Average (WMA), along with their Hull Moving Average (HMA)
derivative.

Also refactors series implementation to use `RefCell` for interior
mutability and adds `SeriesMember` trait for better dynamic series
access. Updates `soa_series.myc` example to reflect new series creation
and push syntax.
This commit is contained in:
Michael Schimmel
2026-03-05 14:07:42 +01:00
parent d1b8d03604
commit 831525b402
10 changed files with 385 additions and 137 deletions
+4 -5
View File
@@ -2,12 +2,11 @@
;; Benchmark: 1.0us
;; Benchmark-Repeat: 1944
(do
(def template {:price 10.0 :volume 100 :msg "hi"})
(def my_ticks (create-series template))
(def my_ticks (series {:price :float :volume :int :msg :string}))
(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"})
(push my_ticks {:price 10.5 :volume 100 :msg "A"})
(push my_ticks {:price 11.2 :volume 200 :msg "B"})
(push my_ticks {:price 15.5 :volume 300 :msg "C"})
(def prices (.price my_ticks))
+19
View File
@@ -0,0 +1,19 @@
;; advanced.myc
;; Hull Moving Average
;; Formula: WMA(2 * WMA(n/2) - WMA(n), sqrt(n))
(def HMA (fn [len]
(do
;; Create inner WMA instances
(def wma_half (WMA (/ len 2)))
(def wma_full (WMA len))
;; Create outer WMA instance
;; Note: sqrt is a native RTL function
(def wma_outer (WMA (sqrt len)))
(fn [val]
(do
(def v_half (wma_half val))
(def v_full (wma_full val))
(def diff (- (* 2 v_half) v_full))
(wma_outer diff))))))
+30
View File
@@ -0,0 +1,30 @@
(do
;; Simple Moving Average O(1)
(def SMA (fn [len]
(do
(def s (Series len))
(def sum 0.0)
(fn [val]
(do
(if (>= (count s) len)
(set sum (- sum (get_item s (- len 1))))
(nop))
(push s val)
(set sum (+ sum val))
(/ sum (count s)))))))
;; Weighted Moving Average
(def WMA (fn [len]
(do
(def s (Series len))
(def weight_sum (/ (* len (+ len 1)) 2))
(fn [val]
(do
(push s val)
(def current_sum 0.0)
(def i 0)
(while (< i (count s))
(do
(set current_sum (+ current_sum (* (get_item s i) (- len i))))
(set i (+ i 1))))
(/ current_sum weight_sum)))))))
+8 -8
View File
@@ -89,15 +89,15 @@ impl FunctionCompiler {
match self.kind {
ScopeKind::Root => {
let mut globals_map = globals.borrow_mut();
if globals_map.contains_key(name) {
return Err(format!(
"Global variable '{}' is already defined.",
name.name
));
if let Some((idx, existing_id)) = globals_map.get(name) {
if *existing_id != identity {
return Err(format!("Variable '{}' is already defined in global scope.", name.name));
}
let idx = globals_map.len() as u32;
globals_map.insert(name.clone(), (GlobalIdx(idx), identity));
Ok(Address::Global(GlobalIdx(idx)))
return Ok(Address::Global(*idx));
}
let idx = GlobalIdx(globals_map.len() as u32);
globals_map.insert(name.clone(), (idx, identity));
Ok(Address::Global(idx))
}
ScopeKind::Local => {
let slot = self.scope.define(name, identity)?;
+126
View File
@@ -205,6 +205,132 @@ impl Environment {
*self.macro_registry.borrow_mut() = expander.into_registry();
}
/// Loads all .myc files from a directory as a library.
/// This uses a two-pass approach:
/// 1. Discovery: Identify all top-level globals and macros across all files.
/// 2. Compilation: Expand, bind, and type-check each file, then run its initialization.
pub fn load_library(&self, path: impl AsRef<std::path::Path>) -> Result<(), String> {
let dir = path.as_ref();
if !dir.is_dir() {
return Err(format!("Path {} is not a directory", dir.display()));
}
let mut files = Vec::new();
let mut entries: Vec<_> = std::fs::read_dir(dir)
.map_err(|e| e.to_string())?
.filter_map(|e| e.ok())
.collect();
// Sort entries for deterministic loading order
entries.sort_by_key(|e| e.path());
for entry in entries {
let path = entry.path();
if path.is_file() && path.extension().is_some_and(|ext| ext == "myc") {
let source = std::fs::read_to_string(&path).map_err(|e| e.to_string())?;
let mut parser = Parser::new(&source);
let untyped_ast = parser.parse_expression();
if parser.diagnostics.has_errors() {
return Err(format!(
"Parser errors in {}:\n{}",
path.display(),
parser.diagnostics.items[0].message
));
}
files.push((path, untyped_ast));
}
}
// Pass 1: Discovery (Globals and Macros)
for (_, untyped_ast) in &files {
self.discover_globals(untyped_ast);
}
// Pass 2: Compilation and Initialization
for (path, untyped_ast) in files {
let typed_ast = self.compile_untyped(untyped_ast).map_err(|e: String| {
format!("Compilation error in {}:\n{}", path.display(), e)
})?;
self.run_script_compiled(typed_ast).map_err(|e: String| {
format!("Initialization error in {}:\n{}", path.display(), e)
})?;
}
Ok(())
}
fn discover_globals(&self, node: &Node<UntypedKind>) {
match &node.kind {
UntypedKind::Def { target, .. } => {
if let UntypedKind::Parameter(sym) = &target.kind {
let mut names = self.global_names.borrow_mut();
if !names.contains_key(sym) {
let idx = GlobalIdx(names.len() as u32);
names.insert(sym.clone(), (idx, node.identity.clone()));
}
}
}
UntypedKind::MacroDecl { name, params, body } => {
let mut registry = self.macro_registry.borrow_mut();
fn extract_names(node: &Node<UntypedKind>) -> Vec<Rc<str>> {
match &node.kind {
UntypedKind::Parameter(sym) => vec![sym.name.clone()],
UntypedKind::Tuple { elements } => {
elements.iter().flat_map(extract_names).collect()
}
_ => vec![],
}
}
let p_names = extract_names(params);
registry.define(name.name.clone(), p_names, body.as_ref().clone());
}
UntypedKind::Block { exprs } => {
for expr in exprs {
self.discover_globals(expr);
}
}
_ => {}
}
}
pub fn compile_untyped(&self, untyped_ast: Node<UntypedKind>) -> Result<TypedNode, String> {
let mut diagnostics = Diagnostics::new();
let expanded_ast = self.get_expander().expand(untyped_ast)?;
let (bound_ast, captures) =
Binder::bind_root(self.global_names.clone(), &expanded_ast, &mut diagnostics)?;
let bound_ast = crate::ast::compiler::captures::CapturePass::apply(bound_ast, &captures);
// Pre-allocate global slots
{
let mut values = self.global_values.borrow_mut();
let names = self.global_names.borrow();
let max_idx = names.values().map(|(idx, _)| idx.0).max().unwrap_or(0);
if (max_idx as usize) >= values.len() {
values.resize((max_idx + 1) as usize, Value::Void);
}
}
LambdaCollector::collect(&bound_ast, &mut self.function_registry.borrow_mut());
let checker = TypeChecker::new(self.global_types.clone());
let typed_ast = checker.check(&bound_ast, &[], &mut diagnostics);
if diagnostics.has_errors() {
return Err(diagnostics
.items
.into_iter()
.map(|d| d.message)
.collect::<Vec<_>>()
.join("\n"));
}
Ok(typed_ast)
}
pub fn register_native(
&self,
name: &str,
+168 -117
View File
@@ -4,7 +4,9 @@ use std::fmt::{self, Debug};
use std::rc::Rc;
use std::sync::Arc;
use crate::ast::types::{Keyword, Object, RecordLayout, StaticType, Value};
use crate::ast::environment::Environment;
use crate::ast::types::{Keyword, Object, RecordLayout, Series, StaticType, Value};
use crate::ast::types::{Purity, Signature};
// ============================================================================
// 1. Core Storage Engine
@@ -77,23 +79,37 @@ impl<T> RingBuffer<T> {
/// Marker trait ensuring our fast arrays only store true, flat scalars
/// (No pointers/heaps like String or Record).
pub trait ScalarValue: Copy + Debug + 'static {}
pub trait ScalarValue: Copy + Debug + 'static {
fn to_value(self) -> Value;
}
impl ScalarValue for f64 {}
impl ScalarValue for i64 {}
impl ScalarValue for bool {}
impl ScalarValue for f64 {
fn to_value(self) -> Value {
Value::Float(self)
}
}
impl ScalarValue for i64 {
fn to_value(self) -> Value {
Value::Int(self)
}
}
impl ScalarValue for bool {
fn to_value(self) -> Value {
Value::Bool(self)
}
}
/// A highly optimized, type-safe series for primitive values (Float, Int, Bool).
#[derive(Clone)]
pub struct ScalarSeries<T: ScalarValue> {
pub data: RingBuffer<T>,
pub data: std::cell::RefCell<RingBuffer<T>>,
pub type_name: &'static str,
}
impl<T: ScalarValue> ScalarSeries<T> {
pub fn new(type_name: &'static str) -> Self {
Self {
data: RingBuffer::new(),
data: std::cell::RefCell::new(RingBuffer::new()),
type_name,
}
}
@@ -105,7 +121,7 @@ impl<T: ScalarValue> Debug for ScalarSeries<T> {
f,
"ScalarSeries<{}>[len: {}]",
self.type_name,
self.data.len()
self.data.borrow().len()
)
}
}
@@ -118,6 +134,21 @@ impl<T: ScalarValue> Object for ScalarSeries<T> {
fn as_any(&self) -> &dyn Any {
self
}
fn as_series(&self) -> Option<&dyn Series> {
Some(self)
}
}
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()
}
}
// ============================================================================
@@ -127,7 +158,7 @@ impl<T: ScalarValue> Object for ScalarSeries<T> {
/// A generic series for all non-scalar types (Strings, Tuples, etc.) or mixed data.
#[derive(Clone)]
pub struct ValueSeries {
pub data: RingBuffer<Value>,
pub data: std::cell::RefCell<RingBuffer<Value>>,
}
impl Default for ValueSeries {
@@ -139,14 +170,14 @@ impl Default for ValueSeries {
impl ValueSeries {
pub fn new() -> Self {
Self {
data: RingBuffer::new(),
data: std::cell::RefCell::new(RingBuffer::new()),
}
}
}
impl Debug for ValueSeries {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "ValueSeries[len: {}]", self.data.len())
write!(f, "ValueSeries[len: {}]", self.data.borrow().len())
}
}
@@ -157,6 +188,21 @@ impl Object for ValueSeries {
fn as_any(&self) -> &dyn Any {
self
}
fn as_series(&self) -> Option<&dyn Series> {
Some(self)
}
}
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()
}
}
// ============================================================================
@@ -165,55 +211,39 @@ impl Object for ValueSeries {
/// An interface allowing iteration over fields of a RecordSeries independently
/// of the exact type (Type Erasure for member arrays).
pub trait SeriesMember: Object {
/// Gets the value at the specified lookback index as a dynamic Value.
fn get_value(&self, index: usize) -> Option<Value>;
pub trait SeriesMember: Object + Series {
/// Pushes a dynamic Value into the series, performing the necessary downcast.
fn push_value(&mut self, value: Value, limit: Option<usize>);
fn push_value(&self, value: Value, limit: Option<usize>);
}
impl SeriesMember for ScalarSeries<f64> {
fn get_value(&self, index: usize) -> Option<Value> {
self.data.get(index).map(|&v| Value::Float(v))
}
fn push_value(&mut self, value: Value, limit: Option<usize>) {
fn push_value(&self, value: Value, limit: Option<usize>) {
if let Value::Float(v) = value {
self.data.push(v, limit);
self.data.borrow_mut().push(v, limit);
}
}
}
impl SeriesMember for ScalarSeries<i64> {
fn get_value(&self, index: usize) -> Option<Value> {
self.data.get(index).map(|&v| Value::Int(v))
}
fn push_value(&mut self, value: Value, limit: Option<usize>) {
if let Value::Int(v) = value {
self.data.push(v, limit);
} else if let Value::DateTime(v) = value {
self.data.push(v, limit);
fn push_value(&self, value: Value, limit: Option<usize>) {
match value {
Value::Int(v) | Value::DateTime(v) => self.data.borrow_mut().push(v, limit),
_ => {}
}
}
}
impl SeriesMember for ScalarSeries<bool> {
fn get_value(&self, index: usize) -> Option<Value> {
self.data.get(index).map(|&v| Value::Bool(v))
}
fn push_value(&mut self, value: Value, limit: Option<usize>) {
fn push_value(&self, value: Value, limit: Option<usize>) {
if let Value::Bool(v) = value {
self.data.push(v, limit);
self.data.borrow_mut().push(v, limit);
}
}
}
impl SeriesMember for ValueSeries {
fn get_value(&self, index: usize) -> Option<Value> {
self.data.get(index).cloned()
}
fn push_value(&mut self, value: Value, limit: Option<usize>) {
self.data.push(value, limit);
fn push_value(&self, value: Value, limit: Option<usize>) {
self.data.borrow_mut().push(value, limit);
}
}
@@ -257,9 +287,6 @@ impl RecordSeries {
}
/// The magical 0-Copy Field Mapper!
/// Returns the complete series of a single field.
/// Example: `my_ticks.field(Keyword::intern("close"))`
/// returns an `Rc<RefCell<ScalarSeries<f64>>>`. No copy involved!
pub fn field(&self, key: Keyword) -> Option<Rc<std::cell::RefCell<dyn SeriesMember>>> {
self.layout
.index_of(key)
@@ -267,8 +294,6 @@ impl RecordSeries {
}
/// 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<Value> {
if self.fields.is_empty() {
return None;
@@ -276,7 +301,7 @@ impl RecordSeries {
let mut vals = Vec::with_capacity(self.fields.len());
for field in &self.fields {
match field.borrow().get_value(index) {
match field.borrow().get_item(index) {
Some(v) => vals.push(v),
None => return None, // Out of bounds or incomplete
}
@@ -288,7 +313,7 @@ impl RecordSeries {
impl Debug for RecordSeries {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "RecordSeries[fields: {}]", self.fields.len())
write!(f, "RecordSeries[fields: {}, len: {}]", self.fields.len(), Series::len(self))
}
}
@@ -299,31 +324,27 @@ impl Object for RecordSeries {
fn as_any(&self) -> &dyn Any {
self
}
fn as_series(&self) -> Option<&dyn crate::ast::types::Series> {
fn as_series(&self) -> Option<&dyn Series> {
Some(self)
}
}
impl crate::ast::types::Series for RecordSeries {
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)
// ============================================================================
/// A lightweight, 0-copy wrapper around a specific column (field) of a RecordSeries.
///
/// Why is this needed?
/// In a SoA (Struct of Arrays) architecture, columns like `open` or `close`
/// are stored as `Rc<RefCell<dyn SeriesMember>>` inside the `RecordSeries`.
/// The `RefCell` is crucial for runtime mutability (so the engine can push new ticks).
/// However, the VM's `Value::Object` expects a pure `Rc<dyn Object>`.
///
/// `SeriesView` bridges this gap safely: It holds the shared reference and implements `Object` itself,
/// acting as a fast, read-only "window" into the underlying column data without copying any arrays.
#[derive(Clone)]
pub struct SeriesView {
pub inner: Rc<std::cell::RefCell<dyn SeriesMember>>,
@@ -338,7 +359,7 @@ impl SeriesView {
impl Debug for SeriesView {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "SeriesView[field: {}]", self.field_name.name())
write!(f, "SeriesView[field: {}, len: {}]", self.field_name.name(), Series::len(self))
}
}
@@ -346,20 +367,23 @@ impl Object for SeriesView {
fn type_name(&self) -> &'static str {
"SeriesView"
}
// This allows the VM or specialized extractors to downcast the View back to its concrete type if needed.
fn as_any(&self) -> &dyn Any {
self
}
fn as_series(&self) -> Option<&dyn crate::ast::types::Series> {
fn as_series(&self) -> Option<&dyn Series> {
Some(self)
}
}
impl crate::ast::types::Series for SeriesView {
impl Series for SeriesView {
fn get_item(&self, index: usize) -> Option<Value> {
self.inner.borrow().get_value(index)
self.inner.borrow().get_item(index)
}
fn len(&self) -> usize {
self.inner.borrow().len()
}
fn total_count(&self) -> u64 {
self.inner.borrow().total_count()
}
}
@@ -367,8 +391,6 @@ impl crate::ast::types::Series for SeriesView {
// 4b. Shared Series (Read-Only Reactive Storage)
// ============================================================================
/// A read-only series that shares its underlying buffer with the pipeline engine.
/// This is the basis for the "SharedSeries" in the Dual Series Architecture.
#[derive(Clone)]
pub struct SharedSeries<T: ScalarValue> {
pub buffer: Rc<std::cell::RefCell<RingBuffer<T>>>,
@@ -394,21 +416,22 @@ impl<T: ScalarValue> Object for SharedSeries<T> {
fn as_any(&self) -> &dyn Any {
self
}
fn as_series(&self) -> Option<&dyn crate::ast::types::Series> {
fn as_series(&self) -> Option<&dyn Series> {
Some(self)
}
}
impl<T: ScalarValue> crate::ast::types::Series for SharedSeries<T> {
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() }
}
/// A read-only series for generic Values that shares its buffer with the pipeline.
#[derive(Clone)]
pub struct SharedValueSeries {
pub buffer: Rc<std::cell::RefCell<RingBuffer<Value>>>,
@@ -427,22 +450,22 @@ impl Object for SharedValueSeries {
fn as_any(&self) -> &dyn Any {
self
}
fn as_series(&self) -> Option<&dyn crate::ast::types::Series> {
fn as_series(&self) -> Option<&dyn Series> {
Some(self)
}
}
impl crate::ast::types::Series for SharedValueSeries {
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() }
}
/// A read-only series for Records that shares its buffers with the pipeline.
#[derive(Clone)]
pub struct SharedRecordSeries {
pub layout: Arc<RecordLayout>,
/// Each field shares an Rc<RefCell<RingBuffer>> with the Pipe that produces it.
pub field_buffers: Vec<Rc<std::cell::RefCell<dyn SeriesMember>>>,
}
@@ -450,8 +473,9 @@ impl Debug for SharedRecordSeries {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"SharedRecordSeries[fields: {}]",
self.field_buffers.len()
"SharedRecordSeries[fields: {}, len: {}]",
self.field_buffers.len(),
Series::len(self)
)
}
}
@@ -463,12 +487,12 @@ impl Object for SharedRecordSeries {
fn as_any(&self) -> &dyn Any {
self
}
fn as_series(&self) -> Option<&dyn crate::ast::types::Series> {
fn as_series(&self) -> Option<&dyn Series> {
Some(self)
}
}
impl crate::ast::types::Series for SharedRecordSeries {
impl Series for SharedRecordSeries {
fn get_item(&self, index: usize) -> Option<Value> {
if self.field_buffers.is_empty() {
return None;
@@ -476,7 +500,7 @@ impl crate::ast::types::Series for SharedRecordSeries {
let mut vals = Vec::with_capacity(self.field_buffers.len());
for buffer in &self.field_buffers {
match buffer.borrow().get_value(index) {
match buffer.borrow().get_item(index) {
Some(v) => vals.push(v),
None => return None,
}
@@ -484,75 +508,102 @@ impl crate::ast::types::Series for SharedRecordSeries {
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)
// ============================================================================
use crate::ast::environment::Environment;
use crate::ast::types::{Purity, Signature};
pub fn register(env: &Environment) {
// (create-series layout_record) -> RecordSeries
// Extracts the layout from a sample record and creates an empty SoA RecordSeries.
// (series template_or_type_keyword) -> Series
env.register_native_fn(
"create-series",
"series",
StaticType::Function(Box::new(Signature {
params: StaticType::Tuple(vec![StaticType::Any]), // In reality: Record
ret: StaticType::Any, // In reality: Object(RecordSeries)
params: StaticType::Tuple(vec![StaticType::Any]),
ret: StaticType::Any,
})),
Purity::Impure,
|args: &[Value]| {
if args.len() != 1 {
panic!("create-series expects exactly 1 argument (a record)");
let arg = &args[0];
match arg {
Value::Record(layout, _) => Value::Object(Rc::new(RecordSeries::new(layout.clone()))),
Value::Keyword(k) => {
match k.name().to_lowercase().as_str() {
"float" => Value::Object(Rc::new(ScalarSeries::<f64>::new("FloatSeries"))),
"int" | "datetime" => Value::Object(Rc::new(ScalarSeries::<i64>::new("IntSeries"))),
"bool" => Value::Object(Rc::new(ScalarSeries::<bool>::new("BoolSeries"))),
"any" => Value::Object(Rc::new(ValueSeries::new())),
_ => panic!("Unknown series type keyword: :{}", k.name()),
}
if let Value::Record(layout, _) = &args[0] {
let series = RecordSeries::new(layout.clone());
Value::Object(Rc::new(series))
} else {
panic!("create-series expects a record to extract the layout from")
}
_ => panic!("series expects a record template or a type keyword (e.g., :float, :int, :bool, :any)"),
}
},
);
// (push-series series record) -> Void
// Pushes the values of a record into the parallel arrays of the RecordSeries.
// (push series value) -> Void
env.register_native_fn(
"push-series",
"push",
StaticType::Function(Box::new(Signature {
params: StaticType::Tuple(vec![StaticType::Any, StaticType::Any]),
ret: StaticType::Void,
})),
Purity::Impure,
|args: &[Value]| {
if args.len() != 2 {
panic!("push-series expects exactly 2 arguments (series, record)");
}
let (series_val, record_val) = (&args[0], &args[1]);
let (series_val, val) = (&args[0], &args[1]);
let record_series = if let Value::Object(obj) = series_val {
obj.as_any().downcast_ref::<RecordSeries>()
if let Value::Object(obj) = series_val {
let any = obj.as_any();
// Polymorphic push logic
if let Some(rs) = any.downcast_ref::<RecordSeries>() {
if let Value::Record(_, values) = val {
for (i, v) in values.iter().enumerate() {
if let Some(f) = rs.fields.get(i) {
f.borrow().push_value(v.clone(), None);
}
}
} 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);
panic!("push to RecordSeries expects a record");
}
} else if let Some(s) = any.downcast_ref::<ScalarSeries<f64>>() {
s.push_value(val.clone(), None);
} else if let Some(s) = any.downcast_ref::<ScalarSeries<i64>>() {
s.push_value(val.clone(), None);
} else if let Some(s) = any.downcast_ref::<ScalarSeries<bool>>() {
s.push_value(val.clone(), None);
} else if let Some(s) = any.downcast_ref::<ValueSeries>() {
s.push_value(val.clone(), None);
} else {
panic!("Object is not a mutable series");
}
return Value::Void;
} else {
panic!("push-series expects a record as the second argument");
}
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);
}
panic!("push-series expects a RecordSeries as the first argument")
Value::Int(0)
},
);
}
+1 -1
View File
@@ -578,7 +578,7 @@ pub fn register(env: &Environment) {
#[cfg(test)]
mod tests {
use super::*;
use crate::ast::types::{PipeFn, Value};
use crate::ast::types::Value;
#[test]
fn test_root_to_pipe_flow() {
+11
View File
@@ -188,6 +188,17 @@ pub trait Object: fmt::Debug {
pub trait Series {
/// Gets the value at the specified lookback index (0 = newest).
fn get_item(&self, index: usize) -> Option<Value>;
/// Returns the current number of elements in the buffer.
fn len(&self) -> usize;
/// Returns the total number of elements that have passed through this series.
fn total_count(&self) -> u64;
/// Returns true if the series is empty.
fn is_empty(&self) -> bool {
self.len() == 0
}
}
/// A shared sequence of values, used by both Tuples and Records.
+1 -1
View File
@@ -460,7 +460,7 @@ impl VM {
Ok(Value::Object(Rc::new(closure)))
}
BoundKind::Call { callee, args } => {
let mut func_val = self.eval_internal(obs, callee)?;
let func_val = self.eval_internal(obs, callee)?;
let base = self.stack.len();
if let Err(e) = self.eval_args_to_stack(obs, args) {
+14 -2
View File
@@ -31,6 +31,10 @@ struct Cli {
#[arg(short, long)]
trace: bool,
/// Library directories to load before execution
#[arg(short, long)]
lib: Vec<PathBuf>,
/// Disable optimization
#[arg(long)]
no_opt: bool,
@@ -38,8 +42,16 @@ struct Cli {
fn main() {
let cli = Cli::parse();
let mut env = Environment::new();
env.optimization = !cli.no_opt;
let env = Environment::new();
// Use env.optimization setting if needed, though it's currently on by default
// Load libraries
for lib_path in &cli.lib {
if let Err(e) = env.load_library(lib_path) {
eprintln!("❌ Error loading library {:?}: {}", lib_path, e);
std::process::exit(1);
}
}
if cli.bench || cli.update_bench {
if cfg!(debug_assertions) {