Skip to main content

shape_runtime/intrinsics/
math.rs

1//! Math 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 zero-copy entry's per-storage-variant
5//! correction subsection + intrinsics-typed-CC entry's Q2 lifecycle
6//! subsection), 14 of math.rs's 19 intrinsics migrate to
7//! `register_typed_fn_N` typed entries via [`create_math_intrinsics_module`].
8//! 4 polymorphic intrinsics remain as legacy `IntrinsicFn` bodies pending
9//! follow-on architectural sub-decisions:
10//!
11//! - **`intrinsic_min` / `intrinsic_max`**: polymorphic return (`i64` for
12//!   `Vec<int>` fast path vs `f64` for `Vec<number>`). Single typed entry
13//!   can't carry both. Awaits **M1-split** sub-decision (per-element-type
14//!   intrinsics; cross-crate change to shape-vm compiler emission + opcode
15//!   discriminants + classification logic).
16//! - **`intrinsic_sum` deleted** (W12-stdlib-intrinsic-collapse,
17//!   Wave-2-Agent-G, 2026-05-14): stdlib `pub fn sum(series)` now routes
18//!   through PHF method dispatch (`series.sum()`) — single discriminator
19//!   per ADR-005 §1, `MethodFnV2` ABI per ADR-006 §2.7.10/Q11.
20//! - **`intrinsic_char_code`**: polymorphic input (`Char` vs `String`).
21//!   `HeapValue::Char` is first-class post-bulldozer (`heap_value.rs:846`)
22//!   so dropping the `Char` branch would break callers iterating
23//!   `for c in s.chars()`. Awaits **multi-input-type dispatch** sub-
24//!   decision.
25//! - **`intrinsic_bspline2_3d_batch`**: 11-arg with FloatArray fast path
26//!   AND generic-array slow path. Slow path uses `to_generic()` which is
27//!   removed; consumer audit of `math.shape:243` wrapper + user-code
28//!   needed before deciding fast-path-only-vs-keep-slow-path.
29//!
30//! Migrated entries take `Arc<Vec<f64>>` (for `Vec<number>`
31//! aggregations) or `f64` (for trig family + `from_char_code`) typed
32//! inputs and return `ConcreteReturn::F64` / `ConcreteReturn::String`
33//! per the dispatcher's `TypedReturn → slot push` projection.
34
35use crate::context::ExecutionContext;
36use crate::marshal::{register_typed_fn_1, register_typed_fn_2};
37use crate::module_exports::ModuleExports;
38use crate::typed_module_exports::{ConcreteReturn, ConcreteType, TypedReturn};
39use shape_ast::error::{Result, ShapeError};
40use shape_value::KindedSlot;
41use std::sync::Arc;
42
43// ───────────────────── Module factory (14 typed entries) ─────────────────────
44
45/// Create the math intrinsics module with 14 typed-marshal entry points.
46/// The 4 polymorphic intrinsics (min, max, char_code, bspline2_3d_batch)
47/// remain as legacy `IntrinsicFn` bodies in this module until their
48/// follow-on architectural sub-decisions land. `sum` was deleted by
49/// W12-stdlib-intrinsic-collapse (Wave-2-Agent-G, 2026-05-14) — stdlib
50/// now routes through PHF `.sum()` method dispatch.
51pub fn create_math_intrinsics_module() -> ModuleExports {
52    let mut module = ModuleExports::new("std::core::intrinsics::math");
53    module.description =
54        "Math intrinsics (typed entries; polymorphic-shape intrinsics stay as legacy bodies pending follow-on sub-decisions)"
55            .to_string();
56
57    // ── Array aggregations ──
58
59    register_typed_fn_1::<_, Arc<Vec<f64>>>(
60        &mut module,
61        "__intrinsic_mean",
62        "Mean (average) of a Vec<number>",
63        "input",
64        "Array<number>",
65        ConcreteType::Number,
66        |input, _ctx| {
67            let data = input.as_slice();
68            if data.is_empty() {
69                return Ok(TypedReturn::Concrete(ConcreteReturn::F64(f64::NAN)));
70            }
71            let sum: f64 = data.iter().sum();
72            Ok(TypedReturn::Concrete(ConcreteReturn::F64(
73                sum / data.len() as f64,
74            )))
75        },
76    );
77
78    register_typed_fn_1::<_, Arc<Vec<f64>>>(
79        &mut module,
80        "__intrinsic_variance",
81        "Population variance of a Vec<number>",
82        "input",
83        "Array<number>",
84        ConcreteType::Number,
85        |input, _ctx| {
86            let data = input.as_slice();
87            if data.is_empty() {
88                return Ok(TypedReturn::Concrete(ConcreteReturn::F64(f64::NAN)));
89            }
90            let mean: f64 = data.iter().sum::<f64>() / data.len() as f64;
91            #[cfg(all(target_arch = "x86_64", target_feature = "avx2"))]
92            let var = variance_avx2(data, mean);
93            #[cfg(not(all(target_arch = "x86_64", target_feature = "avx2")))]
94            let var: f64 = data.iter().map(|&x| (x - mean).powi(2)).sum::<f64>()
95                / data.len() as f64;
96            Ok(TypedReturn::Concrete(ConcreteReturn::F64(var)))
97        },
98    );
99
100    register_typed_fn_1::<_, Arc<Vec<f64>>>(
101        &mut module,
102        "__intrinsic_std",
103        "Population standard deviation of a Vec<number>",
104        "input",
105        "Array<number>",
106        ConcreteType::Number,
107        |input, _ctx| {
108            let data = input.as_slice();
109            if data.is_empty() {
110                return Ok(TypedReturn::Concrete(ConcreteReturn::F64(f64::NAN)));
111            }
112            let mean: f64 = data.iter().sum::<f64>() / data.len() as f64;
113            #[cfg(all(target_arch = "x86_64", target_feature = "avx2"))]
114            let var = variance_avx2(data, mean);
115            #[cfg(not(all(target_arch = "x86_64", target_feature = "avx2")))]
116            let var: f64 = data.iter().map(|&x| (x - mean).powi(2)).sum::<f64>()
117                / data.len() as f64;
118            Ok(TypedReturn::Concrete(ConcreteReturn::F64(var.sqrt())))
119        },
120    );
121
122    // ── Trig family (10 unary + 1 binary atan2) ──
123
124    register_unary_f64_op(&mut module, "__intrinsic_sin", "Sine of x", f64::sin);
125    register_unary_f64_op(&mut module, "__intrinsic_cos", "Cosine of x", f64::cos);
126    register_unary_f64_op(&mut module, "__intrinsic_tan", "Tangent of x", f64::tan);
127    register_unary_f64_op(&mut module, "__intrinsic_asin", "Arc sine of x", f64::asin);
128    register_unary_f64_op(&mut module, "__intrinsic_acos", "Arc cosine of x", f64::acos);
129    register_unary_f64_op(&mut module, "__intrinsic_atan", "Arc tangent of x", f64::atan);
130    register_unary_f64_op(&mut module, "__intrinsic_sinh", "Hyperbolic sine of x", f64::sinh);
131    register_unary_f64_op(&mut module, "__intrinsic_cosh", "Hyperbolic cosine of x", f64::cosh);
132    register_unary_f64_op(&mut module, "__intrinsic_tanh", "Hyperbolic tangent of x", f64::tanh);
133
134    register_typed_fn_2::<_, f64, f64>(
135        &mut module,
136        "__intrinsic_atan2",
137        "Two-argument arc tangent (atan2(y, x))",
138        [("y", "number"), ("x", "number")],
139        ConcreteType::Number,
140        |y, x, _ctx| Ok(TypedReturn::Concrete(ConcreteReturn::F64(y.atan2(x)))),
141    );
142
143    // ── Char code (one-direction migration; reverse stays legacy) ──
144
145    register_typed_fn_1::<_, f64>(
146        &mut module,
147        "__intrinsic_from_char_code",
148        "Create a single-character string from a Unicode code point",
149        "code",
150        "number",
151        ConcreteType::String,
152        |code, _ctx| {
153            let ch = char::from_u32(code as u32).ok_or_else(|| {
154                format!(
155                    "__intrinsic_from_char_code: invalid code point {}",
156                    code as u32
157                )
158            })?;
159            Ok(TypedReturn::Concrete(ConcreteReturn::String(ch.to_string())))
160        },
161    );
162
163    module
164}
165
166/// Helper: register a unary `f64 -> f64` typed entry (the trig family pattern).
167fn register_unary_f64_op(
168    module: &mut ModuleExports,
169    name: &'static str,
170    desc: &'static str,
171    op: fn(f64) -> f64,
172) {
173    register_typed_fn_1::<_, f64>(
174        module,
175        name,
176        desc,
177        "x",
178        "number",
179        ConcreteType::Number,
180        move |x, _ctx| Ok(TypedReturn::Concrete(ConcreteReturn::F64(op(x)))),
181    );
182}
183
184// ───────────────────── Legacy bodies (4 polymorphic intrinsics) ─────────────────────
185
186// W12-stdlib-intrinsic-collapse (Wave-2-Agent-G, 2026-05-14): the legacy
187// `intrinsic_sum` body was deleted alongside `BuiltinFunction::IntrinsicSum`.
188// Stdlib `pub fn sum(series)` now routes through PHF `.sum()` method
189// dispatch — single discriminator per ADR-005 §1.
190
191/// Intrinsic: Minimum value in a series or among multi-scalar arguments.
192///
193/// **Migration deferred** pending M1-split sub-decision (polymorphic return
194/// + polymorphic input shape). Multi-scalar branches are dead code from
195/// stdlib emission per audit but kept until the architectural decision lands.
196pub fn intrinsic_min(_args: &[KindedSlot], _ctx: &mut ExecutionContext) -> Result<KindedSlot> {
197    Err(ShapeError::RuntimeError {
198        message: "intrinsic_min: pending Phase 2c intrinsic kind threading + M1-split — see ADR-006 §2.7.4".to_string(),
199        location: None,
200    })
201}
202
203/// Intrinsic: Maximum value in a series or among multi-scalar arguments.
204///
205/// **Migration deferred** pending M1-split sub-decision. See `intrinsic_min`.
206pub fn intrinsic_max(_args: &[KindedSlot], _ctx: &mut ExecutionContext) -> Result<KindedSlot> {
207    Err(ShapeError::RuntimeError {
208        message: "intrinsic_max: pending Phase 2c intrinsic kind threading + M1-split — see ADR-006 §2.7.4".to_string(),
209        location: None,
210    })
211}
212
213/// Intrinsic: Get the Unicode code point of a single character.
214///
215/// **Migration deferred** pending multi-input-type dispatch sub-decision.
216/// `HeapValue::Char` is first-class post-bulldozer; dropping the `as_char`
217/// branch would break `for c in s.chars()`-style consumers.
218pub fn intrinsic_char_code(
219    _args: &[KindedSlot],
220    _ctx: &mut ExecutionContext,
221) -> Result<KindedSlot> {
222    Err(ShapeError::RuntimeError {
223        message: "intrinsic_char_code: pending Phase 2c intrinsic kind threading — see ADR-006 §2.7.4".to_string(),
224        location: None,
225    })
226}
227
228/// Intrinsic: Batched quadratic B-spline interpolation on a 3D grid.
229///
230/// **Migration deferred** pending consumer audit (fast path uses
231/// `as_f64_slice()`; slow path uses `to_generic()` which is removed.
232/// Audit needs to determine if any consumer passes a generic array
233/// before deciding fast-path-only-vs-keep-slow-path).
234///
235/// Args: grid_data, nx, ny, nz, x_lo, x_hi, y_lo, y_hi, z_lo, z_hi, pos_flat
236pub fn intrinsic_bspline2_3d_batch(
237    _args: &[KindedSlot],
238    _ctx: &mut ExecutionContext,
239) -> Result<KindedSlot> {
240    Err(ShapeError::RuntimeError {
241        message: "intrinsic_bspline2_3d_batch: pending Phase 2c intrinsic kind threading + consumer audit — see ADR-006 §2.7.4".to_string(),
242        location: None,
243    })
244}
245
246// ───────────────────── Helpers (used by typed + legacy bodies) ─────────────────────
247
248#[cfg(all(target_arch = "x86_64", target_feature = "avx2"))]
249fn variance_avx2(data: &[f64], mean: f64) -> f64 {
250    use std::simd::f64x4;
251
252    let chunks = data.chunks_exact(4);
253    let remainder = chunks.remainder();
254
255    let mean_vec = f64x4::splat(mean);
256    let mut var_sum = f64x4::splat(0.0);
257
258    for chunk in chunks {
259        let values = f64x4::from_slice(chunk);
260        let diff = values - mean_vec;
261        var_sum += diff * diff;
262    }
263
264    let vector_var = var_sum.reduce_sum();
265    let remainder_var: f64 = remainder.iter().map(|&x| (x - mean).powi(2)).sum();
266
267    (vector_var + remainder_var) / data.len() as f64
268}
269
270/// Core B-spline computation on a contiguous f64 slice (fastest path).
271#[inline]
272fn bspline2_3d_batch_slice(
273    grid: &[f64],
274    nx: usize, ny: usize, nz: usize,
275    x_lo: f64, x_hi: f64, y_lo: f64, y_hi: f64, z_lo: f64, z_hi: f64,
276    pos: &[f64],
277) -> Vec<f64> {
278    let n = pos.len() / 3;
279    let nxm = (nx - 1) as f64;
280    let nym = (ny - 1) as f64;
281    let nzm = (nz - 1) as f64;
282    let inv_x = nxm / (x_hi - x_lo);
283    let inv_y = nym / (y_hi - y_lo);
284    let inv_z = nzm / (z_hi - z_lo);
285    let nyz = ny * nz;
286    let mut result = Vec::with_capacity(n);
287
288    for s in 0..n {
289        let i3 = s * 3;
290        let gx = ((pos[i3] - x_lo) * inv_x).clamp(0.0, nxm);
291        let gy = ((pos[i3 + 1] - y_lo) * inv_y).clamp(0.0, nym);
292        let gz = ((pos[i3 + 2] - z_lo) * inv_z).clamp(0.0, nzm);
293
294        let cx = (gx + 0.5).floor() as isize;
295        let cy = (gy + 0.5).floor() as isize;
296        let cz = (gz + 0.5).floor() as isize;
297        let tx = gx - cx as f64;
298        let ty = gy - cy as f64;
299        let tz = gz - cz as f64;
300
301        let (wx0, wx1, wx2) = bspline_weights(tx);
302        let (wy0, wy1, wy2) = bspline_weights(ty);
303        let (wz0, wz1, wz2) = bspline_weights(tz);
304
305        let ix = [
306            (cx - 1).max(0) as usize,
307            cx as usize,
308            (cx + 1).min(nx as isize - 1) as usize,
309        ];
310        let iy = [
311            (cy - 1).max(0) as usize,
312            cy as usize,
313            (cy + 1).min(ny as isize - 1) as usize,
314        ];
315        let iz = [
316            (cz - 1).max(0) as usize,
317            cz as usize,
318            (cz + 1).min(nz as isize - 1) as usize,
319        ];
320
321        let wx = [wx0, wx1, wx2];
322        let wy = [wy0, wy1, wy2];
323        let wz = [wz0, wz1, wz2];
324
325        let mut val = 0.0;
326        for a in 0..3 {
327            let rx = ix[a] * nyz;
328            for b in 0..3 {
329                let rxy = rx + iy[b] * nz;
330                let wxy = wx[a] * wy[b];
331                for c in 0..3 {
332                    val += wxy * wz[c] * unsafe { *grid.get_unchecked(rxy + iz[c]) };
333                }
334            }
335        }
336        result.push(val);
337    }
338    result
339}
340
341/// Core B-spline computation using per-element access function (generic arrays).
342/// Only accesses 27 grid elements per query point — no bulk copy.
343#[inline]
344fn bspline2_3d_batch_fn(
345    grid: &dyn Fn(usize) -> f64,
346    nx: usize, ny: usize, nz: usize,
347    x_lo: f64, x_hi: f64, y_lo: f64, y_hi: f64, z_lo: f64, z_hi: f64,
348    pos: &[f64],
349) -> Vec<f64> {
350    let n = pos.len() / 3;
351    let nxm = (nx - 1) as f64;
352    let nym = (ny - 1) as f64;
353    let nzm = (nz - 1) as f64;
354    let inv_x = nxm / (x_hi - x_lo);
355    let inv_y = nym / (y_hi - y_lo);
356    let inv_z = nzm / (z_hi - z_lo);
357    let nyz = ny * nz;
358    let mut result = Vec::with_capacity(n);
359
360    for s in 0..n {
361        let i3 = s * 3;
362        let gx = ((pos[i3] - x_lo) * inv_x).clamp(0.0, nxm);
363        let gy = ((pos[i3 + 1] - y_lo) * inv_y).clamp(0.0, nym);
364        let gz = ((pos[i3 + 2] - z_lo) * inv_z).clamp(0.0, nzm);
365
366        let cx = (gx + 0.5).floor() as isize;
367        let cy = (gy + 0.5).floor() as isize;
368        let cz = (gz + 0.5).floor() as isize;
369        let tx = gx - cx as f64;
370        let ty = gy - cy as f64;
371        let tz = gz - cz as f64;
372
373        let (wx0, wx1, wx2) = bspline_weights(tx);
374        let (wy0, wy1, wy2) = bspline_weights(ty);
375        let (wz0, wz1, wz2) = bspline_weights(tz);
376
377        let ix = [
378            (cx - 1).max(0) as usize,
379            cx as usize,
380            (cx + 1).min(nx as isize - 1) as usize,
381        ];
382        let iy = [
383            (cy - 1).max(0) as usize,
384            cy as usize,
385            (cy + 1).min(ny as isize - 1) as usize,
386        ];
387        let iz = [
388            (cz - 1).max(0) as usize,
389            cz as usize,
390            (cz + 1).min(nz as isize - 1) as usize,
391        ];
392
393        let wx = [wx0, wx1, wx2];
394        let wy = [wy0, wy1, wy2];
395        let wz = [wz0, wz1, wz2];
396
397        let mut val = 0.0;
398        for a in 0..3 {
399            let rx = ix[a] * nyz;
400            for b in 0..3 {
401                let rxy = rx + iy[b] * nz;
402                let wxy = wx[a] * wy[b];
403                for c in 0..3 {
404                    val += wxy * wz[c] * grid(rxy + iz[c]);
405                }
406            }
407        }
408        result.push(val);
409    }
410    result
411}
412
413/// Quadratic B-spline basis weights for offset t.
414#[inline(always)]
415fn bspline_weights(t: f64) -> (f64, f64, f64) {
416    (
417        0.5 * (0.5 - t) * (0.5 - t),
418        0.75 - t * t,
419        0.5 * (0.5 + t) * (0.5 + t),
420    )
421}