Skip to main content

shape_runtime/intrinsics/
array_transforms.rs

1//! Array-transform 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), 6 of 8 array-transform intrinsics migrate to
6//! `register_typed_fn_N` typed entries via [`create_array_transforms_module`].
7//! 2 polymorphic intrinsics (diff, cumsum) remain as legacy `IntrinsicFn`
8//! bodies pending the M1-split sub-decision (per-element-type intrinsics
9//! for polymorphic-return cases; cross-crate compiler change). diff
10//! additionally needs a validity-aware return variant for its i64 fast
11//! path (`option_i64_vec_to_nb` carries a validity bitmap; current
12//! `ConcreteReturn::ArrayI64(Vec<i64>)` does not).
13
14use crate::context::ExecutionContext;
15use crate::marshal::{
16    register_typed_fn_1, register_typed_fn_2, register_typed_fn_2_full, register_typed_fn_3,
17};
18use crate::module_exports::{ModuleExports, ModuleParam};
19use crate::typed_module_exports::{ConcreteReturn, ConcreteType, TypedReturn};
20use shape_ast::error::{Result, ShapeError};
21use shape_value::KindedSlot;
22use std::sync::Arc;
23
24// ───────────────────── Module factory (6 typed entries) ─────────────────────
25
26/// Create the array-transforms intrinsics module with 6 typed-marshal entry
27/// points. The 2 polymorphic intrinsics (diff, cumsum) remain as legacy
28/// `IntrinsicFn` bodies in this module until their M1-split sub-decision lands.
29pub fn create_array_transforms_module() -> ModuleExports {
30    let mut module = ModuleExports::new("std::core::intrinsics::array_transforms");
31    module.description =
32        "Array-transform intrinsics (typed entries; polymorphic-shape intrinsics stay as legacy bodies pending M1-split sub-decision)"
33            .to_string();
34
35    register_typed_fn_1::<_, Arc<Vec<f64>>>(
36        &mut module,
37        "__intrinsic_series",
38        "Identity column projection of a Vec<number>",
39        "input",
40        "Array<number>",
41        ConcreteType::ArrayNumber,
42        |input, _ctx| {
43            Ok(TypedReturn::Concrete(ConcreteReturn::ArrayF64(
44                input.as_slice().to_vec(),
45            )))
46        },
47    );
48
49    register_typed_fn_2::<_, Arc<Vec<f64>>, i64>(
50        &mut module,
51        "__intrinsic_shift",
52        "Shift a Vec<number> by N positions, padding with NaN",
53        [("series", "Array<number>"), ("shift", "int")],
54        ConcreteType::ArrayNumber,
55        |series, shift, _ctx| {
56            let data = series.as_slice();
57            let mut result = Vec::with_capacity(data.len());
58            if shift > 0 {
59                let s = (shift as usize).min(data.len());
60                for _ in 0..s {
61                    result.push(f64::NAN);
62                }
63                result.extend_from_slice(&data[..data.len().saturating_sub(s)]);
64            } else if shift < 0 {
65                let s = ((-shift) as usize).min(data.len());
66                result.extend_from_slice(&data[s..]);
67                for _ in 0..s {
68                    result.push(f64::NAN);
69                }
70            } else {
71                result.extend_from_slice(data);
72            }
73            Ok(TypedReturn::Concrete(ConcreteReturn::ArrayF64(result)))
74        },
75    );
76
77    register_typed_fn_2_full::<_, Arc<Vec<f64>>, i64>(
78        &mut module,
79        "__intrinsic_pct_change",
80        "Percent change between consecutive (or period-spaced) Vec<number> elements",
81        [
82            ModuleParam {
83                name: "series".to_string(),
84                type_name: "Array<number>".to_string(),
85                required: true,
86                description: "Input series".to_string(),
87                ..Default::default()
88            },
89            ModuleParam {
90                name: "period".to_string(),
91                type_name: "int".to_string(),
92                required: false,
93                description: "Period for change comparison (default 1)".to_string(),
94                default_snippet: Some("1".to_string()),
95                ..Default::default()
96            },
97        ],
98        ConcreteType::ArrayNumber,
99        |series, period, _ctx| {
100            let data = series.as_slice();
101            let period = period.max(0) as usize;
102            let mut result = Vec::with_capacity(data.len());
103            for i in 0..data.len() {
104                if i < period {
105                    result.push(f64::NAN);
106                } else {
107                    let prev = data[i - period];
108                    if prev == 0.0 {
109                        result.push(f64::NAN);
110                    } else {
111                        result.push((data[i] - prev) / prev);
112                    }
113                }
114            }
115            Ok(TypedReturn::Concrete(ConcreteReturn::ArrayF64(result)))
116        },
117    );
118
119    register_typed_fn_2::<_, Arc<Vec<f64>>, f64>(
120        &mut module,
121        "__intrinsic_fillna",
122        "Replace NaN entries in a Vec<number> with a fill value",
123        [("series", "Array<number>"), ("value", "number")],
124        ConcreteType::ArrayNumber,
125        |series, value, _ctx| {
126            let result: Vec<f64> = series
127                .as_slice()
128                .iter()
129                .map(|&v| if v.is_nan() { value } else { v })
130                .collect();
131            Ok(TypedReturn::Concrete(ConcreteReturn::ArrayF64(result)))
132        },
133    );
134
135    register_typed_fn_1::<_, Arc<Vec<f64>>>(
136        &mut module,
137        "__intrinsic_cumprod",
138        "Cumulative product of a Vec<number>",
139        "input",
140        "Array<number>",
141        ConcreteType::ArrayNumber,
142        |input, _ctx| {
143            let data = input.as_slice();
144            let mut result = Vec::with_capacity(data.len());
145            let mut acc = 1.0;
146            for &v in data {
147                acc *= v;
148                result.push(acc);
149            }
150            Ok(TypedReturn::Concrete(ConcreteReturn::ArrayF64(result)))
151        },
152    );
153
154    register_typed_fn_3::<_, Arc<Vec<f64>>, f64, f64>(
155        &mut module,
156        "__intrinsic_clip",
157        "Clip Vec<number> elements to the [min, max] interval",
158        [
159            ("series", "Array<number>"),
160            ("min", "number"),
161            ("max", "number"),
162        ],
163        ConcreteType::ArrayNumber,
164        |series, min, max, _ctx| {
165            let result: Vec<f64> = series
166                .as_slice()
167                .iter()
168                .map(|&v| v.max(min).min(max))
169                .collect();
170            Ok(TypedReturn::Concrete(ConcreteReturn::ArrayF64(result)))
171        },
172    );
173
174    module
175}
176
177// ───────────────────── Legacy bodies (2 polymorphic intrinsics) ─────────────────────
178
179/// Intrinsic: Discrete difference of a series.
180///
181/// **Migration deferred** pending M1-split sub-decision (polymorphic
182/// return: `Vec<int>` with validity bitmap for `Vec<int>` fast path vs
183/// `Vec<f64>` with NaN sentinels for `Vec<number>`). The i64 fast path
184/// uses `option_i64_vec_to_nb` (validity-bitmap-aware); a future M1-split
185/// resolution for `diff` specifically needs a validity-aware return
186/// variant (e.g. `ConcreteReturn::ArrayOptionI64`) since
187/// `ConcreteReturn::ArrayI64(Vec<i64>)` does not carry validity. See
188/// the intrinsics-typed-CC entry's sub-decision queue.
189pub fn intrinsic_diff(_args: &[KindedSlot], _ctx: &mut ExecutionContext) -> Result<KindedSlot> {
190    Err(ShapeError::RuntimeError {
191        message: "intrinsic_diff: pending Phase 2c intrinsic kind threading + M1-split — see ADR-006 §2.7.4".to_string(),
192        location: None,
193    })
194}
195
196/// Intrinsic: Cumulative sum of a series.
197///
198/// **Migration deferred** pending M1-split sub-decision (polymorphic
199/// input/return: `Vec<int>` for i64 fast path vs `Vec<number>` for f64
200/// path). See the intrinsics-typed-CC entry's sub-decision queue.
201pub fn intrinsic_cumsum(_args: &[KindedSlot], _ctx: &mut ExecutionContext) -> Result<KindedSlot> {
202    Err(ShapeError::RuntimeError {
203        message: "intrinsic_cumsum: pending Phase 2c intrinsic kind threading + M1-split — see ADR-006 §2.7.4".to_string(),
204        location: None,
205    })
206}