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:
+170
-119
@@ -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)");
|
||||
}
|
||||
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::<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()),
|
||||
}
|
||||
}
|
||||
_ => 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>()
|
||||
} 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::<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 {
|
||||
panic!("push to RecordSeries expects a record");
|
||||
}
|
||||
return Value::Void;
|
||||
} 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!("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)
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user