Skip to main content

shape_runtime/intrinsics/
rolling.rs

1//! Rolling window intrinsics — partial migration to typed marshal layer.
2//!
3//! Per the intrinsics-typed-CC migration's partial-migration pattern (see
4//! `docs/defections.md` 2026-05-07 intrinsics-typed-CC entry's partial-
5//! migration subsection), 3 of 6 rolling intrinsics migrate to
6//! `register_typed_fn_N` typed entries via [`create_rolling_intrinsics_module`].
7//! 3 polymorphic-input intrinsics remain as legacy `IntrinsicFn` bodies
8//! pending the **M1-split sub-decision extension** (per-element-type intrinsics
9//! for polymorphic-input cases; cross-crate compiler change).
10//! `rolling_sum`'s i64 fast path additionally uses a validity-bitmap return
11//! (`option_i64_vec_to_nb`); migrating it joins `array_transforms::diff` in
12//! the validity-aware-return-variant sub-question.
13//!
14//! Migrated entries take `Arc<Vec<f64>>` (boxed dense f64 storage)
15//! + `i64` (window/period scalar) and return `ConcreteReturn::ArrayF64(Vec<f64>)`
16//! per the dispatcher's `TypedReturn → slot push` projection.
17//!
18//! These functions implement efficient O(n) algorithms for rolling window
19//! operations, critical for technical indicators like SMA, Bollinger Bands, etc.
20//! Uses SIMD acceleration via the simd_rolling module.
21
22use crate::context::ExecutionContext;
23use crate::marshal::register_typed_fn_2;
24use crate::module_exports::ModuleExports;
25use crate::simd_rolling;
26use crate::typed_module_exports::{ConcreteReturn, ConcreteType, TypedReturn};
27use shape_ast::error::{Result, ShapeError};
28use shape_value::KindedSlot;
29use std::sync::Arc;
30
31// ───────────────────── Module factory (3 typed entries) ─────────────────────
32
33/// Create the rolling intrinsics module with 3 typed-marshal entry points
34/// (`__intrinsic_rolling_mean`, `__intrinsic_rolling_std`, `__intrinsic_ema`).
35/// The 3 polymorphic-input intrinsics (`rolling_sum`, `rolling_min`,
36/// `rolling_max`) remain as legacy `IntrinsicFn` bodies in this module until
37/// the M1-split sub-decision extension lands.
38pub fn create_rolling_intrinsics_module() -> ModuleExports {
39    let mut module = ModuleExports::new("std::core::intrinsics::rolling");
40    module.description =
41        "Rolling-window intrinsics (typed entries; polymorphic-input intrinsics stay as legacy bodies pending M1-split sub-decision extension)"
42            .to_string();
43
44    register_typed_fn_2::<_, Arc<Vec<f64>>, i64>(
45        &mut module,
46        "__intrinsic_rolling_mean",
47        "Rolling mean (Simple Moving Average) over a fixed-size window",
48        [("series", "Array<number>"), ("window", "int")],
49        ConcreteType::ArrayNumber,
50        |series, window, _ctx| {
51            let data = series.as_slice();
52            if data.is_empty() {
53                return Ok(TypedReturn::Concrete(ConcreteReturn::ArrayF64(vec![])));
54            }
55            if window <= 0 {
56                return Err("Window size must be greater than 0".to_string());
57            }
58            let result = simd_rolling::rolling_mean(data, window as usize);
59            Ok(TypedReturn::Concrete(ConcreteReturn::ArrayF64(result)))
60        },
61    );
62
63    register_typed_fn_2::<_, Arc<Vec<f64>>, i64>(
64        &mut module,
65        "__intrinsic_rolling_std",
66        "Rolling standard deviation (Welford's algorithm) over a fixed-size window",
67        [("series", "Array<number>"), ("window", "int")],
68        ConcreteType::ArrayNumber,
69        |series, window, _ctx| {
70            let data = series.as_slice();
71            if data.is_empty() {
72                return Ok(TypedReturn::Concrete(ConcreteReturn::ArrayF64(vec![])));
73            }
74            if window <= 0 {
75                return Err("Window size must be greater than 0".to_string());
76            }
77            let window = window as usize;
78            if window > data.len() {
79                return Ok(TypedReturn::Concrete(ConcreteReturn::ArrayF64(vec![
80                    f64::NAN;
81                    data.len()
82                ])));
83            }
84            let result = simd_rolling::rolling_std_welford(data, window);
85            Ok(TypedReturn::Concrete(ConcreteReturn::ArrayF64(result)))
86        },
87    );
88
89    register_typed_fn_2::<_, Arc<Vec<f64>>, i64>(
90        &mut module,
91        "__intrinsic_ema",
92        "Exponential Moving Average with smoothing alpha = 2 / (period + 1)",
93        [("series", "Array<number>"), ("period", "int")],
94        ConcreteType::ArrayNumber,
95        |series, period, _ctx| {
96            let data = series.as_slice();
97            if data.is_empty() {
98                return Ok(TypedReturn::Concrete(ConcreteReturn::ArrayF64(vec![])));
99            }
100            if period <= 0 {
101                return Err("EMA period must be greater than 0".to_string());
102            }
103            let alpha = 2.0 / (period as f64 + 1.0);
104            let mut result = Vec::with_capacity(data.len());
105            let mut ema = data[0];
106            result.push(ema);
107            for &price in &data[1..] {
108                ema = alpha * price + (1.0 - alpha) * ema;
109                result.push(ema);
110            }
111            Ok(TypedReturn::Concrete(ConcreteReturn::ArrayF64(result)))
112        },
113    );
114
115    module
116}
117
118// ───────────────────── Legacy bodies (3 polymorphic-input intrinsics) ─────────────────────
119
120/// Intrinsic: Rolling sum over a window.
121///
122/// **Migration deferred** pending M1-split sub-decision extension
123/// (polymorphic input: `Vec<int>` fast path returns `option_i64_vec_to_nb`
124/// validity-bitmap; `Vec<number>` fallback returns `Vec<f64>` with NaN
125/// sentinels). Joins `array_transforms::diff` in the validity-aware-return
126/// sub-question.
127pub fn intrinsic_rolling_sum(
128    _args: &[KindedSlot],
129    _ctx: &mut ExecutionContext,
130) -> Result<KindedSlot> {
131    Err(ShapeError::RuntimeError {
132        message: "intrinsic_rolling_sum: pending Phase 2c intrinsic kind threading + M1-split — see ADR-006 §2.7.4".to_string(),
133        location: None,
134    })
135}
136
137/// Intrinsic: Rolling minimum. See [`intrinsic_rolling_sum`] for the
138/// Phase 1.B deferral rationale.
139pub fn intrinsic_rolling_min(
140    _args: &[KindedSlot],
141    _ctx: &mut ExecutionContext,
142) -> Result<KindedSlot> {
143    Err(ShapeError::RuntimeError {
144        message: "intrinsic_rolling_min: pending Phase 2c intrinsic kind threading + M1-split — see ADR-006 §2.7.4".to_string(),
145        location: None,
146    })
147}
148
149/// Intrinsic: Rolling maximum. See [`intrinsic_rolling_sum`] for the
150/// Phase 1.B deferral rationale.
151pub fn intrinsic_rolling_max(
152    _args: &[KindedSlot],
153    _ctx: &mut ExecutionContext,
154) -> Result<KindedSlot> {
155    Err(ShapeError::RuntimeError {
156        message: "intrinsic_rolling_max: pending Phase 2c intrinsic kind threading + M1-split — see ADR-006 §2.7.4".to_string(),
157        location: None,
158    })
159}