diff --git a/src/ast/rtl/series/data.rs b/src/ast/rtl/series/data.rs index 6ea513b..938d8ec 100644 --- a/src/ast/rtl/series/data.rs +++ b/src/ast/rtl/series/data.rs @@ -4,7 +4,7 @@ use std::fmt::{self, Debug}; use std::rc::Rc; use std::sync::Arc; -use crate::ast::types::{Keyword, Object, PushableSeries, RecordLayout, Series, StaticType, Value}; +use crate::ast::types::{Keyword, PushableStorage, RecordLayout, SeriesStorage, StaticType, Value}; // ============================================================================ // 1. Core Storage Engine @@ -77,6 +77,8 @@ 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; + /// Returns the `StaticType` corresponding to this scalar type. + fn static_type() -> StaticType; } impl ScalarValue for f64 { @@ -86,6 +88,9 @@ impl ScalarValue for f64 { fn from_value(v: Value) -> Option { v.as_float() } + fn static_type() -> StaticType { + StaticType::Float + } } impl ScalarValue for i64 { fn to_value(self) -> Value { @@ -94,6 +99,9 @@ impl ScalarValue for i64 { fn from_value(v: Value) -> Option { v.as_int() } + fn static_type() -> StaticType { + StaticType::Int + } } impl ScalarValue for bool { fn to_value(self) -> Value { @@ -102,6 +110,9 @@ impl ScalarValue for bool { fn from_value(v: Value) -> Option { if let Value::Bool(b) = v { Some(b) } else { None } } + fn static_type() -> StaticType { + StaticType::Bool + } } /// A highly optimized, type-safe series for primitive values (Float, Int, Bool). @@ -131,9 +142,8 @@ impl Debug for ScalarSeries { } } -// Makes the series usable as a dynamic Object for the VM. -impl Object for ScalarSeries { - fn type_name(&self) -> &'static str { +impl SeriesStorage for ScalarSeries { + fn series_type_name(&self) -> &'static str { self.type_name } fn as_any(&self) -> &dyn Any { @@ -142,23 +152,9 @@ impl Object for ScalarSeries { fn into_rc_any(self: Rc) -> Rc { self } - fn as_series(&self) -> Option<&dyn Series> { - Some(self) + fn inner_static_type(&self) -> StaticType { + T::static_type() } - fn as_pushable_series(&self) -> Option<&dyn PushableSeries> { - Some(self) - } -} - -impl PushableSeries for ScalarSeries { - fn push_value(&self, value: Value) { - if let Some(v) = T::from_value(value) { - self.data.borrow_mut().push(v); - } - } -} - -impl Series for ScalarSeries { fn get_item(&self, index: usize) -> Option { self.data.borrow().get(index).map(|&v| v.to_value()) } @@ -168,6 +164,17 @@ impl Series for ScalarSeries { fn total_count(&self) -> u64 { self.data.borrow().total_count() } + fn as_pushable(&self) -> Option<&dyn PushableStorage> { + Some(self) + } +} + +impl PushableStorage for ScalarSeries { + fn push_value(&self, value: Value) { + if let Some(v) = T::from_value(value) { + self.data.borrow_mut().push(v); + } + } } // ============================================================================ @@ -194,8 +201,8 @@ impl Debug for ValueSeries { } } -impl Object for ValueSeries { - fn type_name(&self) -> &'static str { +impl SeriesStorage for ValueSeries { + fn series_type_name(&self) -> &'static str { "ValueSeries" } fn as_any(&self) -> &dyn Any { @@ -204,21 +211,9 @@ impl Object for ValueSeries { fn into_rc_any(self: Rc) -> Rc { self } - fn as_series(&self) -> Option<&dyn Series> { - Some(self) + fn inner_static_type(&self) -> StaticType { + StaticType::Any } - 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 { self.data.borrow().get(index).cloned() } @@ -228,6 +223,15 @@ impl Series for ValueSeries { fn total_count(&self) -> u64 { self.data.borrow().total_count() } + fn as_pushable(&self) -> Option<&dyn PushableStorage> { + Some(self) + } +} + +impl PushableStorage for ValueSeries { + fn push_value(&self, value: Value) { + self.data.borrow_mut().push(value); + } } // ============================================================================ @@ -235,9 +239,8 @@ impl Series for ValueSeries { // ============================================================================ /// 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 {} +/// Ensures each field buffer supports indexed access, dynamic dispatch, and value insertion. +pub trait SeriesMember: PushableStorage {} impl SeriesMember for ScalarSeries {} impl SeriesMember for ScalarSeries {} @@ -316,13 +319,13 @@ impl Debug for RecordSeries { f, "RecordSeries[fields: {}, len: {}]", self.fields.len(), - Series::len(self) + self.len() ) } } -impl Object for RecordSeries { - fn type_name(&self) -> &'static str { +impl SeriesStorage for RecordSeries { + fn series_type_name(&self) -> &'static str { "RecordSeries" } fn as_any(&self) -> &dyn Any { @@ -331,29 +334,9 @@ impl Object for RecordSeries { fn into_rc_any(self: Rc) -> Rc { self } - fn as_series(&self) -> Option<&dyn Series> { - Some(self) + fn inner_static_type(&self) -> StaticType { + StaticType::Record(self.layout.clone()) } - 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 { self.get_record(index) } @@ -366,6 +349,23 @@ impl Series for RecordSeries { .map(|f| f.borrow().total_count()) .unwrap_or(0) } + fn as_pushable(&self) -> Option<&dyn PushableStorage> { + Some(self) + } +} + +impl PushableStorage 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"); + } + } } // ============================================================================ @@ -390,13 +390,13 @@ impl Debug for SeriesView { f, "SeriesView[field: {}, len: {}]", self.field_name.name(), - Series::len(self) + self.len() ) } } -impl Object for SeriesView { - fn type_name(&self) -> &'static str { +impl SeriesStorage for SeriesView { + fn series_type_name(&self) -> &'static str { "SeriesView" } fn as_any(&self) -> &dyn Any { @@ -405,12 +405,9 @@ impl Object for SeriesView { fn into_rc_any(self: Rc) -> Rc { self } - fn as_series(&self) -> Option<&dyn Series> { - Some(self) + fn inner_static_type(&self) -> StaticType { + self.inner.borrow().inner_static_type() } -} - -impl Series for SeriesView { fn get_item(&self, index: usize) -> Option { self.inner.borrow().get_item(index) } @@ -444,8 +441,8 @@ impl Debug for SharedSeries { } } -impl Object for SharedSeries { - fn type_name(&self) -> &'static str { +impl SeriesStorage for SharedSeries { + fn series_type_name(&self) -> &'static str { self.type_name } fn as_any(&self) -> &dyn Any { @@ -454,12 +451,9 @@ impl Object for SharedSeries { fn into_rc_any(self: Rc) -> Rc { self } - fn as_series(&self) -> Option<&dyn Series> { - Some(self) + fn inner_static_type(&self) -> StaticType { + T::static_type() } -} - -impl Series for SharedSeries { fn get_item(&self, index: usize) -> Option { self.buffer .borrow() @@ -485,8 +479,8 @@ impl Debug for SharedValueSeries { } } -impl Object for SharedValueSeries { - fn type_name(&self) -> &'static str { +impl SeriesStorage for SharedValueSeries { + fn series_type_name(&self) -> &'static str { "SharedValueSeries" } fn as_any(&self) -> &dyn Any { @@ -495,12 +489,9 @@ impl Object for SharedValueSeries { fn into_rc_any(self: Rc) -> Rc { self } - fn as_series(&self) -> Option<&dyn Series> { - Some(self) + fn inner_static_type(&self) -> StaticType { + StaticType::Any } -} - -impl Series for SharedValueSeries { fn get_item(&self, index: usize) -> Option { self.buffer.borrow().get(index).cloned() } @@ -524,13 +515,13 @@ impl Debug for SharedRecordSeries { f, "SharedRecordSeries[fields: {}, len: {}]", self.field_buffers.len(), - Series::len(self) + self.len() ) } } -impl Object for SharedRecordSeries { - fn type_name(&self) -> &'static str { +impl SeriesStorage for SharedRecordSeries { + fn series_type_name(&self) -> &'static str { "SharedRecordSeries" } fn as_any(&self) -> &dyn Any { @@ -539,12 +530,9 @@ impl Object for SharedRecordSeries { fn into_rc_any(self: Rc) -> Rc { self } - fn as_series(&self) -> Option<&dyn Series> { - Some(self) + fn inner_static_type(&self) -> StaticType { + StaticType::Record(self.layout.clone()) } -} - -impl Series for SharedRecordSeries { fn get_item(&self, index: usize) -> Option { if self.field_buffers.is_empty() { return None; diff --git a/src/ast/rtl/series/mod.rs b/src/ast/rtl/series/mod.rs index d977c08..405ab64 100644 --- a/src/ast/rtl/series/mod.rs +++ b/src/ast/rtl/series/mod.rs @@ -25,10 +25,10 @@ pub fn register(env: &Environment) { match arg { Value::Keyword(k) => { match k.name().to_lowercase().as_str() { - "float" => Value::Object(Rc::new(ScalarSeries::::new("FloatSeries", lookback_limit))), - "int" | "datetime" => Value::Object(Rc::new(ScalarSeries::::new("IntSeries", lookback_limit))), - "bool" => Value::Object(Rc::new(ScalarSeries::::new("BoolSeries", lookback_limit))), - "text" => Value::Object(Rc::new(ValueSeries::new(lookback_limit))), + "float" => Value::Series(Rc::new(ScalarSeries::::new("FloatSeries", lookback_limit))), + "int" | "datetime" => Value::Series(Rc::new(ScalarSeries::::new("IntSeries", lookback_limit))), + "bool" => Value::Series(Rc::new(ScalarSeries::::new("BoolSeries", lookback_limit))), + "text" => Value::Series(Rc::new(ValueSeries::new(lookback_limit))), _ => panic!("Unknown or unsupported series type keyword: :{}", k.name()), } } @@ -51,7 +51,7 @@ pub fn register(env: &Environment) { fields.push((*name, ty)); } let final_layout = RecordLayout::get_or_create(fields); - Value::Object(Rc::new(RecordSeries::new(final_layout, lookback_limit))) + Value::Series(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}})"), } @@ -69,14 +69,14 @@ pub fn register(env: &Environment) { |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; + if let Value::Series(s) = series_val { + match s.as_pushable() { + Some(p) => p.push_value(val.clone()), + None => panic!("push expects a mutable series, got read-only {}", s.series_type_name()), } - panic!("push expects a pushable series, got: {}", obj.type_name()); + return Value::Void; } - panic!("push expects a series object as the first argument") + panic!("push expects a series as the first argument") }, ); @@ -89,9 +89,7 @@ pub fn register(env: &Environment) { })), Purity::Pure, |args: &[Value]| { - if let Value::Object(obj) = &args[0] - && let Some(s) = obj.as_series() - { + if let Value::Series(s) = &args[0] { return Value::Int(s.len() as i64); } Value::Int(0) diff --git a/src/ast/types.rs b/src/ast/types.rs index 4155a49..a8da050 100644 --- a/src/ast/types.rs +++ b/src/ast/types.rs @@ -177,7 +177,8 @@ impl RecordLayout { } } -/// Interface for custom objects (Series, Streams, SyntaxNodes, and custom extensions). +/// Interface for custom RTL objects (Streams and custom extensions). +/// For series data, use `Value::Series` with the `SeriesStorage` trait instead. pub trait Object: fmt::Debug { fn type_name(&self) -> &'static str; fn as_any(&self) -> &dyn Any; @@ -185,26 +186,20 @@ pub trait Object: fmt::Debug { /// Converts `Rc` into `Rc`, enabling zero-copy downcasting to a concrete /// `Rc` via `Rc::downcast::()`. Requires `arbitrary_self_types` (stable since 1.81). fn into_rc_any(self: Rc) -> Rc; - - /// If this object supports indexed lookback access, returns a `Series` reference. - fn as_series(&self) -> Option<&dyn Series> { - None - } - - /// If this object supports pushing new values, returns a `PushableSeries` reference. - fn as_pushable_series(&self) -> Option<&dyn PushableSeries> { - None - } } -/// Extension of `Object + Series` for types that also accept new values via `push_value`. -/// Only series types that are mutable (ScalarSeries, ValueSeries, RecordSeries) implement this. -pub trait PushableSeries: Object + Series { - fn push_value(&self, val: Value); -} +/// Read interface for all series types: indexed lookback access and type metadata. +pub trait SeriesStorage: fmt::Debug { + fn series_type_name(&self) -> &'static str; + fn as_any(&self) -> &dyn Any; + + /// Converts `Rc` into `Rc` for zero-copy concrete downcast. + fn into_rc_any(self: Rc) -> Rc; + + /// Returns the inner `StaticType` of this series' elements + /// (e.g. `StaticType::Float` for a float series, `StaticType::Record(layout)` for a record series). + fn inner_static_type(&self) -> StaticType; -/// Unified interface for all series types (Local RecordSeries, SeriesViews, and future SharedSeries). -pub trait Series { /// Gets the value at the specified lookback index (0 = newest). fn get_item(&self, index: usize) -> Option; @@ -218,6 +213,17 @@ pub trait Series { fn is_empty(&self) -> bool { self.len() == 0 } + + /// Returns a write interface if this series supports push. Read-only series return `None`. + fn as_pushable(&self) -> Option<&dyn PushableStorage> { + None + } +} + +/// Write interface for mutable series (ScalarSeries, ValueSeries, RecordSeries). +/// Obtained via `SeriesStorage::as_pushable()`. +pub trait PushableStorage: SeriesStorage { + fn push_value(&self, val: Value); } /// A shared sequence of values, used by both Tuples and Records. @@ -265,7 +271,8 @@ pub enum Value { Function(Rc), Closure(Rc), // Compiled function with captured upvalues Quote(Rc), // Quoted AST node (from 'expr or macro expansion) - Object(Rc), // RTL extensions: Series, Streams, and custom types + Series(Rc), // Time series: ScalarSeries, RecordSeries, SeriesView, etc. + Object(Rc), // RTL extensions: Streams and custom types Cell(Rc>), // Boxed value for captures } @@ -287,6 +294,7 @@ impl PartialEq for Value { (Value::Function(a), Value::Function(b)) => Rc::ptr_eq(a, b), (Value::Closure(a), Value::Closure(b)) => Rc::ptr_eq(a, b), (Value::Quote(a), Value::Quote(b)) => Rc::ptr_eq(a, b), + (Value::Series(a), Value::Series(b)) => Rc::ptr_eq(a, b), (Value::Object(a), Value::Object(b)) => Rc::ptr_eq(a, b), (Value::Cell(a), Value::Cell(b)) => Rc::ptr_eq(a, b), _ => false, @@ -645,6 +653,7 @@ impl Value { Value::Function(_) => StaticType::Any, // Dynamic function Value::Closure(_) => StaticType::Object("closure"), Value::Quote(_) => StaticType::Object("ast-node"), + Value::Series(s) => StaticType::Series(Box::new(s.inner_static_type())), Value::Object(o) => StaticType::Object(o.type_name()), Value::Cell(c) => c.borrow().static_type(), } @@ -690,6 +699,7 @@ impl fmt::Display for Value { Value::Function(f_meta) => write!(f, "", f_meta.purity), Value::Closure(_) => write!(f, ""), Value::Quote(_) => write!(f, ""), + Value::Series(s) => write!(f, "", s.series_type_name()), Value::Object(o) => write!(f, "<{:?}>", o), Value::Cell(c) => write!(f, "{}", c.borrow()), } diff --git a/src/ast/vm.rs b/src/ast/vm.rs index 9432413..a402953 100644 --- a/src/ast/vm.rs +++ b/src/ast/vm.rs @@ -138,37 +138,36 @@ impl VM { result = self.eval_observed(observer, &closure.exec_node); self.frames.pop(); } - Value::Object(obj) => { - if let Some(series) = obj.as_series() { - if next_args.len() != 1 { - return Err(format!( - "{} indexer expects exactly 1 argument (the lookback index), got {}", - obj.type_name(), - next_args.len() - )); - } - if let Value::Int(idx) = &next_args[0] { - if *idx < 0 { - return Err(format!( - "{} lookback index cannot be negative: {}", - obj.type_name(), - idx - )); - } - result = Ok(series.get_item(*idx as usize).unwrap_or(Value::Void)); - } else { - return Err(format!( - "{} index must be an integer, got {}", - obj.type_name(), - next_args[0] - )); - } - } else { + Value::Series(s) => { + if next_args.len() != 1 { return Err(format!( - "Tail call target is not callable: {}", - obj.type_name() + "{} indexer expects exactly 1 argument (the lookback index), got {}", + s.series_type_name(), + next_args.len() )); } + if let Value::Int(idx) = &next_args[0] { + if *idx < 0 { + return Err(format!( + "{} lookback index cannot be negative: {}", + s.series_type_name(), + idx + )); + } + result = Ok(s.get_item(*idx as usize).unwrap_or(Value::Void)); + } else { + return Err(format!( + "{} index must be an integer, got {}", + s.series_type_name(), + next_args[0] + )); + } + } + Value::Object(obj) => { + return Err(format!( + "Tail call target is not callable: {}", + obj.type_name() + )); } other => { return Err(format!("Tail call target is not callable: {}", other)); @@ -348,21 +347,26 @@ impl VM { Err(format!("Record does not have field :{}", field.name())) } } - Value::Object(obj) => { - let any_ptr = obj.as_any(); - if let Some(record_series) = - any_ptr.downcast_ref::() - { + Value::Series(s) => { + if let Some(record_series) = s.as_any().downcast_ref::() { if let Some(field_series) = record_series.field(*field) { - let view = - SeriesView::new(field_series, *field); - return Ok(Value::Object(std::rc::Rc::new(view))); } else { + let view = SeriesView::new(field_series, *field); + return Ok(Value::Series(std::rc::Rc::new(view))); + } else { return Err(format!( "RecordSeries does not have field :{}", field.name() )); } - } else if let Some(sn) = obj.as_any().downcast_ref::() { + } + Err(format!( + "Field accessor .{} expects a record or RecordSeries, got series:{}", + field.name(), + s.series_type_name() + )) + } + Value::Object(obj) => { + if let Some(sn) = obj.as_any().downcast_ref::() { let mapped = build_map_stream(sn.inner.clone(), *field); return Ok(Value::Object(Rc::new(mapped))); } @@ -433,7 +437,7 @@ impl VM { self.stack.truncate(base); match func_val { - Value::Closure(_) | Value::Object(_) => { + Value::Closure(_) | Value::Object(_) | Value::Series(_) => { self.tail_call = Some((func_val, arg_vals)); return Ok(Value::Void); } @@ -456,33 +460,33 @@ impl VM { 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::() - { + } else if let Value::Series(s) = rec { + if let Some(rs) = s.as_any().downcast_ref::() { if let Some(field_series) = rs.field(k) { - let view = SeriesView::new( - field_series, - k, - ); - return Ok(Value::Object(std::rc::Rc::new(view))); - } else { + let view = SeriesView::new(field_series, k); + return Ok(Value::Series(std::rc::Rc::new(view))); + } else { return Err(format!( "RecordSeries does not have field :{}", k.name() )); - } - } else if let Some(sn) = obj.as_any().downcast_ref::() { - let mapped = build_map_stream(sn.inner.clone(), k); - return Ok(Value::Object(Rc::new(mapped))); - } - return Err(format!( - "Field accessor .{} expects a record, RecordSeries or Stream, got {}", - k.name(), - obj.type_name() - )); + } + } + return Err(format!( + "Field accessor .{} expects a record or RecordSeries, got series:{}", + k.name(), + s.series_type_name() + )); + } else if let Value::Object(obj) = rec { + if let Some(sn) = obj.as_any().downcast_ref::() { + let mapped = build_map_stream(sn.inner.clone(), k); + return Ok(Value::Object(Rc::new(mapped))); + } + return Err(format!( + "Field accessor .{} expects a record, RecordSeries or Stream, got {}", + k.name(), + obj.type_name() + )); } else { return Err(format!( "Field accessor .{} expects a record, RecordSeries or Stream, got {}", @@ -526,24 +530,26 @@ impl VM { } else { Err(format!("Record does not have field :{}", k.name())) } - } else if let Value::Object(obj) = rec { - if let Some(rs) = obj - .as_any() - .downcast_ref::() - { + } else if let Value::Series(s) = rec { + if let Some(rs) = s.as_any().downcast_ref::() { if let Some(field_series) = rs.field(*k) { - let view = SeriesView::new( - field_series, - *k, - ); - Ok(Value::Object(std::rc::Rc::new(view))) + let view = SeriesView::new(field_series, *k); + Ok(Value::Series(std::rc::Rc::new(view))) } else { Err(format!( "RecordSeries does not have field :{}", k.name() )) } - } else if let Some(sn) = obj.as_any().downcast_ref::() { + } else { + Err(format!( + "Field accessor .{} expects a record or RecordSeries, got series:{}", + k.name(), + s.series_type_name() + )) + } + } else if let Value::Object(obj) = rec { + if let Some(sn) = obj.as_any().downcast_ref::() { let mapped = build_map_stream(sn.inner.clone(), *k); Ok(Value::Object(Rc::new(mapped))) } else { @@ -612,37 +618,36 @@ impl VM { self.frames.pop(); res } - Value::Object(obj) => { - if let Some(series) = obj.as_series() { - let arg_len = self.stack.len() - base; - let res = if arg_len != 1 { + Value::Series(s) => { + let arg_len = self.stack.len() - base; + let res = if arg_len != 1 { + Err(format!( + "{} indexer expects exactly 1 argument (the lookback index)", + s.series_type_name() + )) + } else if let Value::Int(idx) = self.stack[base] { + if idx < 0 { Err(format!( - "{} indexer expects exactly 1 argument (the lookback index)", - obj.type_name() + "{} lookback index cannot be negative", + s.series_type_name() )) - } else if let Value::Int(idx) = self.stack[base] { - if idx < 0 { - Err(format!( - "{} lookback index cannot be negative", - obj.type_name() - )) - } else if let Some(val) = series.get_item(idx as usize) { - Ok(val) - } else { - Ok(Value::Void) - } + } else if let Some(val) = s.get_item(idx as usize) { + Ok(val) } else { - Err(format!( - "{} index must be an integer", - obj.type_name() - )) - }; - self.stack.truncate(base); - return res; + Ok(Value::Void) + } } else { - self.stack.truncate(base); - return Err(format!("Object is not callable: {}", obj.type_name())); - } + Err(format!( + "{} index must be an integer", + s.series_type_name() + )) + }; + self.stack.truncate(base); + return res; + } + Value::Object(obj) => { + self.stack.truncate(base); + return Err(format!("Object is not callable: {}", obj.type_name())); } _ => { self.stack.truncate(base); @@ -660,6 +665,7 @@ impl VM { Value::Function(_) => "Function", Value::Closure(_) => "Closure", Value::Quote(_) => "Quote", + Value::Series(_) => "Series", Value::Object(_) => "Object", Value::Cell(_) => "Cell", };