Files
RustAst/src/ast/types.rs
T
Michael Schimmel 831525b402 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.
2026-03-05 14:07:42 +01:00

641 lines
22 KiB
Rust

use chrono::{TimeZone, Utc};
use std::any::Any;
use std::cell::RefCell;
use std::collections::HashMap;
use std::fmt;
use std::rc::Rc;
use std::sync::Mutex; // Still needed for global keyword registry
use std::sync::OnceLock;
/// Simple source location
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct SourceLocation {
pub line: u32,
pub col: u32,
}
/// Shared identity for nodes. The identity is defined by the object instance (unique ID).
/// SourceLocation is kept as optional metadata.
#[derive(Debug, Clone)]
pub struct NodeIdentity {
pub id: u64,
pub location: Option<SourceLocation>,
}
impl PartialEq for NodeIdentity {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
}
}
impl Eq for NodeIdentity {}
impl std::hash::Hash for NodeIdentity {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.id.hash(state);
}
}
pub type Identity = Rc<NodeIdentity>;
static NODE_ID_COUNTER: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
impl NodeIdentity {
pub fn next_id() -> u64 {
NODE_ID_COUNTER.fetch_add(1, std::sync::atomic::Ordering::SeqCst)
}
/// Creates a new unique identity at the given location.
pub fn new(location: SourceLocation) -> Identity {
Rc::new(NodeIdentity {
id: Self::next_id(),
location: Some(location),
})
}
/// Creates a new unique identity without location metadata (for synthetic nodes).
pub fn anonymous() -> Identity {
Rc::new(NodeIdentity {
id: Self::next_id(),
location: None,
})
}
}
/// Interned string identifier
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Keyword(pub u32);
static KEYWORD_REGISTRY: OnceLock<Mutex<HashMap<String, u32>>> = OnceLock::new();
static KEYWORD_REVERSE: OnceLock<Mutex<Vec<String>>> = OnceLock::new();
impl Keyword {
pub fn intern(name: &str) -> Self {
let mut reg = KEYWORD_REGISTRY
.get_or_init(|| Mutex::new(HashMap::new()))
.lock()
.unwrap();
if let Some(&id) = reg.get(name) {
Keyword(id)
} else {
let mut rev = KEYWORD_REVERSE
.get_or_init(|| Mutex::new(Vec::new()))
.lock()
.unwrap();
let id = rev.len() as u32;
reg.insert(name.to_string(), id);
rev.push(name.to_string());
Keyword(id)
}
}
pub fn name(&self) -> String {
let rev = KEYWORD_REVERSE
.get_or_init(|| Mutex::new(Vec::new()))
.lock()
.unwrap();
rev[self.0 as usize].clone()
}
}
/// A shared schema for Records, providing O(1) field lookup via FMap optimization.
#[derive(Debug, Clone)]
pub struct RecordLayout {
pub fields: Vec<(Keyword, StaticType)>,
/// Optimization: maps (Keyword.idx - min_idx) to index in fields.
fmap: Vec<i32>,
min_idx: u32,
}
impl PartialEq for RecordLayout {
fn eq(&self, other: &Self) -> bool {
self.fields == other.fields
}
}
impl Eq for RecordLayout {}
impl std::hash::Hash for RecordLayout {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.fields.hash(state);
}
}
type LayoutMap = HashMap<Vec<(Keyword, StaticType)>, std::sync::Arc<RecordLayout>>;
static LAYOUT_REGISTRY: OnceLock<Mutex<LayoutMap>> = OnceLock::new();
impl RecordLayout {
pub fn get_or_create(fields: Vec<(Keyword, StaticType)>) -> std::sync::Arc<Self> {
let mut reg = LAYOUT_REGISTRY
.get_or_init(|| Mutex::new(HashMap::new()))
.lock()
.unwrap();
if let Some(layout) = reg.get(&fields) {
return layout.clone();
}
let mut min_idx = u32::MAX;
let mut max_idx = 0;
for (k, _) in &fields {
min_idx = min_idx.min(k.0);
max_idx = max_idx.max(k.0);
}
let mut fmap = vec![-1; (max_idx - min_idx + 1) as usize];
for (i, (k, _)) in fields.iter().enumerate() {
fmap[(k.0 - min_idx) as usize] = i as i32;
}
let layout = std::sync::Arc::new(RecordLayout {
fields: fields.clone(),
fmap,
min_idx,
});
reg.insert(fields, layout.clone());
layout
}
#[inline(always)]
pub fn index_of(&self, key: Keyword) -> Option<usize> {
if key.0 < self.min_idx {
return None;
}
let offset = (key.0 - self.min_idx) as usize;
if offset >= self.fmap.len() {
return None;
}
let idx = self.fmap[offset];
if idx < 0 { None } else { Some(idx as usize) }
}
}
/// Interface for custom objects (Closures, Series, Streams)
pub trait Object: fmt::Debug {
fn type_name(&self) -> &'static str;
fn as_any(&self) -> &dyn Any;
/// Optional optimization: If this object behaves like a time series (indexable lookbacks),
/// it can return a reference to its Series trait implementation.
fn as_series(&self) -> Option<&dyn Series> {
None
}
}
/// 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<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.
pub type ValueList = Rc<Vec<Value>>;
/// Purity level of an expression or function.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Purity {
/// Has side effects (e.g. Set, Print)
Impure,
/// No side effects, but not deterministic (e.g. now(), random())
SideEffectFree,
/// No side effects and deterministic (e.g. sin(x), 1 + 2)
Pure,
}
pub type NativeFn = dyn Fn(&[Value]) -> Value;
pub type PipeFn = dyn FnMut(&[Value]) -> Value;
/// A native host function with metadata.
pub struct NativeFunction {
pub func: Rc<NativeFn>,
pub purity: Purity,
}
impl fmt::Debug for NativeFunction {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "<native fn, {:?}>", self.purity)
}
}
/// Core data value in Myc Script (similar to TDataValue)
#[derive(Clone)]
pub enum Value {
Void,
Bool(bool),
Int(i64),
Float(f64),
DateTime(i64),
Text(Rc<str>),
Keyword(Keyword),
Tuple(ValueList),
Record(std::sync::Arc<RecordLayout>, ValueList),
FieldAccessor(Keyword),
Function(Rc<NativeFunction>),
Object(Rc<dyn Object>), // For compiled Closures and other opaque types
Cell(Rc<RefCell<Value>>), // Boxed value for captures
TailCallRequest(Box<(Rc<dyn Object>, Vec<Value>)>), // Internal: For TCO (Boxed to keep Value small)
}
impl PartialEq for Value {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Value::Void, Value::Void) => true,
(Value::Bool(a), Value::Bool(b)) => a == b,
(Value::Int(a), Value::Int(b)) => a == b,
(Value::Float(a), Value::Float(b)) => a == b,
(Value::DateTime(a), Value::DateTime(b)) => a == b,
(Value::Text(a), Value::Text(b)) => a == b,
(Value::Keyword(a), Value::Keyword(b)) => a == b,
(Value::Tuple(a), Value::Tuple(b)) => a == b,
(Value::Record(la, va), Value::Record(lb, vb)) => {
std::sync::Arc::ptr_eq(la, lb) && va == vb
}
(Value::FieldAccessor(a), Value::FieldAccessor(b)) => a == b,
(Value::Function(a), Value::Function(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),
(Value::TailCallRequest(a), Value::TailCallRequest(b)) => {
Rc::ptr_eq(&a.0, &b.0) && a.1 == b.1
}
_ => false,
}
}
}
impl PartialOrd for Value {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
match (self, other) {
(Value::Int(a), Value::Int(b)) => a.partial_cmp(b),
(Value::Float(a), Value::Float(b)) => a.partial_cmp(b),
(Value::Int(a), Value::Float(b)) => (*a as f64).partial_cmp(b),
(Value::Float(a), Value::Int(b)) => a.partial_cmp(&(*b as f64)),
(Value::DateTime(a), Value::DateTime(b)) => a.partial_cmp(b),
(Value::Text(a), Value::Text(b)) => a.partial_cmp(b),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Signature {
pub params: StaticType,
pub ret: StaticType,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum StaticType {
Any,
Void,
Bool,
Int,
Float,
DateTime,
Text,
Keyword,
Optional(Box<StaticType>), // Represents T | Void (e.g. for filter pipes)
List(Box<StaticType>), // Legacy / Dynamic list
Series(Box<StaticType>), // Time series of a specific type
Tuple(Vec<StaticType>), // Heterogeneous fixed-size
Vector(Box<StaticType>, usize), // Homogeneous fixed-size
Matrix(Box<StaticType>, Vec<usize>), // Multi-dimensional homogeneous
Record(std::sync::Arc<RecordLayout>),
FieldAccessor(Keyword),
Function(Box<Signature>),
FunctionOverloads(Vec<Signature>),
Object(&'static str),
/// A diagnostic poison type, allowing type-checking to continue after an error.
Error,
}
impl fmt::Display for StaticType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
StaticType::Any => write!(f, "any"),
StaticType::Void => write!(f, "void"),
StaticType::Bool => write!(f, "bool"),
StaticType::Int => write!(f, "int"),
StaticType::Float => write!(f, "float"),
StaticType::DateTime => write!(f, "datetime"),
StaticType::Text => write!(f, "text"),
StaticType::Keyword => write!(f, "keyword"),
StaticType::Optional(inner) => write!(f, "optional({})", inner),
StaticType::List(inner) => write!(f, "list({})", inner),
StaticType::Series(inner) => write!(f, "series<{}>", inner),
StaticType::Tuple(elements) => {
write!(f, "[")?;
for (i, el) in elements.iter().enumerate() {
if i > 0 {
write!(f, " ")?;
}
write!(f, "{}", el)?;
}
write!(f, "]")
}
StaticType::Vector(inner, len) => write!(f, "vector<{}, {}>", inner, len),
StaticType::Matrix(inner, shape) => {
write!(f, "matrix<{}, [", inner)?;
for (i, s) in shape.iter().enumerate() {
if i > 0 {
write!(f, " ")?;
}
write!(f, "{}", s)?;
}
write!(f, "]>")
}
StaticType::Record(layout) => {
write!(f, "{{")?;
for (i, (k, v)) in layout.fields.iter().enumerate() {
if i > 0 {
write!(f, ", ")?;
}
write!(f, ":{} {}", k.name(), v)?;
}
write!(f, "}}")
}
StaticType::FieldAccessor(k) => write!(f, ".{}", k.name()),
StaticType::Function(sig) => {
write!(f, "fn({}) -> {}", sig.params, sig.ret)
}
StaticType::FunctionOverloads(sigs) => {
write!(f, "overloads({} variants)", sigs.len())
}
StaticType::Object(name) => write!(f, "{}", name),
StaticType::Error => write!(f, "<error>"),
}
}
}
impl StaticType {
/// Returns true if `other` can be assigned to a location of type `self`.
pub fn is_assignable_from(&self, other: &StaticType) -> bool {
if self == other
|| matches!(self, StaticType::Any)
|| matches!(other, StaticType::Any)
|| matches!(self, StaticType::Error)
|| matches!(other, StaticType::Error)
{
return true;
}
match (self, other) {
// Optional(T) is assignable from T or Void
(StaticType::Optional(inner), other) => {
matches!(other, StaticType::Void) || inner.is_assignable_from(other)
}
// A Vector is a Tuple
(StaticType::Tuple(elements), StaticType::Vector(inner, len)) => {
if elements.len() != *len {
return false;
}
elements.iter().all(|e| e.is_assignable_from(inner))
}
// Tuple to Tuple (Element-wise)
(StaticType::Tuple(elements), StaticType::Tuple(other_elements)) => {
if elements.len() != other_elements.len() {
return false;
}
elements
.iter()
.zip(other_elements.iter())
.all(|(e, o)| e.is_assignable_from(o))
}
// A Matrix is a Vector (of Vectors/Matrices)
(StaticType::Vector(inner, len), StaticType::Matrix(m_inner, shape)) => {
if shape.is_empty() || shape[0] != *len {
return false;
}
if shape.len() == 1 {
inner.is_assignable_from(m_inner)
} else {
// It's a matrix of higher dimension, so inner must be assignable from a sub-matrix
let sub_shape = shape[1..].to_vec();
inner.is_assignable_from(&StaticType::Matrix(m_inner.clone(), sub_shape))
}
}
// Records are assignable if their layouts match (Structural identity via interning)
(StaticType::Record(a), StaticType::Record(b)) => std::sync::Arc::ptr_eq(a, b),
// Series are assignable if their inner types are assignable
(StaticType::Series(inner_a), StaticType::Series(inner_b)) => {
inner_a.is_assignable_from(inner_b)
}
_ => false,
}
}
/// Tries to resolve a call with the given argument type (usually a Tuple) and returns the return type.
pub fn resolve_call(&self, args_ty: &StaticType) -> Option<StaticType> {
match self {
StaticType::Any => Some(StaticType::Any),
StaticType::FieldAccessor(k) => {
// (.name { :name val })
// The args_ty should be a Tuple of [Record]
let rec_ty = if let StaticType::Tuple(t) = args_ty {
t.first()?
} else {
args_ty
};
match rec_ty {
StaticType::Record(layout) => {
if let Some(idx) = layout.index_of(*k) {
return Some(layout.fields[idx].1.clone());
}
}
StaticType::Any => return Some(StaticType::Any),
_ => {}
}
None
}
StaticType::Function(sig) => {
if sig.params.is_assignable_from(args_ty) {
Some(sig.ret.clone())
} else {
None
}
}
StaticType::FunctionOverloads(sigs) => sigs
.iter()
.find(|sig| sig.params.is_assignable_from(args_ty))
.map(|sig| sig.ret.clone()),
_ => None,
}
}
/// Returns true if this type and all its recursive elements are scalars (Int, Float, etc.)
pub fn is_scalar_pure(&self) -> bool {
match self {
StaticType::Int | StaticType::Float | StaticType::Bool | StaticType::DateTime => true,
StaticType::Tuple(elements) => elements.iter().all(|e| e.is_scalar_pure()),
StaticType::Vector(inner, _) => inner.is_scalar_pure(),
StaticType::Matrix(inner, _) => inner.is_scalar_pure(),
_ => false,
}
}
}
impl Value {
pub fn is_truthy(&self) -> bool {
match self {
Value::Void => false,
Value::Bool(b) => *b,
Value::Cell(c) => c.borrow().is_truthy(),
_ => true,
}
}
/// Returns the underlying values as a slice if this is a Tuple.
pub fn as_slice(&self) -> Option<&[Value]> {
match self {
Value::Tuple(v) => Some(v),
_ => None,
}
}
pub fn make_tuple(values: Vec<Value>) -> Self {
Value::Tuple(Rc::new(values))
}
pub fn make_record(keys: Vec<Keyword>, values: Vec<Value>) -> Self {
let fields: Vec<_> = keys
.into_iter()
.zip(values.iter().map(|v| v.static_type()))
.collect();
let layout = RecordLayout::get_or_create(fields);
Value::Record(layout, Rc::new(values))
}
pub fn make_function(purity: Purity, func: impl Fn(&[Value]) -> Value + 'static) -> Self {
Value::Function(Rc::new(NativeFunction {
func: Rc::new(func),
purity,
}))
}
pub fn static_type(&self) -> StaticType {
match self {
Value::Void => StaticType::Void,
Value::Bool(_) => StaticType::Bool,
Value::Int(_) => StaticType::Int,
Value::Float(_) => StaticType::Float,
Value::DateTime(_) => StaticType::DateTime,
Value::Text(_) => StaticType::Text,
Value::Keyword(_) => StaticType::Keyword,
Value::Tuple(values) => {
if values.is_empty() {
return StaticType::Vector(Box::new(StaticType::Any), 0);
}
let element_types: Vec<_> = values.iter().map(|v| v.static_type()).collect();
// Check for Homogeneity (Vector)
let first_ty = &element_types[0];
let all_same = element_types.iter().all(|t| t == first_ty);
if all_same {
match first_ty {
StaticType::Vector(inner, len) => {
// Possible Matrix
StaticType::Matrix(inner.clone(), vec![values.len(), *len])
}
StaticType::Matrix(inner, shape) => {
let mut new_shape = vec![values.len()];
new_shape.extend(shape);
StaticType::Matrix(inner.clone(), new_shape)
}
_ => StaticType::Vector(Box::new(first_ty.clone()), values.len()),
}
} else {
StaticType::Tuple(element_types)
}
}
Value::Record(layout, _) => StaticType::Record(layout.clone()),
Value::FieldAccessor(k) => StaticType::FieldAccessor(*k),
Value::Function(_) => StaticType::Any, // Dynamic function
Value::Object(o) => StaticType::Object(o.type_name()),
Value::Cell(c) => c.borrow().static_type(),
Value::TailCallRequest(_) => StaticType::Any, // Internal state, but typable as Any
}
}
}
impl fmt::Display for Value {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Value::Void => write!(f, "void"),
Value::Bool(b) => write!(f, "{}", b),
Value::Int(i) => write!(f, "{}", i),
Value::Float(fl) => write!(f, "{}", fl),
Value::DateTime(ts) => match Utc.timestamp_millis_opt(*ts) {
chrono::LocalResult::Single(dt) => {
write!(f, "#{}#", dt.format("%Y-%m-%d %H:%M:%S"))
}
_ => write!(f, "#timestamp({})#", ts),
},
Value::Text(t) => write!(f, "\"{}\"", t),
Value::Keyword(k) => write!(f, ":{}", k.name()),
Value::Tuple(values) => {
write!(f, "[")?;
for (i, val) in values.iter().enumerate() {
if i > 0 {
write!(f, " ")?;
}
write!(f, "{}", val)?;
}
write!(f, "]")
}
Value::Record(layout, values) => {
write!(f, "{{")?;
for i in 0..values.len() {
if i > 0 {
write!(f, ", ")?;
}
write!(f, ":{} {}", layout.fields[i].0.name(), values[i])?;
}
write!(f, "}}")
}
Value::FieldAccessor(k) => write!(f, ".{}", k.name()),
Value::Function(f_meta) => write!(f, "<native fn, {:?}>", f_meta.purity),
Value::Object(o) => {
if let Some(pipe) = o
.as_any()
.downcast_ref::<crate::ast::rtl::streams::PipelineNode>()
{
write!(f, "PipelineNode[last: {:?}]", pipe.series.get_item(0))
} else if let Some(val_series) =
o.as_any()
.downcast_ref::<crate::ast::rtl::series::SharedValueSeries>()
{
write!(
f,
"SharedValueSeries[len: {}]",
val_series.buffer.borrow().len()
)
} else {
write!(f, "<{}>", o.type_name())
}
}
Value::Cell(c) => write!(f, "{}", c.borrow()),
Value::TailCallRequest(_) => write!(f, "<tail call request>"),
}
}
}
impl fmt::Debug for Value {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(self, f)
}
}