diff --git a/examples/soa_series.myc b/examples/soa_series.myc index ae95b75..0c7de63 100644 --- a/examples/soa_series.myc +++ b/examples/soa_series.myc @@ -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)) diff --git a/lib/advanced.myc b/lib/advanced.myc new file mode 100644 index 0000000..d4da5be --- /dev/null +++ b/lib/advanced.myc @@ -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)))))) diff --git a/lib/indicators.myc b/lib/indicators.myc new file mode 100644 index 0000000..bc8f9aa --- /dev/null +++ b/lib/indicators.myc @@ -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))))))) diff --git a/src/ast/compiler/binder.rs b/src/ast/compiler/binder.rs index 66a968e..b05c434 100644 --- a/src/ast/compiler/binder.rs +++ b/src/ast/compiler/binder.rs @@ -89,16 +89,16 @@ 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)); + } + return Ok(Address::Global(*idx)); + } + let idx = GlobalIdx(globals_map.len() as u32); + globals_map.insert(name.clone(), (idx, identity)); + Ok(Address::Global(idx)) } - let idx = globals_map.len() as u32; - globals_map.insert(name.clone(), (GlobalIdx(idx), identity)); - Ok(Address::Global(GlobalIdx(idx))) - } ScopeKind::Local => { let slot = self.scope.define(name, identity)?; Ok(Address::Local(slot)) diff --git a/src/ast/environment.rs b/src/ast/environment.rs index f5be136..638a4e0 100644 --- a/src/ast/environment.rs +++ b/src/ast/environment.rs @@ -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) -> 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) { + 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) -> Vec> { + 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) -> Result { + 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::>() + .join("\n")); + } + + Ok(typed_ast) + } + pub fn register_native( &self, name: &str, diff --git a/src/ast/rtl/series.rs b/src/ast/rtl/series.rs index 2a4b2a3..005a659 100644 --- a/src/ast/rtl/series.rs +++ b/src/ast/rtl/series.rs @@ -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 RingBuffer { /// 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 { - pub data: RingBuffer, + pub data: std::cell::RefCell>, pub type_name: &'static str, } impl ScalarSeries { 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 Debug for ScalarSeries { f, "ScalarSeries<{}>[len: {}]", self.type_name, - self.data.len() + self.data.borrow().len() ) } } @@ -118,6 +134,21 @@ impl Object for ScalarSeries { fn as_any(&self) -> &dyn Any { self } + fn as_series(&self) -> Option<&dyn Series> { + Some(self) + } +} + +impl Series for ScalarSeries { + fn get_item(&self, index: usize) -> Option { + 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 Object for ScalarSeries { /// A generic series for all non-scalar types (Strings, Tuples, etc.) or mixed data. #[derive(Clone)] pub struct ValueSeries { - pub data: RingBuffer, + pub data: std::cell::RefCell>, } 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 { + 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; - +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); + fn push_value(&self, value: Value, limit: Option); } impl SeriesMember for ScalarSeries { - fn get_value(&self, index: usize) -> Option { - self.data.get(index).map(|&v| Value::Float(v)) - } - fn push_value(&mut self, value: Value, limit: Option) { + fn push_value(&self, value: Value, limit: Option) { if let Value::Float(v) = value { - self.data.push(v, limit); + self.data.borrow_mut().push(v, limit); } } } impl SeriesMember for ScalarSeries { - fn get_value(&self, index: usize) -> Option { - self.data.get(index).map(|&v| Value::Int(v)) - } - fn push_value(&mut self, value: Value, limit: Option) { - 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) { + match value { + Value::Int(v) | Value::DateTime(v) => self.data.borrow_mut().push(v, limit), + _ => {} } } } impl SeriesMember for ScalarSeries { - fn get_value(&self, index: usize) -> Option { - self.data.get(index).map(|&v| Value::Bool(v)) - } - fn push_value(&mut self, value: Value, limit: Option) { + fn push_value(&self, value: Value, limit: Option) { 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 { - self.data.get(index).cloned() - } - fn push_value(&mut self, value: Value, limit: Option) { - self.data.push(value, limit); + fn push_value(&self, value: Value, limit: Option) { + 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>>`. No copy involved! pub fn field(&self, key: Keyword) -> Option>> { 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 { 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 { 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>` 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`. -/// -/// `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>, @@ -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 { - 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 { pub buffer: Rc>>, @@ -394,21 +416,22 @@ impl Object for SharedSeries { 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 SharedSeries { +impl Series for SharedSeries { fn get_item(&self, index: usize) -> Option { 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>>, @@ -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 { 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, - /// Each field shares an Rc> with the Pipe that produces it. pub field_buffers: Vec>>, } @@ -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 { 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)"); - } - 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") + 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::::new("FloatSeries"))), + "int" | "datetime" => Value::Object(Rc::new(ScalarSeries::::new("IntSeries"))), + "bool" => Value::Object(Rc::new(ScalarSeries::::new("BoolSeries"))), + "any" => Value::Object(Rc::new(ValueSeries::new())), + _ => panic!("Unknown series type keyword: :{}", k.name()), + } + } + _ => 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::() - } 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); + if let Value::Object(obj) = series_val { + let any = obj.as_any(); + + // Polymorphic push logic + if let Some(rs) = any.downcast_ref::() { + 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 { + panic!("push to RecordSeries expects a record"); } - return Value::Void; + } else if let Some(s) = any.downcast_ref::>() { + s.push_value(val.clone(), None); + } else if let Some(s) = any.downcast_ref::>() { + s.push_value(val.clone(), None); + } else if let Some(s) = any.downcast_ref::>() { + s.push_value(val.clone(), None); + } else if let Some(s) = any.downcast_ref::() { + s.push_value(val.clone(), None); } else { - panic!("push-series expects a record as the second argument"); + panic!("Object is not a mutable series"); } + return Value::Void; } - panic!("push-series expects a RecordSeries as the first 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); + } + Value::Int(0) }, ); } diff --git a/src/ast/rtl/streams.rs b/src/ast/rtl/streams.rs index 18224c9..e0a2d1f 100644 --- a/src/ast/rtl/streams.rs +++ b/src/ast/rtl/streams.rs @@ -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() { diff --git a/src/ast/types.rs b/src/ast/types.rs index aa9cfb1..234785d 100644 --- a/src/ast/types.rs +++ b/src/ast/types.rs @@ -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; + + /// 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. diff --git a/src/ast/vm.rs b/src/ast/vm.rs index e1d7cc9..0b3eea5 100644 --- a/src/ast/vm.rs +++ b/src/ast/vm.rs @@ -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) { diff --git a/src/bin/ast.rs b/src/bin/ast.rs index 989f484..96fb8da 100644 --- a/src/bin/ast.rs +++ b/src/bin/ast.rs @@ -31,6 +31,10 @@ struct Cli { #[arg(short, long)] trace: bool, + /// Library directories to load before execution + #[arg(short, long)] + lib: Vec, + /// 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) {