Files
RustAst/src/ast/rtl/series/mod.rs
T
Brummel 141ae36ede Refactor: Create typed series factory function
Introduces `create_typed_series` to centralize series creation logic.
This function dispatches on `StaticType` to select the appropriate
`SeriesStorage` implementation,
improving code reuse and maintainability within the series RTL.

The `SeriesHook` compiler hook is updated to use this new factory
function.
Additionally, new tests have been added to `rtl/streams.rs` and
`tests/pipeline.rs`
to specifically cover the behavior of `pipe-buffered`, focusing on its
fill gate
mechanism and multi-input handling.
2026-03-30 10:38:28 +02:00

231 lines
9.2 KiB
Rust

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<f64>`
/// - `Int`/`DateTime` → `ScalarSeries<i64>`
/// - `Bool` → `ScalarSeries<bool>`
/// - `Record` → `RecordSeries` (SoA layout)
/// - Anything else → `ValueSeries` (boxed fallback)
pub fn create_typed_series(element_type: &StaticType, lookback: usize) -> Rc<dyn SeriesStorage> {
match element_type {
StaticType::Float => Rc::new(ScalarSeries::<f64>::new("FloatSeries", lookback)),
StaticType::Int | StaticType::DateTime => Rc::new(ScalarSeries::<i64>::new("IntSeries", lookback)),
StaticType::Bool => Rc::new(ScalarSeries::<bool>::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::<f64>` for
/// Float, `RecordSeries` with a captured `Arc<RecordLayout>` 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<TypedNode>,
args: Rc<TypedNode>,
node_ty: &StaticType,
_subst: &HashMap<u32, StaticType>,
) -> Option<NodeKind<TypedPhase>> {
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)"]);
}