pub mod data; pub use data::*; use std::collections::HashMap; use std::rc::Rc; use std::sync::Arc; use crate::ast::compiler::call_hooks::{InferenceAccess, RtlCompilerHook}; use crate::ast::diagnostics::Diagnostics; use crate::ast::environment::Environment; use crate::ast::nodes::{IdentifierBinding, Node, NodeKind, TypedNode, TypedPhase}; use crate::ast::types::{NativeFunction, NodeIdentity, Purity, SeriesStorage, Signature, SourceLocation, StaticType, Value}; /// Creates a type-specialized series with the given lookback depth. /// Dispatches on `StaticType` to allocate the optimal storage backend: /// - `Float` → `ScalarSeries` /// - `Int`/`DateTime` → `ScalarSeries` /// - `Bool` → `ScalarSeries` /// - `Record` → `RecordSeries` (SoA layout) /// - Anything else → `ValueSeries` (boxed fallback) pub fn create_typed_series(element_type: &StaticType, lookback: usize) -> Rc { match element_type { StaticType::Float => Rc::new(ScalarSeries::::new("FloatSeries", lookback)), StaticType::Int | StaticType::DateTime => Rc::new(ScalarSeries::::new("IntSeries", lookback)), StaticType::Bool => Rc::new(ScalarSeries::::new("BoolSeries", lookback)), StaticType::Record(layout) => Rc::new(RecordSeries::new(Arc::clone(layout), lookback)), _ => Rc::new(ValueSeries::new(lookback)), } } // ============================================================================ // 4. Compiler Hooks (registered via RTL bootstrap, no name coupling) // ============================================================================ /// Compiler hook for `(series n)`. /// /// - **post_call:** Replaces the `Series(Any)` return type with a fresh TypeVar /// so that subsequent `push` calls can unify the element type (HM inference). /// - **finalize:** Replaces the callee with a pre-configured factory closure that /// directly allocates the correct storage type (e.g. `ScalarSeries::` for /// Float, `RecordSeries` with a captured `Arc` for record types). pub struct SeriesHook; impl RtlCompilerHook for SeriesHook { fn post_call( &self, _args: &TypedNode, ret_ty: StaticType, ctx: &dyn InferenceAccess, _diag: &mut Diagnostics, ) -> StaticType { if let StaticType::Series(inner) = &ret_ty && **inner == StaticType::Any { return StaticType::Series(Box::new(ctx.fresh_var())); } ret_ty } fn finalize( &self, _callee: Rc, args: Rc, node_ty: &StaticType, _subst: &HashMap, ) -> Option> { let StaticType::Series(inner) = node_ty else { return None }; let NodeKind::Tuple { elements } = &args.kind else { return None }; if elements.len() != 1 { return None; } // Build a factory Value::Function whose closure already knows the exact // storage type — no keyword encoding, no runtime dispatch. let elem_ty = inner.as_ref().clone(); let factory_value: Value = Value::Function(Rc::new(NativeFunction { func: Rc::new(move |args: &[Value]| { let n = args[0].as_int().unwrap_or(0) as usize; Value::Series(create_typed_series(&elem_ty, n)) }), purity: Purity::Impure, })); let factory_node = Rc::new(Node { kind: NodeKind::Constant(factory_value), ty: StaticType::Any, identity: NodeIdentity::new(SourceLocation { line: 0, col: 0 }), comments: Rc::from([]), }); Some(NodeKind::Call { callee: factory_node, args: Rc::clone(&args) }) } } /// Compiler hook for `(push series val)`. /// /// Unifies the series element TypeVar with the pushed value type, applying /// Int→Float numeric promotion and record field promotion when needed. /// /// We intentionally do NOT bake the resolved type into ctx after normal /// unification. Keeping `Series(TypeVar(n))` in ctx allows a later push /// to recover the TypeVar ID and rebind it (e.g. Int → Float promotion). /// All identifier lookups call `apply_subst`, so the resolved type is /// always correct regardless of what ctx stores. pub struct PushHook; impl RtlCompilerHook for PushHook { fn post_call( &self, args: &TypedNode, ret_ty: StaticType, ctx: &dyn InferenceAccess, diag: &mut Diagnostics, ) -> StaticType { let NodeKind::Tuple { elements } = &args.kind else { return ret_ty }; if elements.len() < 2 { return ret_ty; } let series_arg = &elements[0]; let value_arg = &elements[1]; let StaticType::Series(inner) = &series_arg.ty else { return ret_ty }; let inner_ty = (**inner).clone(); let val_ty = value_arg.ty.clone(); if let NodeKind::Identifier { binding: IdentifierBinding::Reference(addr), .. } = &series_arg.kind { // Recover the raw inner type from the scope slot (before apply_subst) // so we can find the TypeVar ID even after the first push resolved it. let raw_inner = match ctx.get_slot_type(*addr) { StaticType::Series(ri) => *ri, _ => inner_ty.clone(), }; if let Some(promoted) = ctx .try_numeric_widen(&inner_ty, &val_ty) .or_else(|| ctx.try_record_promote(&inner_ty, &val_ty)) { // Rebind the TypeVar to the promoted type so that finalize // injects the correct schema (e.g. :float instead of :int). if let StaticType::TypeVar(n) = raw_inner { ctx.bind_typevar(n, promoted.clone()); } } else { ctx.unify(inner_ty, val_ty, diag); } } else { ctx.unify(inner_ty, val_ty, diag); } ret_ty } } // ============================================================================ // 5. Script Integration (RTL Registration) // ============================================================================ pub fn register(env: &Environment) { // (series lookback_limit) -> Series // The element type is inferred by the HM type checker from subsequent push calls. // After type checking, the compiler elaborates (series n) into (series n schema) // so the runtime always receives the explicit schema argument. env.register_native_fn( "series", StaticType::Function(Box::new(Signature { params: StaticType::Tuple(vec![StaticType::Int]), ret: StaticType::Series(Box::new(StaticType::Any)), })), Purity::Impure, |args: &[Value]| { // The finalize hook replaces this call with a pre-configured factory // for resolved element types. This fallback only runs when the element // type could not be determined at compile time (e.g. push inside a // nested closure the type checker could not reach). let lookback_limit = args[0].as_int().unwrap_or(0) as usize; Value::Series(Rc::new(ValueSeries::new(lookback_limit))) }, ).with_compiler_hook(Rc::new(SeriesHook)) .doc("Creates a new series (ring buffer) with the given lookback limit.") .description("Takes a single lookback limit (int). The element type is inferred at compile time from subsequent push calls via HM type inference. The compiler injects a second schema argument automatically; the runtime also accepts an explicit schema for cases where inference is not possible.") .examples(&[ "(series 100)", "(do (def s (series 5)) (push s 1.5) (s 0))", ]); // (push series value) -> Void env.register_native_fn( "push", StaticType::Function(Box::new(Signature { params: StaticType::Tuple(vec![StaticType::Any, StaticType::Any]), ret: StaticType::Void, })), Purity::Impure, |args: &[Value]| { let (series_val, val) = (&args[0], &args[1]); 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()), } return Value::Void; } panic!("push expects a series as the first argument") }, ).with_compiler_hook(Rc::new(PushHook)) .doc("Pushes a new value into a series. After push, index 0 returns this value.") .examples(&[ "(push my-ticks {:price 10.5 :volume 100})", "(push prices 42.0)", ]); // (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::Series(s) = &args[0] { return Value::Int(s.len() as i64); } Value::Int(0) }, ).doc("Returns the current number of items stored in a series.") .examples(&["(len my-ticks)"]); }