Skip to main content

cjc_runtime/
builtins.rs

1//! Shared builtin function implementations for CJC eval and MIR-exec.
2//!
3//! These are pure (or nearly pure) dispatchers that return
4//! `Result<Value, String>`. Each caller wraps the `String` into its own
5//! error type (e.g. `EvalError::Runtime` or `MirExecError::Runtime`).
6//!
7//! **Contract:** Functions in this module must NOT depend on interpreter
8//! state. Anything that needs `&mut self` on an interpreter (print, gc_*,
9//! clock, Tensor.randn) stays in each executor.
10
11use std::cell::RefCell;
12use std::rc::Rc;
13
14use cjc_regex;
15use crate::accumulator::BinnedAccumulatorF64;
16use crate::complex::ComplexF64;
17use crate::scratchpad::Scratchpad;
18use crate::paged_kv::PagedKvCache;
19use crate::tensor::Tensor;
20use crate::tensor_simd::UnaryOp;
21use crate::value::{Bf16, Value};
22
23// ---------------------------------------------------------------------------
24// Value conversion helpers
25// ---------------------------------------------------------------------------
26
27/// Convert a `Value::Array` of ints into a `Vec<usize>` (shape).
28pub fn value_to_shape(val: &Value) -> Result<Vec<usize>, String> {
29    match val {
30        Value::Array(arr) => {
31            let mut shape = Vec::with_capacity(arr.len());
32            for v in arr.iter() {
33                shape.push(value_to_usize(v)?);
34            }
35            Ok(shape)
36        }
37        _ => Err(format!("expected Array for shape, got {}", val.type_name())),
38    }
39}
40
41/// Convert a `Value::Int` to `usize`, rejecting negatives.
42pub fn value_to_usize(val: &Value) -> Result<usize, String> {
43    match val {
44        Value::Int(i) => {
45            if *i < 0 {
46                Err(format!("expected non-negative integer, got {i}"))
47            } else {
48                Ok(*i as usize)
49            }
50        }
51        _ => Err(format!("expected Int, got {}", val.type_name())),
52    }
53}
54
55/// Convert a `Value::Array` of floats/ints to `Vec<f64>`.
56pub fn value_to_f64_vec(val: &Value) -> Result<Vec<f64>, String> {
57    match val {
58        Value::Array(arr) => {
59            let mut data = Vec::with_capacity(arr.len());
60            for v in arr.iter() {
61                match v {
62                    Value::Float(f) => data.push(*f),
63                    Value::Int(i) => data.push(*i as f64),
64                    // NA values are silently skipped in aggregations (na_rm=true default)
65                    Value::Na => {}
66                    _ => {
67                        return Err(format!(
68                            "expected numeric values in array, got {}",
69                            v.type_name()
70                        ));
71                    }
72                }
73            }
74            Ok(data)
75        }
76        _ => Err(format!("expected Array, got {}", val.type_name())),
77    }
78}
79
80/// Convert a `Value::Array` of complex tuples (re, im) to `Vec<(f64, f64)>`.
81pub fn value_to_complex_vec(val: &Value) -> Result<Vec<(f64, f64)>, String> {
82    match val {
83        Value::Array(arr) => {
84            let mut data = Vec::with_capacity(arr.len());
85            for v in arr.iter() {
86                match v {
87                    Value::Tuple(t) if t.len() == 2 => {
88                        let re = match &t[0] { Value::Float(f) => *f, Value::Int(i) => *i as f64, _ => return Err("complex tuple element must be numeric".into()) };
89                        let im = match &t[1] { Value::Float(f) => *f, Value::Int(i) => *i as f64, _ => return Err("complex tuple element must be numeric".into()) };
90                        data.push((re, im));
91                    }
92                    _ => return Err("expected array of (re, im) tuples".into()),
93                }
94            }
95            Ok(data)
96        }
97        _ => Err(format!("expected Array of complex tuples, got {}", val.type_name())),
98    }
99}
100
101/// Convert a `Value::Array` of ints to `Vec<usize>`.
102pub fn value_to_usize_vec(val: &Value) -> Result<Vec<usize>, String> {
103    match val {
104        Value::Array(arr) => {
105            let mut indices = Vec::with_capacity(arr.len());
106            for v in arr.iter() {
107                indices.push(value_to_usize(v)?);
108            }
109            Ok(indices)
110        }
111        _ => Err(format!("expected Array for indices, got {}", val.type_name())),
112    }
113}
114
115/// Extract a `&Tensor` from a `Value::Tensor`.
116pub fn value_to_tensor(val: &Value) -> Result<&Tensor, String> {
117    match val {
118        Value::Tensor(t) => Ok(t),
119        _ => Err(format!("expected Tensor, got {}", val.type_name())),
120    }
121}
122
123/// Convert a `Value::Float` or `Value::Int` to `f64`.
124pub fn value_to_f64(val: &Value) -> Result<f64, String> {
125    match val {
126        Value::Float(v) => Ok(*v),
127        Value::Int(v) => Ok(*v as f64),
128        _ => Err(format!("expected Float or Int, got {}", val.type_name())),
129    }
130}
131
132/// Extract bytes from a `ByteSlice`, `Bytes`, or `String` value.
133pub fn value_to_bytes(val: &Value) -> Result<Vec<u8>, String> {
134    match val {
135        Value::ByteSlice(b) => Ok(b.as_ref().clone()),
136        Value::Bytes(b) => Ok(b.borrow().clone()),
137        Value::String(s) => Ok(s.as_bytes().to_vec()),
138        _ => Err(format!("expected ByteSlice or Bytes, got {}", val.type_name())),
139    }
140}
141
142/// Structural equality comparison for assertion builtins.
143pub fn values_equal(a: &Value, b: &Value) -> bool {
144    match (a, b) {
145        (Value::Int(a), Value::Int(b)) => a == b,
146        (Value::Float(a), Value::Float(b)) => a == b,
147        (Value::Bool(a), Value::Bool(b)) => a == b,
148        (Value::String(a), Value::String(b)) => a == b,
149        (Value::Void, Value::Void) => true,
150        _ => false,
151    }
152}
153
154// ---------------------------------------------------------------------------
155// Deterministic categorical sampling (needs external RNG)
156// ---------------------------------------------------------------------------
157
158/// Sample an index from a 1-D probability tensor using the given uniform
159/// random value `u` in [0, 1). Returns the selected index (0-based).
160/// This is a general-purpose RL primitive, not domain-specific.
161pub fn categorical_sample_with_u(probs: &Tensor, u: f64) -> Result<i64, String> {
162    if probs.ndim() == 0 {
163        return Err("categorical_sample requires at least a 1-D tensor".into());
164    }
165    let data = probs.to_vec();
166    if data.is_empty() {
167        return Err("categorical_sample: empty probability tensor".into());
168    }
169    let mut cumsum = 0.0;
170    for (i, &p) in data.iter().enumerate() {
171        cumsum += p;
172        if u < cumsum {
173            return Ok(i as i64);
174        }
175    }
176    // Numerical safety: return last valid index
177    Ok((data.len() - 1) as i64)
178}
179
180// ---------------------------------------------------------------------------
181// Stateless builtin functions
182// ---------------------------------------------------------------------------
183
184/// Dispatch a stateless builtin function by name.
185/// Returns `Ok(Some(value))` if handled, `Ok(None)` if not a known builtin.
186pub fn dispatch_builtin(name: &str, args: &[Value]) -> Result<Option<Value>, String> {
187    match name {
188        "Complex" => {
189            let re = match args.get(0) {
190                Some(Value::Float(v)) => *v,
191                Some(Value::Int(v)) => *v as f64,
192                _ => return Err("Complex() requires numeric re argument".into()),
193            };
194            let im = match args.get(1) {
195                Some(Value::Float(v)) => *v,
196                Some(Value::Int(v)) => *v as f64,
197                None => 0.0,
198                _ => return Err("Complex() requires numeric im argument".into()),
199            };
200            Ok(Some(Value::Complex(ComplexF64::new(re, im))))
201        }
202        // f16 conversion builtins
203        "f16_to_f64" => match &args[0] {
204            Value::F16(v) => Ok(Some(Value::Float(v.to_f64()))),
205            _ => Err("f16_to_f64 expects f16".into()),
206        },
207        "f64_to_f16" => match &args[0] {
208            Value::Float(v) => Ok(Some(Value::F16(crate::f16::F16::from_f64(*v)))),
209            Value::Int(v) => Ok(Some(Value::F16(crate::f16::F16::from_f64(*v as f64)))),
210            _ => Err("f64_to_f16 expects f64".into()),
211        },
212        "f16_to_f32" => match &args[0] {
213            Value::F16(v) => Ok(Some(Value::Float(v.to_f32() as f64))),
214            _ => Err("f16_to_f32 expects f16".into()),
215        },
216        "f32_to_f16" => match &args[0] {
217            Value::Float(v) => Ok(Some(Value::F16(crate::f16::F16::from_f32(*v as f32)))),
218            _ => Err("f32_to_f16 expects f32".into()),
219        },
220        // bf16 conversion builtins
221        "bf16_to_f32" => match &args[0] {
222            Value::Bf16(v) => Ok(Some(Value::Float(v.to_f32() as f64))),
223            _ => Err("bf16_to_f32 expects bf16".into()),
224        },
225        "f32_to_bf16" => match &args[0] {
226            Value::Float(v) => Ok(Some(Value::Bf16(Bf16::from_f32(*v as f32)))),
227            _ => Err("f32_to_bf16 expects f32".into()),
228        },
229        // Tensor constructors (stateless ones)
230        "Tensor.zeros" => {
231            let shape = value_to_shape(&args[0])?;
232            Ok(Some(Value::Tensor(Tensor::zeros(&shape))))
233        }
234        "Tensor.ones" => {
235            let shape = value_to_shape(&args[0])?;
236            Ok(Some(Value::Tensor(Tensor::ones(&shape))))
237        }
238        "Tensor.from_vec" => {
239            if args.len() != 2 {
240                return Err("Tensor.from_vec requires 2 arguments: data and shape".into());
241            }
242            let data = value_to_f64_vec(&args[0])?;
243            let shape = value_to_shape(&args[1])?;
244            let t = Tensor::from_vec(data, &shape).map_err(|e| format!("{e}"))?;
245            Ok(Some(Value::Tensor(t)))
246        }
247        "matmul" => {
248            if args.len() != 2 {
249                return Err("matmul requires 2 Tensor arguments".into());
250            }
251            let a = value_to_tensor(&args[0])?;
252            let b = value_to_tensor(&args[1])?;
253            Ok(Some(Value::Tensor(a.matmul(b).map_err(|e| format!("{e}"))?)))
254        }
255        "attention" => {
256            if args.len() != 3 {
257                return Err("attention requires 3 Tensor arguments: queries, keys, values".into());
258            }
259            let q = value_to_tensor(&args[0])?;
260            let k = value_to_tensor(&args[1])?;
261            let v = value_to_tensor(&args[2])?;
262            Ok(Some(Value::Tensor(
263                Tensor::scaled_dot_product_attention(q, k, v).map_err(|e| format!("{e}"))?,
264            )))
265        }
266        "Buffer.alloc" => {
267            if args.is_empty() {
268                return Err("Buffer.alloc requires a length argument".into());
269            }
270            let len = value_to_usize(&args[0])?;
271            Ok(Some(Value::Tensor(Tensor::zeros(&[len]))))
272        }
273        "Tensor.from_bytes" => {
274            if args.len() < 2 || args.len() > 3 {
275                return Err(
276                    "Tensor.from_bytes requires 2-3 arguments: bytes, shape, [dtype='f64']".into(),
277                );
278            }
279            let bytes = match &args[0] {
280                Value::ByteSlice(b) => b.clone(),
281                Value::Bytes(b) => Rc::new(b.borrow().clone()),
282                _ => {
283                    return Err(
284                        "Tensor.from_bytes: first argument must be ByteSlice or Bytes".into(),
285                    )
286                }
287            };
288            let shape = value_to_shape(&args[1])?;
289            let dtype = if args.len() == 3 {
290                match &args[2] {
291                    Value::String(s) => s.as_str().to_string(),
292                    _ => return Err("Tensor.from_bytes: dtype must be a string".into()),
293                }
294            } else {
295                "f64".to_string()
296            };
297            Ok(Some(Value::Tensor(
298                Tensor::from_bytes(&bytes, &shape, &dtype).map_err(|e| format!("{e}"))?,
299            )))
300        }
301        "Scratchpad.new" => {
302            if args.len() != 2 {
303                return Err("Scratchpad.new requires 2 arguments: max_seq_len, dim".into());
304            }
305            let max_seq_len = value_to_usize(&args[0])?;
306            let dim = value_to_usize(&args[1])?;
307            Ok(Some(Value::Scratchpad(Rc::new(RefCell::new(
308                Scratchpad::new(max_seq_len, dim),
309            )))))
310        }
311        "PagedKvCache.new" => {
312            if args.len() != 2 {
313                return Err("PagedKvCache.new requires 2 arguments: max_tokens, dim".into());
314            }
315            let max_tokens = value_to_usize(&args[0])?;
316            let dim = value_to_usize(&args[1])?;
317            Ok(Some(Value::PagedKvCache(Rc::new(RefCell::new(
318                PagedKvCache::new(max_tokens, dim),
319            )))))
320        }
321        "AlignedByteSlice.from_bytes" => {
322            if args.len() != 1 {
323                return Err("AlignedByteSlice.from_bytes requires 1 argument: bytes".into());
324            }
325            let bytes = match &args[0] {
326                Value::ByteSlice(b) => b.clone(),
327                Value::Bytes(b) => Rc::new(b.borrow().clone()),
328                _ => {
329                    return Err(
330                        "AlignedByteSlice.from_bytes: argument must be ByteSlice or Bytes".into(),
331                    )
332                }
333            };
334            Ok(Some(Value::AlignedBytes(
335                crate::aligned_pool::AlignedByteSlice::from_bytes(bytes),
336            )))
337        }
338        "to_string" => {
339            if args.len() != 1 {
340                return Err("to_string requires exactly 1 argument".into());
341            }
342            Ok(Some(Value::String(Rc::new(format!("{}", args[0])))))
343        }
344
345        // ── String manipulation builtins ────────────────────────────────
346        "str_upper" => {
347            if args.len() != 1 { return Err("str_upper requires 1 argument".into()); }
348            match &args[0] {
349                Value::String(s) => Ok(Some(Value::String(Rc::new(s.to_uppercase())))),
350                _ => Err("str_upper: argument must be a string".into()),
351            }
352        }
353        "str_lower" => {
354            if args.len() != 1 { return Err("str_lower requires 1 argument".into()); }
355            match &args[0] {
356                Value::String(s) => Ok(Some(Value::String(Rc::new(s.to_lowercase())))),
357                _ => Err("str_lower: argument must be a string".into()),
358            }
359        }
360        "str_trim" => {
361            if args.len() != 1 { return Err("str_trim requires 1 argument".into()); }
362            match &args[0] {
363                Value::String(s) => Ok(Some(Value::String(Rc::new(s.trim().to_string())))),
364                _ => Err("str_trim: argument must be a string".into()),
365            }
366        }
367        "str_contains" => {
368            if args.len() != 2 { return Err("str_contains requires 2 arguments".into()); }
369            match (&args[0], &args[1]) {
370                (Value::String(haystack), Value::String(needle)) => {
371                    Ok(Some(Value::Bool(haystack.contains(needle.as_str()))))
372                }
373                _ => Err("str_contains: both arguments must be strings".into()),
374            }
375        }
376        "str_replace" => {
377            if args.len() != 3 { return Err("str_replace requires 3 arguments (str, from, to)".into()); }
378            match (&args[0], &args[1], &args[2]) {
379                (Value::String(s), Value::String(from), Value::String(to)) => {
380                    // Replace first occurrence only (matches tidy/stringr semantics).
381                    // Use str_replace_all for global replacement.
382                    Ok(Some(Value::String(Rc::new(s.replacen(from.as_str(), to.as_str(), 1)))))
383                }
384                _ => Err("str_replace: all arguments must be strings".into()),
385            }
386        }
387        "str_split" => {
388            if args.len() != 2 { return Err("str_split requires 2 arguments (str, delimiter)".into()); }
389            match (&args[0], &args[1]) {
390                (Value::String(s), Value::String(delim)) => {
391                    let parts: Vec<Value> = s.split(delim.as_str())
392                        .map(|p| Value::String(Rc::new(p.to_string())))
393                        .collect();
394                    Ok(Some(Value::Array(Rc::new(parts))))
395                }
396                _ => Err("str_split: both arguments must be strings".into()),
397            }
398        }
399        "str_join" => {
400            if args.len() != 2 { return Err("str_join requires 2 arguments (array, delimiter)".into()); }
401            match (&args[0], &args[1]) {
402                (Value::Array(arr), Value::String(delim)) => {
403                    let parts: Vec<String> = arr.iter()
404                        .map(|v| format!("{}", v))
405                        .collect();
406                    Ok(Some(Value::String(Rc::new(parts.join(delim.as_str())))))
407                }
408                _ => Err("str_join: first arg must be array, second must be string".into()),
409            }
410        }
411        "str_starts_with" => {
412            if args.len() != 2 { return Err("str_starts_with requires 2 arguments".into()); }
413            match (&args[0], &args[1]) {
414                (Value::String(s), Value::String(prefix)) => {
415                    Ok(Some(Value::Bool(s.starts_with(prefix.as_str()))))
416                }
417                _ => Err("str_starts_with: both arguments must be strings".into()),
418            }
419        }
420        "str_ends_with" => {
421            if args.len() != 2 { return Err("str_ends_with requires 2 arguments".into()); }
422            match (&args[0], &args[1]) {
423                (Value::String(s), Value::String(suffix)) => {
424                    Ok(Some(Value::Bool(s.ends_with(suffix.as_str()))))
425                }
426                _ => Err("str_ends_with: both arguments must be strings".into()),
427            }
428        }
429        // ── Regex composition helpers ───────────────────────────────────────
430        // These operate purely on pattern strings; the composed string is then
431        // passed to the normal cjc_regex engine by the caller.
432        "regex_or" => {
433            // regex_or(p1, p2, ...) → "(?:p1)|(?:p2)|..."
434            // Combines two or more patterns with alternation.
435            if args.is_empty() { return Err("regex_or requires at least 1 argument".into()); }
436            let mut parts = Vec::with_capacity(args.len());
437            for (i, arg) in args.iter().enumerate() {
438                match arg {
439                    Value::String(s) => parts.push(format!("(?:{})", s)),
440                    _ => return Err(format!("regex_or: argument {} must be a string pattern", i)),
441                }
442            }
443            Ok(Some(Value::String(Rc::new(parts.join("|")))))
444        }
445        "regex_seq" => {
446            // regex_seq(p1, p2, ...) → "(?:p1)(?:p2)..."
447            // Concatenates patterns in sequence.
448            if args.is_empty() { return Err("regex_seq requires at least 1 argument".into()); }
449            let mut result = String::new();
450            for (i, arg) in args.iter().enumerate() {
451                match arg {
452                    Value::String(s) => result.push_str(&format!("(?:{})", s)),
453                    _ => return Err(format!("regex_seq: argument {} must be a string pattern", i)),
454                }
455            }
456            Ok(Some(Value::String(Rc::new(result))))
457        }
458        "regex_explain" => {
459            // regex_explain(pattern, flags?) → human-readable NFA description string
460            if args.is_empty() || args.len() > 2 {
461                return Err("regex_explain requires 1 or 2 arguments (pattern, flags?)".into());
462            }
463            let pattern = match &args[0] {
464                Value::String(s) => s.as_str(),
465                _ => return Err("regex_explain: first argument must be a string".into()),
466            };
467            let flags = if args.len() == 2 {
468                match &args[1] {
469                    Value::String(s) => s.as_str().to_string(),
470                    _ => return Err("regex_explain: second argument must be a string".into()),
471                }
472            } else {
473                String::new()
474            };
475            match cjc_regex::regex_explain(pattern, &flags) {
476                Ok(desc) => Ok(Some(Value::String(Rc::new(desc)))),
477                Err(e) => Err(format!("regex_explain: {}", e)),
478            }
479        }
480        // ── Regex capture builtins ────────────────────────────────────────
481        "regex_captures" => {
482            // regex_captures(pattern, flags, text) -> Array of strings
483            // Index 0 = full match, 1..n = capture groups. Unmatched -> "". No match -> [].
484            if args.len() != 3 {
485                return Err("regex_captures requires 3 arguments (pattern, flags, text)".into());
486            }
487            let pattern = match &args[0] {
488                Value::String(s) => s.as_str().to_string(),
489                _ => return Err("regex_captures: first argument must be a string".into()),
490            };
491            let flags = match &args[1] {
492                Value::String(s) => s.as_str().to_string(),
493                _ => return Err("regex_captures: second argument must be a string".into()),
494            };
495            let text = match &args[2] {
496                Value::String(s) => s.as_str().to_string(),
497                _ => return Err("regex_captures: third argument must be a string".into()),
498            };
499            match cjc_regex::find_captures(&pattern, &flags, text.as_bytes()) {
500                Some(cr) => {
501                    let mut result = Vec::with_capacity(cr.groups.len());
502                    for group in &cr.groups {
503                        match group {
504                            Some(cap) => {
505                                let s = cap.extract_str(text.as_bytes())
506                                    .unwrap_or("")
507                                    .to_string();
508                                result.push(Value::String(Rc::new(s)));
509                            }
510                            None => result.push(Value::String(Rc::new(String::new()))),
511                        }
512                    }
513                    Ok(Some(Value::Array(Rc::new(result))))
514                }
515                None => Ok(Some(Value::Array(Rc::new(Vec::new())))),
516            }
517        }
518        "regex_named_capture" => {
519            // regex_named_capture(pattern, flags, text, name) -> String
520            // Returns matched text for named group, or "" if no match.
521            if args.len() != 4 {
522                return Err("regex_named_capture requires 4 arguments (pattern, flags, text, name)".into());
523            }
524            let pattern = match &args[0] {
525                Value::String(s) => s.as_str().to_string(),
526                _ => return Err("regex_named_capture: first argument must be a string".into()),
527            };
528            let flags = match &args[1] {
529                Value::String(s) => s.as_str().to_string(),
530                _ => return Err("regex_named_capture: second argument must be a string".into()),
531            };
532            let text = match &args[2] {
533                Value::String(s) => s.as_str().to_string(),
534                _ => return Err("regex_named_capture: third argument must be a string".into()),
535            };
536            let name = match &args[3] {
537                Value::String(s) => s.as_str().to_string(),
538                _ => return Err("regex_named_capture: fourth argument must be a string".into()),
539            };
540            match cjc_regex::find_captures(&pattern, &flags, text.as_bytes()) {
541                Some(cr) => {
542                    match cr.get_named(&name) {
543                        Some(cap) => {
544                            let s = cap.extract_str(text.as_bytes())
545                                .unwrap_or("")
546                                .to_string();
547                            Ok(Some(Value::String(Rc::new(s))))
548                        }
549                        None => Ok(Some(Value::String(Rc::new(String::new())))),
550                    }
551                }
552                None => Ok(Some(Value::String(Rc::new(String::new())))),
553            }
554        }
555        "regex_capture_count" => {
556            // regex_capture_count(pattern, flags) -> i64
557            // Returns number of capture groups (0 if none or invalid).
558            if args.len() != 2 {
559                return Err("regex_capture_count requires 2 arguments (pattern, flags)".into());
560            }
561            let pattern = match &args[0] {
562                Value::String(s) => s.as_str().to_string(),
563                _ => return Err("regex_capture_count: first argument must be a string".into()),
564            };
565            let flags = match &args[1] {
566                Value::String(s) => s.as_str().to_string(),
567                _ => return Err("regex_capture_count: second argument must be a string".into()),
568            };
569            let count = cjc_regex::capture_count(&pattern, &flags) as i64;
570            Ok(Some(Value::Int(count)))
571        }
572        // ── End regex composition helpers ───────────────────────────────────
573        "str_repeat" => {
574            if args.len() != 2 { return Err("str_repeat requires 2 arguments (str, count)".into()); }
575            match (&args[0], &args[1]) {
576                (Value::String(s), Value::Int(n)) => {
577                    if *n < 0 { return Err("str_repeat: count must be non-negative".into()); }
578                    Ok(Some(Value::String(Rc::new(s.repeat(*n as usize)))))
579                }
580                _ => Err("str_repeat: first arg must be string, second must be integer".into()),
581            }
582        }
583        "str_chars" => {
584            if args.len() != 1 { return Err("str_chars requires 1 argument".into()); }
585            match &args[0] {
586                Value::String(s) => {
587                    let chars: Vec<Value> = s.chars()
588                        .map(|c| Value::String(Rc::new(c.to_string())))
589                        .collect();
590                    Ok(Some(Value::Array(Rc::new(chars))))
591                }
592                _ => Err("str_chars: argument must be a string".into()),
593            }
594        }
595        "str_substr" => {
596            if args.len() != 3 { return Err("str_substr requires 3 arguments (str, start, len)".into()); }
597            match (&args[0], &args[1], &args[2]) {
598                (Value::String(s), Value::Int(start), Value::Int(len)) => {
599                    let start = (*start).max(0) as usize;
600                    let len = (*len).max(0) as usize;
601                    let result: String = s.chars().skip(start).take(len).collect();
602                    Ok(Some(Value::String(Rc::new(result))))
603                }
604                _ => Err("str_substr: (str, int, int) expected".into()),
605            }
606        }
607
608        "len" => {
609            if args.len() != 1 {
610                return Err("len requires exactly 1 argument".into());
611            }
612            match &args[0] {
613                Value::Array(arr) => Ok(Some(Value::Int(arr.len() as i64))),
614                Value::String(s) => Ok(Some(Value::Int(s.len() as i64))),
615                Value::Tensor(t) => Ok(Some(Value::Int(t.len() as i64))),
616                Value::Tuple(t) => Ok(Some(Value::Int(t.len() as i64))),
617                other => Err(format!("len not supported for {}", other.type_name())),
618            }
619        }
620        "push" => {
621            if args.len() != 2 {
622                return Err("push requires 2 arguments: array and value".into());
623            }
624            match (&args[0], &args[1]) {
625                (Value::Array(a), val) => {
626                    let mut new_arr = (**a).clone();
627                    new_arr.push(val.clone());
628                    Ok(Some(Value::Array(Rc::new(new_arr))))
629                }
630                _ => Err("push requires an Array as first argument".into()),
631            }
632        }
633        "sort" => {
634            if args.len() != 1 {
635                return Err("sort requires exactly 1 argument".into());
636            }
637            match &args[0] {
638                Value::Array(arr) => {
639                    let mut sorted: Vec<Value> = (**arr).clone();
640                    sorted.sort_by(|a, b| {
641                        let fa = match a {
642                            Value::Float(f) => *f,
643                            Value::Int(i) => *i as f64,
644                            _ => f64::NAN,
645                        };
646                        let fb = match b {
647                            Value::Float(f) => *f,
648                            Value::Int(i) => *i as f64,
649                            _ => f64::NAN,
650                        };
651                        fa.partial_cmp(&fb).unwrap_or(std::cmp::Ordering::Equal)
652                    });
653                    Ok(Some(Value::Array(Rc::new(sorted))))
654                }
655                _ => Err(format!("sort requires an Array, got {}", args[0].type_name())),
656            }
657        }
658        "sqrt" => {
659            if args.len() != 1 {
660                return Err("sqrt requires exactly 1 argument".into());
661            }
662            match &args[0] {
663                Value::Float(f) => Ok(Some(Value::Float(f.sqrt()))),
664                Value::Int(i) => Ok(Some(Value::Float((*i as f64).sqrt()))),
665                _ => Err(format!("sqrt requires a number, got {}", args[0].type_name())),
666            }
667        }
668        "log" => {
669            if args.len() != 1 {
670                return Err("log requires exactly 1 argument".into());
671            }
672            match &args[0] {
673                Value::Float(f) => Ok(Some(Value::Float(f.ln()))),
674                Value::Int(i) => Ok(Some(Value::Float((*i as f64).ln()))),
675                _ => Err(format!("log requires a number, got {}", args[0].type_name())),
676            }
677        }
678        "exp" => {
679            if args.len() != 1 {
680                return Err("exp requires exactly 1 argument".into());
681            }
682            match &args[0] {
683                Value::Float(f) => Ok(Some(Value::Float(f.exp()))),
684                Value::Int(i) => Ok(Some(Value::Float((*i as f64).exp()))),
685                _ => Err(format!("exp requires a number, got {}", args[0].type_name())),
686            }
687        }
688        // ---- Mathematics Hardening Phase: Trigonometric ----
689        "sin" => {
690            if args.len() != 1 { return Err("sin requires exactly 1 argument".into()); }
691            match &args[0] {
692                Value::Float(f) => Ok(Some(Value::Float(f.sin()))),
693                Value::Int(i) => Ok(Some(Value::Float((*i as f64).sin()))),
694                _ => Err(format!("sin requires a number, got {}", args[0].type_name())),
695            }
696        }
697        "cos" => {
698            if args.len() != 1 { return Err("cos requires exactly 1 argument".into()); }
699            match &args[0] {
700                Value::Float(f) => Ok(Some(Value::Float(f.cos()))),
701                Value::Int(i) => Ok(Some(Value::Float((*i as f64).cos()))),
702                _ => Err(format!("cos requires a number, got {}", args[0].type_name())),
703            }
704        }
705        "tan" => {
706            if args.len() != 1 { return Err("tan requires exactly 1 argument".into()); }
707            match &args[0] {
708                Value::Float(f) => Ok(Some(Value::Float(f.tan()))),
709                Value::Int(i) => Ok(Some(Value::Float((*i as f64).tan()))),
710                _ => Err(format!("tan requires a number, got {}", args[0].type_name())),
711            }
712        }
713        "asin" => {
714            if args.len() != 1 { return Err("asin requires exactly 1 argument".into()); }
715            match &args[0] {
716                Value::Float(f) => Ok(Some(Value::Float(f.asin()))),
717                Value::Int(i) => Ok(Some(Value::Float((*i as f64).asin()))),
718                _ => Err(format!("asin requires a number, got {}", args[0].type_name())),
719            }
720        }
721        "acos" => {
722            if args.len() != 1 { return Err("acos requires exactly 1 argument".into()); }
723            match &args[0] {
724                Value::Float(f) => Ok(Some(Value::Float(f.acos()))),
725                Value::Int(i) => Ok(Some(Value::Float((*i as f64).acos()))),
726                _ => Err(format!("acos requires a number, got {}", args[0].type_name())),
727            }
728        }
729        "atan" => {
730            if args.len() != 1 { return Err("atan requires exactly 1 argument".into()); }
731            match &args[0] {
732                Value::Float(f) => Ok(Some(Value::Float(f.atan()))),
733                Value::Int(i) => Ok(Some(Value::Float((*i as f64).atan()))),
734                _ => Err(format!("atan requires a number, got {}", args[0].type_name())),
735            }
736        }
737        "atan2" => {
738            if args.len() != 2 { return Err("atan2 requires exactly 2 arguments".into()); }
739            let y = match &args[0] {
740                Value::Float(f) => *f,
741                Value::Int(i) => *i as f64,
742                _ => return Err(format!("atan2 requires numbers, got {}", args[0].type_name())),
743            };
744            let x = match &args[1] {
745                Value::Float(f) => *f,
746                Value::Int(i) => *i as f64,
747                _ => return Err(format!("atan2 requires numbers, got {}", args[1].type_name())),
748            };
749            Ok(Some(Value::Float(y.atan2(x))))
750        }
751        // ---- Mathematics Hardening Phase: Hyperbolic ----
752        "sinh" => {
753            if args.len() != 1 { return Err("sinh requires exactly 1 argument".into()); }
754            match &args[0] {
755                Value::Float(f) => Ok(Some(Value::Float(f.sinh()))),
756                Value::Int(i) => Ok(Some(Value::Float((*i as f64).sinh()))),
757                _ => Err(format!("sinh requires a number, got {}", args[0].type_name())),
758            }
759        }
760        "cosh" => {
761            if args.len() != 1 { return Err("cosh requires exactly 1 argument".into()); }
762            match &args[0] {
763                Value::Float(f) => Ok(Some(Value::Float(f.cosh()))),
764                Value::Int(i) => Ok(Some(Value::Float((*i as f64).cosh()))),
765                _ => Err(format!("cosh requires a number, got {}", args[0].type_name())),
766            }
767        }
768        "tanh" | "tanh_scalar" => {
769            if args.len() != 1 { return Err("tanh requires exactly 1 argument".into()); }
770            match &args[0] {
771                Value::Float(f) => Ok(Some(Value::Float(f.tanh()))),
772                Value::Int(i) => Ok(Some(Value::Float((*i as f64).tanh()))),
773                Value::Tensor(t) => Ok(Some(Value::Tensor(t.tanh_activation()))),
774                _ => Err(format!("tanh requires a number or Tensor, got {}", args[0].type_name())),
775            }
776        }
777        // ---- Mathematics Hardening Phase: Exponentiation & Logarithms ----
778        "pow" => {
779            if args.len() != 2 { return Err("pow requires exactly 2 arguments".into()); }
780            let base = match &args[0] {
781                Value::Float(f) => *f,
782                Value::Int(i) => *i as f64,
783                _ => return Err(format!("pow requires numbers, got {}", args[0].type_name())),
784            };
785            let exp = match &args[1] {
786                Value::Float(f) => *f,
787                Value::Int(i) => *i as f64,
788                _ => return Err(format!("pow requires numbers, got {}", args[1].type_name())),
789            };
790            Ok(Some(Value::Float(base.powf(exp))))
791        }
792        "log2" => {
793            if args.len() != 1 { return Err("log2 requires exactly 1 argument".into()); }
794            match &args[0] {
795                Value::Float(f) => Ok(Some(Value::Float(f.log2()))),
796                Value::Int(i) => Ok(Some(Value::Float((*i as f64).log2()))),
797                _ => Err(format!("log2 requires a number, got {}", args[0].type_name())),
798            }
799        }
800        "log10" => {
801            if args.len() != 1 { return Err("log10 requires exactly 1 argument".into()); }
802            match &args[0] {
803                Value::Float(f) => Ok(Some(Value::Float(f.log10()))),
804                Value::Int(i) => Ok(Some(Value::Float((*i as f64).log10()))),
805                _ => Err(format!("log10 requires a number, got {}", args[0].type_name())),
806            }
807        }
808        "log1p" => {
809            if args.len() != 1 { return Err("log1p requires exactly 1 argument".into()); }
810            match &args[0] {
811                Value::Float(f) => Ok(Some(Value::Float(f.ln_1p()))),
812                Value::Int(i) => Ok(Some(Value::Float((*i as f64).ln_1p()))),
813                _ => Err(format!("log1p requires a number, got {}", args[0].type_name())),
814            }
815        }
816        "expm1" => {
817            if args.len() != 1 { return Err("expm1 requires exactly 1 argument".into()); }
818            match &args[0] {
819                Value::Float(f) => Ok(Some(Value::Float(f.exp_m1()))),
820                Value::Int(i) => Ok(Some(Value::Float((*i as f64).exp_m1()))),
821                _ => Err(format!("expm1 requires a number, got {}", args[0].type_name())),
822            }
823        }
824        // ---- Mathematics Hardening Phase: Rounding ----
825        "ceil" => {
826            if args.len() != 1 { return Err("ceil requires exactly 1 argument".into()); }
827            match &args[0] {
828                Value::Float(f) => Ok(Some(Value::Float(f.ceil()))),
829                Value::Int(i) => Ok(Some(Value::Int(*i))),
830                _ => Err(format!("ceil requires a number, got {}", args[0].type_name())),
831            }
832        }
833        "round" => {
834            if args.len() != 1 { return Err("round requires exactly 1 argument".into()); }
835            match &args[0] {
836                Value::Float(f) => Ok(Some(Value::Float(f.round()))),
837                Value::Int(i) => Ok(Some(Value::Int(*i))),
838                _ => Err(format!("round requires a number, got {}", args[0].type_name())),
839            }
840        }
841        "floor" => {
842            if args.len() != 1 {
843                return Err("floor requires exactly 1 argument".into());
844            }
845            match &args[0] {
846                Value::Float(f) => Ok(Some(Value::Float(f.floor()))),
847                Value::Int(i) => Ok(Some(Value::Int(*i))),
848                _ => Err(format!("floor requires a number, got {}", args[0].type_name())),
849            }
850        }
851        "int" => {
852            if args.len() != 1 {
853                return Err("int requires exactly 1 argument".into());
854            }
855            match &args[0] {
856                Value::Float(f) => Ok(Some(Value::Int(*f as i64))),
857                Value::Int(i) => Ok(Some(Value::Int(*i))),
858                Value::Bool(b) => Ok(Some(Value::Int(if *b { 1 } else { 0 }))),
859                _ => Err(format!("int requires a number, got {}", args[0].type_name())),
860            }
861        }
862        "float" => {
863            if args.len() != 1 {
864                return Err("float requires exactly 1 argument".into());
865            }
866            match &args[0] {
867                Value::Int(i) => Ok(Some(Value::Float(*i as f64))),
868                Value::Float(f) => Ok(Some(Value::Float(*f))),
869                Value::Bool(b) => Ok(Some(Value::Float(if *b { 1.0 } else { 0.0 }))),
870                _ => Err(format!("float requires a number, got {}", args[0].type_name())),
871            }
872        }
873        "isnan" => {
874            if args.len() != 1 {
875                return Err("isnan requires exactly 1 argument".into());
876            }
877            match &args[0] {
878                Value::Float(f) => Ok(Some(Value::Bool(f.is_nan()))),
879                Value::Int(_) => Ok(Some(Value::Bool(false))),
880                _ => Err(format!("isnan requires a number, got {}", args[0].type_name())),
881            }
882        }
883        "isinf" => {
884            if args.len() != 1 {
885                return Err("isinf requires exactly 1 argument".into());
886            }
887            match &args[0] {
888                Value::Float(f) => Ok(Some(Value::Bool(f.is_infinite()))),
889                Value::Int(_) => Ok(Some(Value::Bool(false))),
890                _ => Err(format!("isinf requires a number, got {}", args[0].type_name())),
891            }
892        }
893        "abs" => {
894            if args.len() != 1 {
895                return Err("abs requires exactly 1 argument".into());
896            }
897            match &args[0] {
898                Value::Float(f) => Ok(Some(Value::Float(f.abs()))),
899                Value::Int(i) => Ok(Some(Value::Int(i.abs()))),
900                _ => Err(format!("abs requires a number, got {}", args[0].type_name())),
901            }
902        }
903        // ---- Mathematics Hardening Phase: Comparison & Sign ----
904        "min" => {
905            if args.len() != 2 { return Err("min requires exactly 2 arguments".into()); }
906            let a = match &args[0] {
907                Value::Float(f) => *f,
908                Value::Int(i) => *i as f64,
909                _ => return Err(format!("min requires numbers, got {}", args[0].type_name())),
910            };
911            let b = match &args[1] {
912                Value::Float(f) => *f,
913                Value::Int(i) => *i as f64,
914                _ => return Err(format!("min requires numbers, got {}", args[1].type_name())),
915            };
916            Ok(Some(Value::Float(a.min(b))))
917        }
918        "max" => {
919            if args.len() != 2 { return Err("max requires exactly 2 arguments".into()); }
920            let a = match &args[0] {
921                Value::Float(f) => *f,
922                Value::Int(i) => *i as f64,
923                _ => return Err(format!("max requires numbers, got {}", args[0].type_name())),
924            };
925            let b = match &args[1] {
926                Value::Float(f) => *f,
927                Value::Int(i) => *i as f64,
928                _ => return Err(format!("max requires numbers, got {}", args[1].type_name())),
929            };
930            Ok(Some(Value::Float(a.max(b))))
931        }
932        "sign" => {
933            if args.len() != 1 { return Err("sign requires exactly 1 argument".into()); }
934            match &args[0] {
935                Value::Float(f) => Ok(Some(Value::Float(f.signum()))),
936                Value::Int(i) => Ok(Some(Value::Float((*i as f64).signum()))),
937                _ => Err(format!("sign requires a number, got {}", args[0].type_name())),
938            }
939        }
940        // ---- Mathematics Hardening Phase: Precision Helpers ----
941        "hypot" => {
942            if args.len() != 2 { return Err("hypot requires exactly 2 arguments".into()); }
943            let x = match &args[0] {
944                Value::Float(f) => *f,
945                Value::Int(i) => *i as f64,
946                _ => return Err(format!("hypot requires numbers, got {}", args[0].type_name())),
947            };
948            let y = match &args[1] {
949                Value::Float(f) => *f,
950                Value::Int(i) => *i as f64,
951                _ => return Err(format!("hypot requires numbers, got {}", args[1].type_name())),
952            };
953            Ok(Some(Value::Float(x.hypot(y))))
954        }
955        // ---- Mathematics Hardening Phase: Constants ----
956        "PI" => {
957            if !args.is_empty() { return Err("PI takes no arguments".into()); }
958            Ok(Some(Value::Float(std::f64::consts::PI)))
959        }
960        "E" => {
961            if !args.is_empty() { return Err("E takes no arguments".into()); }
962            Ok(Some(Value::Float(std::f64::consts::E)))
963        }
964        "TAU" => {
965            if !args.is_empty() { return Err("TAU takes no arguments".into()); }
966            Ok(Some(Value::Float(std::f64::consts::TAU)))
967        }
968        "INF" => {
969            if !args.is_empty() { return Err("INF takes no arguments".into()); }
970            Ok(Some(Value::Float(f64::INFINITY)))
971        }
972        "NAN_VAL" => {
973            if !args.is_empty() { return Err("NAN_VAL takes no arguments".into()); }
974            Ok(Some(Value::Float(f64::NAN)))
975        }
976        // ---- Mathematics Hardening Phase: Vector Operations ----
977        "dot" => {
978            if args.len() != 2 { return Err("dot requires exactly 2 arguments".into()); }
979            let a = match &args[0] {
980                Value::Tensor(t) => t,
981                _ => return Err(format!("dot requires tensors, got {}", args[0].type_name())),
982            };
983            let b = match &args[1] {
984                Value::Tensor(t) => t,
985                _ => return Err(format!("dot requires tensors, got {}", args[1].type_name())),
986            };
987            if a.ndim() != 1 || b.ndim() != 1 {
988                return Err("dot requires 1D tensors".into());
989            }
990            if a.len() != b.len() {
991                return Err(format!("dot: length mismatch ({} vs {})", a.len(), b.len()));
992            }
993            let av = a.to_vec();
994            let bv = b.to_vec();
995            let products: Vec<f64> = av.iter().zip(bv.iter()).map(|(x, y)| x * y).collect();
996            let sum = crate::accumulator::binned_sum_f64(&products);
997            Ok(Some(Value::Float(sum)))
998        }
999        "outer" => {
1000            if args.len() != 2 { return Err("outer requires exactly 2 arguments".into()); }
1001            let a = match &args[0] {
1002                Value::Tensor(t) => t,
1003                _ => return Err(format!("outer requires tensors, got {}", args[0].type_name())),
1004            };
1005            let b = match &args[1] {
1006                Value::Tensor(t) => t,
1007                _ => return Err(format!("outer requires tensors, got {}", args[1].type_name())),
1008            };
1009            if a.ndim() != 1 || b.ndim() != 1 {
1010                return Err("outer requires 1D tensors".into());
1011            }
1012            let av = a.to_vec();
1013            let bv = b.to_vec();
1014            let m = av.len();
1015            let n = bv.len();
1016            let mut data = Vec::with_capacity(m * n);
1017            for ai in &av {
1018                for bj in &bv {
1019                    data.push(ai * bj);
1020                }
1021            }
1022            Ok(Some(Value::Tensor(Tensor::from_vec(data, &[m, n]).map_err(|e| format!("{e}"))?)))
1023        }
1024        "cross" => {
1025            if args.len() != 2 { return Err("cross requires exactly 2 arguments".into()); }
1026            let a = match &args[0] {
1027                Value::Tensor(t) => t,
1028                _ => return Err(format!("cross requires tensors, got {}", args[0].type_name())),
1029            };
1030            let b = match &args[1] {
1031                Value::Tensor(t) => t,
1032                _ => return Err(format!("cross requires tensors, got {}", args[1].type_name())),
1033            };
1034            if a.ndim() != 1 || b.ndim() != 1 || a.len() != 3 || b.len() != 3 {
1035                return Err("cross requires two 3-element 1D tensors".into());
1036            }
1037            let av = a.to_vec();
1038            let bv = b.to_vec();
1039            let result = vec![
1040                av[1] * bv[2] - av[2] * bv[1],
1041                av[2] * bv[0] - av[0] * bv[2],
1042                av[0] * bv[1] - av[1] * bv[0],
1043            ];
1044            Ok(Some(Value::Tensor(Tensor::from_vec(result, &[3]).map_err(|e| format!("{e}"))?)))
1045        }
1046        "norm" => {
1047            if args.len() < 1 || args.len() > 2 { return Err("norm requires 1-2 arguments".into()); }
1048            let t = match &args[0] {
1049                Value::Tensor(t) => t,
1050                _ => return Err(format!("norm requires a tensor, got {}", args[0].type_name())),
1051            };
1052            let ord = if args.len() == 2 {
1053                match &args[1] {
1054                    Value::Int(i) => *i,
1055                    Value::Float(f) => *f as i64,
1056                    _ => return Err("norm: ord must be an integer".into()),
1057                }
1058            } else {
1059                2 // default: L2 norm
1060            };
1061            let data = t.to_vec();
1062            let result = match ord {
1063                1 => {
1064                    let abs_vals: Vec<f64> = data.iter().map(|x| x.abs()).collect();
1065                    crate::accumulator::binned_sum_f64(&abs_vals)
1066                }
1067                2 => {
1068                    let sq_vals: Vec<f64> = data.iter().map(|x| x * x).collect();
1069                    crate::accumulator::binned_sum_f64(&sq_vals).sqrt()
1070                }
1071                _ => {
1072                    let p = ord as f64;
1073                    let pow_vals: Vec<f64> = data.iter().map(|x| x.abs().powf(p)).collect();
1074                    crate::accumulator::binned_sum_f64(&pow_vals).powf(1.0 / p)
1075                }
1076            };
1077            Ok(Some(Value::Float(result)))
1078        }
1079        // ---- Mathematics Hardening Phase: Tensor Constructors ----
1080        "Tensor.linspace" => {
1081            if args.len() != 3 { return Err("Tensor.linspace requires 3 arguments (start, end, n)".into()); }
1082            let start = match &args[0] {
1083                Value::Float(f) => *f,
1084                Value::Int(i) => *i as f64,
1085                _ => return Err("Tensor.linspace: start must be a number".into()),
1086            };
1087            let end = match &args[1] {
1088                Value::Float(f) => *f,
1089                Value::Int(i) => *i as f64,
1090                _ => return Err("Tensor.linspace: end must be a number".into()),
1091            };
1092            let n = match &args[2] {
1093                Value::Int(i) => *i as usize,
1094                _ => return Err("Tensor.linspace: n must be an integer".into()),
1095            };
1096            if n == 0 {
1097                return Ok(Some(Value::Tensor(Tensor::from_vec(vec![], &[0]).map_err(|e| format!("{e}"))?)));
1098            }
1099            if n == 1 {
1100                return Ok(Some(Value::Tensor(Tensor::from_vec(vec![start], &[1]).map_err(|e| format!("{e}"))?)));
1101            }
1102            let step = (end - start) / (n as f64 - 1.0);
1103            let data: Vec<f64> = (0..n).map(|i| start + step * i as f64).collect();
1104            Ok(Some(Value::Tensor(Tensor::from_vec(data, &[n]).map_err(|e| format!("{e}"))?)))
1105        }
1106        "Tensor.arange" => {
1107            if args.len() < 2 || args.len() > 3 { return Err("Tensor.arange requires 2-3 arguments (start, end, step?)".into()); }
1108            let start = match &args[0] {
1109                Value::Float(f) => *f,
1110                Value::Int(i) => *i as f64,
1111                _ => return Err("Tensor.arange: start must be a number".into()),
1112            };
1113            let end = match &args[1] {
1114                Value::Float(f) => *f,
1115                Value::Int(i) => *i as f64,
1116                _ => return Err("Tensor.arange: end must be a number".into()),
1117            };
1118            let step = if args.len() == 3 {
1119                match &args[2] {
1120                    Value::Float(f) => *f,
1121                    Value::Int(i) => *i as f64,
1122                    _ => return Err("Tensor.arange: step must be a number".into()),
1123                }
1124            } else {
1125                1.0
1126            };
1127            if step == 0.0 { return Err("Tensor.arange: step cannot be zero".into()); }
1128            let mut data = Vec::new();
1129            let mut val = start;
1130            if step > 0.0 {
1131                while val < end { data.push(val); val += step; }
1132            } else {
1133                while val > end { data.push(val); val += step; }
1134            }
1135            let n = data.len();
1136            Ok(Some(Value::Tensor(Tensor::from_vec(data, &[n]).map_err(|e| format!("{e}"))?)))
1137        }
1138        "Tensor.eye" => {
1139            if args.len() != 1 { return Err("Tensor.eye requires 1 argument (n)".into()); }
1140            let n = match &args[0] {
1141                Value::Int(i) => *i as usize,
1142                _ => return Err("Tensor.eye: n must be an integer".into()),
1143            };
1144            let mut data = vec![0.0; n * n];
1145            for i in 0..n {
1146                data[i * n + i] = 1.0;
1147            }
1148            Ok(Some(Value::Tensor(Tensor::from_vec(data, &[n, n]).map_err(|e| format!("{e}"))?)))
1149        }
1150        "Tensor.full" => {
1151            if args.len() != 2 { return Err("Tensor.full requires 2 arguments (shape, value)".into()); }
1152            let shape = match &args[0] {
1153                Value::Array(arr) => {
1154                    let mut s = Vec::new();
1155                    for v in arr.iter() {
1156                        match v {
1157                            Value::Int(i) => s.push(*i as usize),
1158                            _ => return Err("Tensor.full: shape must be an array of ints".into()),
1159                        }
1160                    }
1161                    s
1162                }
1163                _ => return Err("Tensor.full: shape must be an array".into()),
1164            };
1165            let fill_val = match &args[1] {
1166                Value::Float(f) => *f,
1167                Value::Int(i) => *i as f64,
1168                _ => return Err("Tensor.full: value must be a number".into()),
1169            };
1170            let total: usize = shape.iter().product();
1171            let data = vec![fill_val; total];
1172            Ok(Some(Value::Tensor(Tensor::from_vec(data, &shape).map_err(|e| format!("{e}"))?)))
1173        }
1174        "Tensor.diag" => {
1175            if args.len() != 1 { return Err("Tensor.diag requires 1 argument".into()); }
1176            let t = match &args[0] {
1177                Value::Tensor(t) => t,
1178                _ => return Err("Tensor.diag requires a tensor".into()),
1179            };
1180            match t.ndim() {
1181                1 => {
1182                    // 1D -> 2D diagonal matrix
1183                    let data = t.to_vec();
1184                    let n = data.len();
1185                    let mut out = vec![0.0; n * n];
1186                    for i in 0..n {
1187                        out[i * n + i] = data[i];
1188                    }
1189                    Ok(Some(Value::Tensor(Tensor::from_vec(out, &[n, n]).map_err(|e| format!("{e}"))?)))
1190                }
1191                2 => {
1192                    // 2D -> 1D diagonal extraction
1193                    let rows = t.shape()[0];
1194                    let cols = t.shape()[1];
1195                    let n = rows.min(cols);
1196                    let mut data = Vec::with_capacity(n);
1197                    for i in 0..n {
1198                        data.push(t.get(&[i, i]).map_err(|e| format!("{e}"))?);
1199                    }
1200                    Ok(Some(Value::Tensor(Tensor::from_vec(data, &[n]).map_err(|e| format!("{e}"))?)))
1201                }
1202                _ => Err("Tensor.diag requires a 1D or 2D tensor".into()),
1203            }
1204        }
1205        "assert" => {
1206            if args.len() != 1 {
1207                return Err("assert requires exactly 1 argument".into());
1208            }
1209            match &args[0] {
1210                Value::Bool(true) => Ok(Some(Value::Void)),
1211                Value::Bool(false) => Err("assertion failed".into()),
1212                other => Err(format!("assert requires Bool, got {}", other.type_name())),
1213            }
1214        }
1215        "assert_eq" => {
1216            if args.len() != 2 {
1217                return Err("assert_eq requires exactly 2 arguments".into());
1218            }
1219            if values_equal(&args[0], &args[1]) {
1220                Ok(Some(Value::Void))
1221            } else {
1222                Err(format!("assertion failed: `{}` != `{}`", args[0], args[1]))
1223            }
1224        }
1225        // ── JSON builtins ──────────────────────────────────────────
1226        "json_parse" => {
1227            if args.len() != 1 {
1228                return Err("json_parse requires exactly 1 argument".into());
1229            }
1230            let s = match &args[0] {
1231                Value::String(s) => s.as_str(),
1232                _ => return Err(format!("json_parse requires String, got {}", args[0].type_name())),
1233            };
1234            crate::json::json_parse(s).map(Some)
1235        }
1236        "json_stringify" => {
1237            if args.len() != 1 {
1238                return Err("json_stringify requires exactly 1 argument".into());
1239            }
1240            crate::json::json_stringify(&args[0]).map(|s| Some(Value::String(Rc::new(s))))
1241        }
1242
1243        // ── DateTime builtins (pure arithmetic, except datetime_now) ──
1244        "datetime_from_epoch" => {
1245            if args.len() != 1 {
1246                return Err("datetime_from_epoch requires exactly 1 argument".into());
1247            }
1248            match &args[0] {
1249                Value::Int(n) => Ok(Some(Value::Int(crate::datetime::datetime_from_epoch(*n)))),
1250                _ => Err(format!("datetime_from_epoch requires Int, got {}", args[0].type_name())),
1251            }
1252        }
1253        "datetime_from_parts" => {
1254            if args.len() != 6 {
1255                return Err("datetime_from_parts requires 6 arguments (year, month, day, hour, min, sec)".into());
1256            }
1257            let mut vals = [0i64; 6];
1258            for (i, arg) in args.iter().enumerate() {
1259                match arg {
1260                    Value::Int(n) => vals[i] = *n,
1261                    _ => return Err(format!("datetime_from_parts arg {} must be Int", i)),
1262                }
1263            }
1264            Ok(Some(Value::Int(crate::datetime::datetime_from_parts(
1265                vals[0], vals[1], vals[2], vals[3], vals[4], vals[5],
1266            ))))
1267        }
1268        "datetime_year" => {
1269            if args.len() != 1 { return Err("datetime_year requires 1 argument".into()); }
1270            match &args[0] {
1271                Value::Int(n) => Ok(Some(Value::Int(crate::datetime::datetime_year(*n)))),
1272                _ => Err(format!("datetime_year requires Int, got {}", args[0].type_name())),
1273            }
1274        }
1275        "datetime_month" => {
1276            if args.len() != 1 { return Err("datetime_month requires 1 argument".into()); }
1277            match &args[0] {
1278                Value::Int(n) => Ok(Some(Value::Int(crate::datetime::datetime_month(*n)))),
1279                _ => Err(format!("datetime_month requires Int, got {}", args[0].type_name())),
1280            }
1281        }
1282        "datetime_day" => {
1283            if args.len() != 1 { return Err("datetime_day requires 1 argument".into()); }
1284            match &args[0] {
1285                Value::Int(n) => Ok(Some(Value::Int(crate::datetime::datetime_day(*n)))),
1286                _ => Err(format!("datetime_day requires Int, got {}", args[0].type_name())),
1287            }
1288        }
1289        "datetime_hour" => {
1290            if args.len() != 1 { return Err("datetime_hour requires 1 argument".into()); }
1291            match &args[0] {
1292                Value::Int(n) => Ok(Some(Value::Int(crate::datetime::datetime_hour(*n)))),
1293                _ => Err(format!("datetime_hour requires Int, got {}", args[0].type_name())),
1294            }
1295        }
1296        "datetime_minute" => {
1297            if args.len() != 1 { return Err("datetime_minute requires 1 argument".into()); }
1298            match &args[0] {
1299                Value::Int(n) => Ok(Some(Value::Int(crate::datetime::datetime_minute(*n)))),
1300                _ => Err(format!("datetime_minute requires Int, got {}", args[0].type_name())),
1301            }
1302        }
1303        "datetime_second" => {
1304            if args.len() != 1 { return Err("datetime_second requires 1 argument".into()); }
1305            match &args[0] {
1306                Value::Int(n) => Ok(Some(Value::Int(crate::datetime::datetime_second(*n)))),
1307                _ => Err(format!("datetime_second requires Int, got {}", args[0].type_name())),
1308            }
1309        }
1310        "datetime_diff" => {
1311            if args.len() != 2 { return Err("datetime_diff requires 2 arguments".into()); }
1312            match (&args[0], &args[1]) {
1313                (Value::Int(a), Value::Int(b)) => Ok(Some(Value::Int(crate::datetime::datetime_diff(*a, *b)))),
1314                _ => Err("datetime_diff requires two Int arguments".into()),
1315            }
1316        }
1317        "datetime_add_millis" => {
1318            if args.len() != 2 { return Err("datetime_add_millis requires 2 arguments".into()); }
1319            match (&args[0], &args[1]) {
1320                (Value::Int(a), Value::Int(b)) => Ok(Some(Value::Int(crate::datetime::datetime_add_millis(*a, *b)))),
1321                _ => Err("datetime_add_millis requires two Int arguments".into()),
1322            }
1323        }
1324        "datetime_format" => {
1325            if args.len() != 1 { return Err("datetime_format requires 1 argument".into()); }
1326            match &args[0] {
1327                Value::Int(n) => Ok(Some(Value::String(Rc::new(crate::datetime::datetime_format(*n))))),
1328                _ => Err(format!("datetime_format requires Int, got {}", args[0].type_name())),
1329            }
1330        }
1331
1332        // ── File I/O builtins ─────────────────────────────────────────
1333        "file_read" => {
1334            if args.len() != 1 { return Err("file_read requires 1 argument".into()); }
1335            match &args[0] {
1336                Value::String(path) => {
1337                    let content = std::fs::read_to_string(path.as_str())
1338                        .map_err(|e| format!("file_read error: {}", e))?;
1339                    Ok(Some(Value::String(Rc::new(content))))
1340                }
1341                _ => Err(format!("file_read requires String path, got {}", args[0].type_name())),
1342            }
1343        }
1344        "file_write" => {
1345            if args.len() != 2 { return Err("file_write requires 2 arguments (path, content)".into()); }
1346            match (&args[0], &args[1]) {
1347                (Value::String(path), Value::String(content)) => {
1348                    std::fs::write(path.as_str(), content.as_str())
1349                        .map_err(|e| format!("file_write error: {}", e))?;
1350                    Ok(Some(Value::Void))
1351                }
1352                _ => Err("file_write requires (String, String) arguments".into()),
1353            }
1354        }
1355        "file_append" => {
1356            if args.len() != 2 { return Err("file_append requires 2 arguments (path, content)".into()); }
1357            match (&args[0], &args[1]) {
1358                (Value::String(path), Value::String(content)) => {
1359                    use std::io::Write;
1360                    let mut f = std::fs::OpenOptions::new()
1361                        .create(true)
1362                        .append(true)
1363                        .open(path.as_str())
1364                        .map_err(|e| format!("file_append error: {}", e))?;
1365                    f.write_all(content.as_bytes())
1366                        .map_err(|e| format!("file_append error: {}", e))?;
1367                    Ok(Some(Value::Void))
1368                }
1369                _ => Err("file_append requires (String, String) arguments".into()),
1370            }
1371        }
1372        "file_exists" => {
1373            if args.len() != 1 { return Err("file_exists requires 1 argument".into()); }
1374            match &args[0] {
1375                Value::String(path) => Ok(Some(Value::Bool(std::path::Path::new(path.as_str()).exists()))),
1376                _ => Err(format!("file_exists requires String path, got {}", args[0].type_name())),
1377            }
1378        }
1379        "file_lines" => {
1380            if args.len() != 1 { return Err("file_lines requires 1 argument".into()); }
1381            match &args[0] {
1382                Value::String(path) => {
1383                    let content = std::fs::read_to_string(path.as_str())
1384                        .map_err(|e| format!("file_lines error: {}", e))?;
1385                    let lines: Vec<Value> = content
1386                        .lines()
1387                        .map(|l| Value::String(Rc::new(l.to_string())))
1388                        .collect();
1389                    Ok(Some(Value::Array(Rc::new(lines))))
1390                }
1391                _ => Err(format!("file_lines requires String path, got {}", args[0].type_name())),
1392            }
1393        }
1394
1395        // ── Profile counters (Tier 2, Chess RL v2.3) ─────────────────
1396        // Thread-local write-only sink. The handle returned by
1397        // profile_zone_start is an opaque token for profile_zone_stop;
1398        // program logic must not branch on its integer value. See
1399        // `docs/chess_rl_v2/PROFILE_DESIGN.md` for the full determinism
1400        // story.
1401        "profile_zone_start" => {
1402            if args.len() != 1 {
1403                return Err("profile_zone_start requires 1 argument (name: String)".into());
1404            }
1405            match &args[0] {
1406                Value::String(name) => {
1407                    let h = crate::profile::zone_start(name.as_str());
1408                    Ok(Some(Value::Int(h)))
1409                }
1410                _ => Err(format!(
1411                    "profile_zone_start requires String name, got {}",
1412                    args[0].type_name()
1413                )),
1414            }
1415        }
1416        "profile_zone_stop" => {
1417            if args.len() != 1 {
1418                return Err("profile_zone_stop requires 1 argument (handle: Int)".into());
1419            }
1420            match &args[0] {
1421                Value::Int(h) => {
1422                    let elapsed = crate::profile::zone_stop(*h);
1423                    Ok(Some(Value::Float(elapsed)))
1424                }
1425                _ => Err(format!(
1426                    "profile_zone_stop requires Int handle, got {}",
1427                    args[0].type_name()
1428                )),
1429            }
1430        }
1431        "profile_dump" => {
1432            if args.len() != 1 {
1433                return Err("profile_dump requires 1 argument (path: String)".into());
1434            }
1435            match &args[0] {
1436                Value::String(path) => {
1437                    let rows = crate::profile::dump_to_path(path.as_str())?;
1438                    Ok(Some(Value::Int(rows)))
1439                }
1440                _ => Err(format!(
1441                    "profile_dump requires String path, got {}",
1442                    args[0].type_name()
1443                )),
1444            }
1445        }
1446
1447        // ── Chess RL v2.3 native hot-path kernels (Tier 3) ────────────
1448        // These replace O(n²) CJC-Lang loops with tight Rust implementations.
1449        // They must produce **bit-identical** output to the pure-CJC-Lang
1450        // equivalents for every legal chess position.
1451
1452        // encode_state_fast(board, side, castling, ep_sq, halfmove) -> Tensor[1,774]
1453        //
1454        // Native replacement for the CJC-Lang `encode_state` function.
1455        // The pure-CJC version calls `arr_set` ~38 times, each copying the
1456        // entire 774-element array (quadratic). This version fills a
1457        // pre-allocated buffer in a single pass: O(774).
1458        //
1459        // Determinism story: pure arithmetic on integer inputs. No RNG, no
1460        // FMA, no iteration-order dependence. The output is a fresh tensor
1461        // with exactly the same f64 values that the CJC-Lang version would
1462        // produce. Tested via bit-identical parity on ≥100 random boards.
1463        "encode_state_fast" => {
1464            if args.len() != 5 {
1465                return Err(
1466                    "encode_state_fast requires 5 arguments: board(Array[64]), side(Int), castling(Array[4]), ep_sq(Int), halfmove(Int)"
1467                        .into(),
1468                );
1469            }
1470            let board = match &args[0] {
1471                Value::Array(arr) => {
1472                    if arr.len() != 64 {
1473                        return Err(format!(
1474                            "encode_state_fast: board must have 64 elements, got {}",
1475                            arr.len()
1476                        ));
1477                    }
1478                    let mut b = [0i64; 64];
1479                    for (i, v) in arr.iter().enumerate() {
1480                        b[i] = match v {
1481                            Value::Int(n) => *n,
1482                            _ => {
1483                                return Err(format!(
1484                                    "encode_state_fast: board[{i}] must be Int, got {}",
1485                                    v.type_name()
1486                                ))
1487                            }
1488                        };
1489                    }
1490                    b
1491                }
1492                _ => {
1493                    return Err(format!(
1494                        "encode_state_fast: board must be Array, got {}",
1495                        args[0].type_name()
1496                    ))
1497                }
1498            };
1499            let side = match &args[1] {
1500                Value::Int(n) => *n,
1501                _ => {
1502                    return Err(format!(
1503                        "encode_state_fast: side must be Int, got {}",
1504                        args[1].type_name()
1505                    ))
1506                }
1507            };
1508            let castling = match &args[2] {
1509                Value::Array(arr) => {
1510                    if arr.len() != 4 {
1511                        return Err(format!(
1512                            "encode_state_fast: castling must have 4 elements, got {}",
1513                            arr.len()
1514                        ));
1515                    }
1516                    let mut c = [0i64; 4];
1517                    for (i, v) in arr.iter().enumerate() {
1518                        c[i] = match v {
1519                            Value::Int(n) => *n,
1520                            _ => {
1521                                return Err(format!(
1522                                    "encode_state_fast: castling[{i}] must be Int, got {}",
1523                                    v.type_name()
1524                                ))
1525                            }
1526                        };
1527                    }
1528                    c
1529                }
1530                _ => {
1531                    return Err(format!(
1532                        "encode_state_fast: castling must be Array, got {}",
1533                        args[2].type_name()
1534                    ))
1535                }
1536            };
1537            let ep_sq = match &args[3] {
1538                Value::Int(n) => *n,
1539                _ => {
1540                    return Err(format!(
1541                        "encode_state_fast: ep_sq must be Int, got {}",
1542                        args[3].type_name()
1543                    ))
1544                }
1545            };
1546            let halfmove = match &args[4] {
1547                Value::Int(n) => *n,
1548                _ => {
1549                    return Err(format!(
1550                        "encode_state_fast: halfmove must be Int, got {}",
1551                        args[4].type_name()
1552                    ))
1553                }
1554            };
1555
1556            // --- Compute the 774-dim feature vector ---
1557            let mut data = vec![0.0f64; 774];
1558
1559            // Helper: feat_sq — flip for black.
1560            let feat_sq = |sq: i64, s: i64| -> usize {
1561                if s == 1 {
1562                    sq as usize
1563                } else {
1564                    let r = sq / 8;
1565                    let f = sq % 8;
1566                    ((7 - r) * 8 + f) as usize
1567                }
1568            };
1569
1570            // Fill piece planes.
1571            for i in 0..64 {
1572                let p = board[i];
1573                if p != 0 {
1574                    let abs_p = p.unsigned_abs() as i64;
1575                    if abs_p < 1 || abs_p > 6 {
1576                        return Err(format!(
1577                            "encode_state_fast: invalid piece value {} at square {}",
1578                            p, i
1579                        ));
1580                    }
1581                    let piece_idx = abs_p - 1; // 0..5
1582                    let owner = if p > 0 { 1i64 } else { -1i64 };
1583                    let plane = if owner == side {
1584                        piece_idx
1585                    } else {
1586                        piece_idx + 6
1587                    };
1588                    let mapped = feat_sq(i as i64, side);
1589                    let idx = (plane as usize) * 64 + mapped;
1590                    data[idx] = 1.0;
1591                }
1592            }
1593
1594            // Castling rights (flipped if black to move).
1595            let (my_k, my_q, op_k, op_q) = if side == 1 {
1596                (castling[0], castling[1], castling[2], castling[3])
1597            } else {
1598                (castling[2], castling[3], castling[0], castling[1])
1599            };
1600            data[768] = my_k as f64;
1601            data[769] = my_q as f64;
1602            data[770] = op_k as f64;
1603            data[771] = op_q as f64;
1604
1605            // Halfmove clock fraction.
1606            data[772] = (halfmove as f64) / 100.0;
1607
1608            // Has en passant target.
1609            data[773] = if ep_sq >= 0 { 1.0 } else { 0.0 };
1610
1611            let tensor = Tensor::from_vec(data, &[1, 774]).map_err(|e| format!("{e}"))?;
1612            Ok(Some(Value::Tensor(tensor)))
1613        }
1614
1615        // score_moves_batch(weights_list, feature, legal_move_pairs) -> [scores_tensor, value]
1616        //
1617        // Native replacement for the CJC-Lang `score_moves` function.
1618        // The pure-CJC version loops over legal moves, doing per-move
1619        // `tensor.get()` + `array_push()` (O(num_moves²) due to COW copies).
1620        // This version does a single forward pass and gathers scores from
1621        // the logit tensors by index.
1622        //
1623        // Arguments:
1624        //   weights_list: Array of 11 values [W1, b1, W2, b2, _, Wpf, bpf, Wpt, bpt, Wv, bv]
1625        //   feature:      Tensor [1, 774]
1626        //   legal_move_pairs: Array of [from_sq, to_sq, from_sq, to_sq, ...] (flat i64 pairs)
1627        //   side: Int (1 for white, -1 for black)
1628        //
1629        // Returns: Array [scores_tensor([num_moves]), value_f64]
1630        //
1631        // Determinism story: uses the same matmul + element-wise ops already
1632        // in cjc-runtime (Kahan-accumulated, no FMA). The only change is
1633        // replacing the per-move interpreter loop with a Rust loop over
1634        // the legal move indices + direct f64 reads from the logit tensors.
1635        "score_moves_batch" => {
1636            if args.len() != 4 {
1637                return Err(
1638                    "score_moves_batch requires 4 arguments: weights(Array[11]), feature(Tensor[1,774]), legal_moves(Array), side(Int)"
1639                        .into(),
1640                );
1641            }
1642            // Parse weights.
1643            let weights = match &args[0] {
1644                Value::Array(arr) => {
1645                    if arr.len() < 11 {
1646                        return Err(format!(
1647                            "score_moves_batch: weights must have ≥11 elements, got {}",
1648                            arr.len()
1649                        ));
1650                    }
1651                    arr.clone()
1652                }
1653                _ => {
1654                    return Err(format!(
1655                        "score_moves_batch: weights must be Array, got {}",
1656                        args[0].type_name()
1657                    ))
1658                }
1659            };
1660            let feature = value_to_tensor(&args[1])?;
1661            let moves = match &args[2] {
1662                Value::Array(arr) => {
1663                    let mut mv = Vec::with_capacity(arr.len());
1664                    for (i, v) in arr.iter().enumerate() {
1665                        mv.push(match v {
1666                            Value::Int(n) => *n,
1667                            _ => {
1668                                return Err(format!(
1669                                    "score_moves_batch: legal_moves[{i}] must be Int, got {}",
1670                                    v.type_name()
1671                                ))
1672                            }
1673                        });
1674                    }
1675                    mv
1676                }
1677                _ => {
1678                    return Err(format!(
1679                        "score_moves_batch: legal_moves must be Array, got {}",
1680                        args[2].type_name()
1681                    ))
1682                }
1683            };
1684            let side = match &args[3] {
1685                Value::Int(n) => *n,
1686                _ => {
1687                    return Err(format!(
1688                        "score_moves_batch: side must be Int, got {}",
1689                        args[3].type_name()
1690                    ))
1691                }
1692            };
1693
1694            let num_moves = moves.len() / 2;
1695
1696            // Extract weight tensors.
1697            let w1 = value_to_tensor(&weights[0])?;
1698            let b1 = value_to_tensor(&weights[1])?;
1699            let w2 = value_to_tensor(&weights[2])?;
1700            let b2 = value_to_tensor(&weights[3])?;
1701            // weights[4] is reserved (placeholder 0)
1702            let wpf = value_to_tensor(&weights[5])?;
1703            let bpf = value_to_tensor(&weights[6])?;
1704            let wpt = value_to_tensor(&weights[7])?;
1705            let bpt = value_to_tensor(&weights[8])?;
1706            let wv = value_to_tensor(&weights[9])?;
1707            let bv = value_to_tensor(&weights[10])?;
1708
1709            // Forward pass: identical to forward_eager in the CJC-Lang PRELUDE.
1710            // z1 = feature @ W1 + b1
1711            let z1 = feature.matmul(&w1).map_err(|e| format!("{e}"))?
1712                .add(&b1).map_err(|e| format!("{e}"))?;
1713            let h1 = z1.relu();
1714            // z2 = h1 @ W2 + b2
1715            let z2 = h1.matmul(&w2).map_err(|e| format!("{e}"))?
1716                .add(&b2).map_err(|e| format!("{e}"))?;
1717            let h2 = z2.relu();
1718            // from_logits = h2 @ Wpf + bpf  [1, 64]
1719            let from_logits = h2.matmul(&wpf).map_err(|e| format!("{e}"))?
1720                .add(&bpf).map_err(|e| format!("{e}"))?;
1721            // to_logits = h2 @ Wpt + bpt  [1, 64]
1722            let to_logits = h2.matmul(&wpt).map_err(|e| format!("{e}"))?
1723                .add(&bpt).map_err(|e| format!("{e}"))?;
1724            // value = tanh(h2 @ Wv + bv)[0, 0]
1725            let v_pre = h2.matmul(&wv).map_err(|e| format!("{e}"))?
1726                .add(&bv).map_err(|e| format!("{e}"))?;
1727            let v_tanh = v_pre.tanh_activation();
1728            let value = v_tanh.get(&[0, 0]).map_err(|e| format!("{e}"))?;
1729
1730            // Gather scores for legal moves.
1731            // feat_sq: flip for black.
1732            let feat_sq = |sq: i64, s: i64| -> usize {
1733                if s == 1 {
1734                    sq as usize
1735                } else {
1736                    let r = sq / 8;
1737                    let f = sq % 8;
1738                    ((7 - r) * 8 + f) as usize
1739                }
1740            };
1741
1742            let mut scores = Vec::with_capacity(num_moves);
1743            for i in 0..num_moves {
1744                let from_sq = moves[i * 2];
1745                let to_sq = moves[i * 2 + 1];
1746                let from_mapped = feat_sq(from_sq, side);
1747                let to_mapped = feat_sq(to_sq, side);
1748                let fs = from_logits.get(&[0, from_mapped]).map_err(|e| format!("{e}"))?;
1749                let ts = to_logits.get(&[0, to_mapped]).map_err(|e| format!("{e}"))?;
1750                scores.push(fs + ts);
1751            }
1752
1753            let scores_tensor = Tensor::from_vec(scores, &[num_moves]).map_err(|e| format!("{e}"))?;
1754            let result = vec![
1755                Value::Tensor(scores_tensor),
1756                Value::Float(value),
1757            ];
1758            Ok(Some(Value::Array(Rc::new(result))))
1759        }
1760
1761        // ── Tensor snap (deterministic binary serialization) ─────────
1762        "tensor_save" => {
1763            if args.len() != 2 {
1764                return Err("tensor_save requires 2 arguments: path, tensor".into());
1765            }
1766            let path = match &args[0] {
1767                Value::String(p) => p.as_str().to_string(),
1768                _ => return Err("tensor_save: first argument must be String path".into()),
1769            };
1770            let tensor = value_to_tensor(&args[1])?;
1771            let bytes = crate::tensor_snap::encode_one(tensor);
1772            std::fs::write(&path, &bytes)
1773                .map_err(|e| format!("tensor_save error: {}", e))?;
1774            Ok(Some(Value::Void))
1775        }
1776        "tensor_load" => {
1777            if args.len() != 1 {
1778                return Err("tensor_load requires 1 argument: path".into());
1779            }
1780            let path = match &args[0] {
1781                Value::String(p) => p.as_str().to_string(),
1782                _ => return Err("tensor_load: argument must be String path".into()),
1783            };
1784            let bytes = std::fs::read(&path)
1785                .map_err(|e| format!("tensor_load error: {}", e))?;
1786            let tensor = crate::tensor_snap::decode_one(&bytes)
1787                .map_err(|e| format!("tensor_load error: {}", e))?;
1788            Ok(Some(Value::Tensor(tensor)))
1789        }
1790        "tensor_list_save" => {
1791            if args.len() != 2 {
1792                return Err("tensor_list_save requires 2 arguments: path, [tensors]".into());
1793            }
1794            let path = match &args[0] {
1795                Value::String(p) => p.as_str().to_string(),
1796                _ => return Err("tensor_list_save: first argument must be String path".into()),
1797            };
1798            let tensors = match &args[1] {
1799                Value::Array(arr) => {
1800                    let mut out = Vec::with_capacity(arr.len());
1801                    for v in arr.iter() {
1802                        out.push(value_to_tensor(v)?.clone());
1803                    }
1804                    out
1805                }
1806                _ => return Err("tensor_list_save: second argument must be Array of Tensors".into()),
1807            };
1808            let bytes = crate::tensor_snap::encode_list(&tensors);
1809            std::fs::write(&path, &bytes)
1810                .map_err(|e| format!("tensor_list_save error: {}", e))?;
1811            Ok(Some(Value::Void))
1812        }
1813        "tensor_list_load" => {
1814            if args.len() != 1 {
1815                return Err("tensor_list_load requires 1 argument: path".into());
1816            }
1817            let path = match &args[0] {
1818                Value::String(p) => p.as_str().to_string(),
1819                _ => return Err("tensor_list_load: argument must be String path".into()),
1820            };
1821            let bytes = std::fs::read(&path)
1822                .map_err(|e| format!("tensor_list_load error: {}", e))?;
1823            let tensors = crate::tensor_snap::decode_list(&bytes)
1824                .map_err(|e| format!("tensor_list_load error: {}", e))?;
1825            let arr: Vec<Value> = tensors.into_iter().map(Value::Tensor).collect();
1826            Ok(Some(Value::Array(Rc::new(arr))))
1827        }
1828        "tensor_list_hash" => {
1829            if args.len() != 1 {
1830                return Err("tensor_list_hash requires 1 argument: [tensors]".into());
1831            }
1832            let tensors = match &args[0] {
1833                Value::Array(arr) => {
1834                    let mut out = Vec::with_capacity(arr.len());
1835                    for v in arr.iter() {
1836                        out.push(value_to_tensor(v)?.clone());
1837                    }
1838                    out
1839                }
1840                _ => return Err("tensor_list_hash: argument must be Array of Tensors".into()),
1841            };
1842            let h = crate::tensor_snap::hash_list(&tensors);
1843            // Return as a signed i64 so CJC-Lang code can print/compare it
1844            // without unsigned support. Cast via `as` preserves bit pattern.
1845            Ok(Some(Value::Int(h as i64)))
1846        }
1847        // ── Adam optimizer fused step (Phase B1) ─────────────────────
1848        // Signature: adam_step(w, g, m, v, lr, b1, b2, eps, t) -> [new_w, new_m, new_v]
1849        // Applies one bias-corrected Adam update across the whole tensor in
1850        // native code. Present in cjc-runtime so both cjc-eval and
1851        // cjc-mir-exec dispatch through the same deterministic path —
1852        // byte-identical by construction.
1853        "adam_step" => {
1854            if args.len() != 9 {
1855                return Err("adam_step requires 9 arguments: w, g, m, v, lr, b1, b2, eps, t".into());
1856            }
1857            let w = value_to_tensor(&args[0])?;
1858            let g = value_to_tensor(&args[1])?;
1859            let m = value_to_tensor(&args[2])?;
1860            let v = value_to_tensor(&args[3])?;
1861            let lr = match &args[4] {
1862                Value::Float(f) => *f,
1863                Value::Int(i) => *i as f64,
1864                _ => return Err(format!("adam_step lr must be number, got {}", args[4].type_name())),
1865            };
1866            let b1 = match &args[5] {
1867                Value::Float(f) => *f,
1868                Value::Int(i) => *i as f64,
1869                _ => return Err(format!("adam_step b1 must be number, got {}", args[5].type_name())),
1870            };
1871            let b2 = match &args[6] {
1872                Value::Float(f) => *f,
1873                Value::Int(i) => *i as f64,
1874                _ => return Err(format!("adam_step b2 must be number, got {}", args[6].type_name())),
1875            };
1876            let eps = match &args[7] {
1877                Value::Float(f) => *f,
1878                Value::Int(i) => *i as f64,
1879                _ => return Err(format!("adam_step eps must be number, got {}", args[7].type_name())),
1880            };
1881            let t = match &args[8] {
1882                Value::Int(i) => *i,
1883                Value::Float(f) => *f as i64,
1884                _ => return Err(format!("adam_step t must be integer, got {}", args[8].type_name())),
1885            };
1886            if t < 1 {
1887                return Err(format!("adam_step t must be >= 1, got {}", t));
1888            }
1889            let shape = w.shape();
1890            if g.shape() != shape || m.shape() != shape || v.shape() != shape {
1891                return Err(format!(
1892                    "adam_step: w/g/m/v shapes must match; got w={:?} g={:?} m={:?} v={:?}",
1893                    shape, g.shape(), m.shape(), v.shape()
1894                ));
1895            }
1896            let shape_vec: Vec<usize> = shape.to_vec();
1897            let w_flat = w.to_vec();
1898            let g_flat = g.to_vec();
1899            let m_flat = m.to_vec();
1900            let v_flat = v.to_vec();
1901            let n = w_flat.len();
1902            // Bias correction computed once.
1903            let bc1 = 1.0 - b1.powf(t as f64);
1904            let bc2 = 1.0 - b2.powf(t as f64);
1905            let inv_b1 = 1.0 - b1;
1906            let inv_b2 = 1.0 - b2;
1907            let mut new_w = Vec::with_capacity(n);
1908            let mut new_m = Vec::with_capacity(n);
1909            let mut new_v = Vec::with_capacity(n);
1910            for i in 0..n {
1911                let gi = g_flat[i];
1912                let new_mi = b1 * m_flat[i] + inv_b1 * gi;
1913                let new_vi = b2 * v_flat[i] + inv_b2 * gi * gi;
1914                let m_hat = new_mi / bc1;
1915                let v_hat = new_vi / bc2;
1916                let new_wi = w_flat[i] - lr * m_hat / (v_hat.sqrt() + eps);
1917                new_w.push(new_wi);
1918                new_m.push(new_mi);
1919                new_v.push(new_vi);
1920            }
1921            let new_w_t = Tensor::from_vec(new_w, &shape_vec)
1922                .map_err(|e| format!("adam_step: {}", e))?;
1923            let new_m_t = Tensor::from_vec(new_m, &shape_vec)
1924                .map_err(|e| format!("adam_step: {}", e))?;
1925            let new_v_t = Tensor::from_vec(new_v, &shape_vec)
1926                .map_err(|e| format!("adam_step: {}", e))?;
1927            Ok(Some(Value::Array(Rc::new(vec![
1928                Value::Tensor(new_w_t),
1929                Value::Tensor(new_m_t),
1930                Value::Tensor(new_v_t),
1931            ]))))
1932        }
1933        // ── TidyView Phase 1: Data I/O builtins ──────────────────────
1934        "dir_list" => {
1935            if args.len() != 1 { return Err("dir_list requires 1 argument (path)".into()); }
1936            match &args[0] {
1937                Value::String(path) => {
1938                    let entries = std::fs::read_dir(path.as_str())
1939                        .map_err(|e| format!("dir_list error: {}", e))?;
1940                    // Collect into BTreeSet for deterministic ordering
1941                    let mut sorted = std::collections::BTreeSet::new();
1942                    for entry in entries {
1943                        let entry = entry.map_err(|e| format!("dir_list error: {}", e))?;
1944                        let name = entry.file_name().to_string_lossy().to_string();
1945                        sorted.insert(name);
1946                    }
1947                    let values: Vec<Value> = sorted
1948                        .into_iter()
1949                        .map(|s| Value::String(Rc::new(s)))
1950                        .collect();
1951                    Ok(Some(Value::Array(Rc::new(values))))
1952                }
1953                _ => Err(format!("dir_list requires String path, got {}", args[0].type_name())),
1954            }
1955        }
1956        "path_join" => {
1957            if args.len() != 2 { return Err("path_join requires 2 arguments (base, segment)".into()); }
1958            match (&args[0], &args[1]) {
1959                (Value::String(a), Value::String(b)) => {
1960                    let joined = std::path::Path::new(a.as_str())
1961                        .join(b.as_str())
1962                        .to_string_lossy()
1963                        .to_string();
1964                    Ok(Some(Value::String(Rc::new(joined))))
1965                }
1966                _ => Err(format!(
1967                    "path_join requires (String, String) arguments, got ({}, {})",
1968                    args[0].type_name(), args[1].type_name()
1969                )),
1970            }
1971        }
1972
1973        // ── Window function builtins ──────────────────────────────────
1974        "window_sum" | "window_mean" | "window_min" | "window_max" => {
1975            if args.len() != 2 {
1976                return Err(format!("{name} requires 2 arguments (array, window_size)"));
1977            }
1978            let data = value_to_f64_vec(&args[0])?;
1979            let ws = match &args[1] {
1980                Value::Int(i) => {
1981                    if *i < 0 {
1982                        return Err(format!("{name}: window_size must be non-negative, got {i}"));
1983                    }
1984                    *i as usize
1985                }
1986                _ => return Err(format!("{name} requires Int window_size, got {}", args[1].type_name())),
1987            };
1988            let result = match name {
1989                "window_sum" => crate::window::window_sum(&data, ws),
1990                "window_mean" => crate::window::window_mean(&data, ws),
1991                "window_min" => crate::window::window_min(&data, ws),
1992                "window_max" => crate::window::window_max(&data, ws),
1993                _ => unreachable!(),
1994            };
1995            let values: Vec<Value> = result.into_iter().map(Value::Float).collect();
1996            Ok(Some(Value::Array(Rc::new(values))))
1997        }
1998
1999        // ── Stats builtins ───────────────────────────────────────────
2000        "mean" => {
2001            if args.len() != 1 { return Err("mean requires 1 argument".into()); }
2002            let data = value_to_f64_vec(&args[0])?;
2003            if data.is_empty() { return Err("mean: empty data".into()); }
2004            Ok(Some(Value::Float(cjc_repro::kahan_sum_f64(&data) / data.len() as f64)))
2005        }
2006        "variance" => {
2007            if args.len() != 1 { return Err("variance requires 1 argument".into()); }
2008            let data = value_to_f64_vec(&args[0])?;
2009            Ok(Some(Value::Float(crate::stats::variance(&data)?)))
2010        }
2011        "sd" => {
2012            if args.len() != 1 { return Err("sd requires 1 argument".into()); }
2013            let data = value_to_f64_vec(&args[0])?;
2014            Ok(Some(Value::Float(crate::stats::sd(&data)?)))
2015        }
2016        "se" => {
2017            if args.len() != 1 { return Err("se requires 1 argument".into()); }
2018            let data = value_to_f64_vec(&args[0])?;
2019            Ok(Some(Value::Float(crate::stats::se(&data)?)))
2020        }
2021        "median" => {
2022            if args.len() != 1 { return Err("median requires 1 argument".into()); }
2023            let data = value_to_f64_vec(&args[0])?;
2024            Ok(Some(Value::Float(crate::stats::median(&data)?)))
2025        }
2026        "quantile" => {
2027            if args.len() != 2 { return Err("quantile requires 2 arguments".into()); }
2028            let data = value_to_f64_vec(&args[0])?;
2029            let p = match &args[1] {
2030                Value::Float(f) => *f,
2031                Value::Int(i) => *i as f64,
2032                _ => return Err("quantile: p must be a number".into()),
2033            };
2034            Ok(Some(Value::Float(crate::stats::quantile(&data, p)?)))
2035        }
2036        // ── Bastion primitives ──────────────────────────────────────
2037        "nth_element" => {
2038            if args.len() != 2 { return Err("nth_element requires 2 arguments: data, k".into()); }
2039            let data = value_to_f64_vec(&args[0])?;
2040            let k = value_to_usize(&args[1])?;
2041            Ok(Some(Value::Float(crate::stats::nth_element_copy(&data, k)?)))
2042        }
2043        "median_fast" => {
2044            if args.len() != 1 { return Err("median_fast requires 1 argument".into()); }
2045            let data = value_to_f64_vec(&args[0])?;
2046            Ok(Some(Value::Float(crate::stats::median_fast(&data)?)))
2047        }
2048        "quantile_fast" => {
2049            if args.len() != 2 { return Err("quantile_fast requires 2 arguments: data, p".into()); }
2050            let data = value_to_f64_vec(&args[0])?;
2051            let p = match &args[1] {
2052                Value::Float(f) => *f,
2053                Value::Int(i) => *i as f64,
2054                _ => return Err("quantile_fast: p must be a number".into()),
2055            };
2056            Ok(Some(Value::Float(crate::stats::quantile_fast(&data, p)?)))
2057        }
2058        "filter_mask" => {
2059            if args.len() != 2 { return Err("filter_mask requires 2 arguments: data, mask".into()); }
2060            let data = value_to_f64_vec(&args[0])?;
2061            let mask: Vec<bool> = match &args[1] {
2062                Value::Array(arr) => arr.iter().map(|v| match v {
2063                    Value::Bool(b) => Ok(*b),
2064                    Value::Int(i) => Ok(*i != 0),
2065                    _ => Err("filter_mask: mask must be array of bools".to_string()),
2066                }).collect::<Result<Vec<_>, _>>()?,
2067                _ => return Err("filter_mask: mask must be an array".into()),
2068            };
2069            let result = crate::stats::filter_mask(&data, &mask)?;
2070            let values: Vec<Value> = result.into_iter().map(Value::Float).collect();
2071            Ok(Some(Value::Array(Rc::new(values))))
2072        }
2073        "erf" => {
2074            if args.len() != 1 { return Err("erf requires 1 argument".into()); }
2075            let x = match &args[0] {
2076                Value::Float(f) => *f,
2077                Value::Int(i) => *i as f64,
2078                _ => return Err("erf requires a number".into()),
2079            };
2080            Ok(Some(Value::Float(crate::distributions::erf(x))))
2081        }
2082        "erfc" => {
2083            if args.len() != 1 { return Err("erfc requires 1 argument".into()); }
2084            let x = match &args[0] {
2085                Value::Float(f) => *f,
2086                Value::Int(i) => *i as f64,
2087                _ => return Err("erfc requires a number".into()),
2088            };
2089            Ok(Some(Value::Float(crate::distributions::erfc(x))))
2090        }
2091        "iqr" => {
2092            if args.len() != 1 { return Err("iqr requires 1 argument".into()); }
2093            let data = value_to_f64_vec(&args[0])?;
2094            Ok(Some(Value::Float(crate::stats::iqr(&data)?)))
2095        }
2096        "skewness" => {
2097            if args.len() != 1 { return Err("skewness requires 1 argument".into()); }
2098            let data = value_to_f64_vec(&args[0])?;
2099            Ok(Some(Value::Float(crate::stats::skewness(&data)?)))
2100        }
2101        "kurtosis" => {
2102            if args.len() != 1 { return Err("kurtosis requires 1 argument".into()); }
2103            let data = value_to_f64_vec(&args[0])?;
2104            Ok(Some(Value::Float(crate::stats::kurtosis(&data)?)))
2105        }
2106        "z_score" => {
2107            if args.len() != 1 { return Err("z_score requires 1 argument".into()); }
2108            let data = value_to_f64_vec(&args[0])?;
2109            let result = crate::stats::z_score(&data)?;
2110            let values: Vec<Value> = result.into_iter().map(Value::Float).collect();
2111            Ok(Some(Value::Array(Rc::new(values))))
2112        }
2113        "standardize" => {
2114            if args.len() != 1 { return Err("standardize requires 1 argument".into()); }
2115            let data = value_to_f64_vec(&args[0])?;
2116            let result = crate::stats::standardize(&data)?;
2117            let values: Vec<Value> = result.into_iter().map(Value::Float).collect();
2118            Ok(Some(Value::Array(Rc::new(values))))
2119        }
2120        "n_distinct" => {
2121            if args.len() != 1 { return Err("n_distinct requires 1 argument".into()); }
2122            let data = value_to_f64_vec(&args[0])?;
2123            Ok(Some(Value::Int(crate::stats::n_distinct(&data) as i64)))
2124        }
2125        // ── Correlation builtins ─────────────────────────────────────
2126        "cor" => {
2127            if args.len() != 2 { return Err("cor requires 2 arguments".into()); }
2128            let x = value_to_f64_vec(&args[0])?;
2129            let y = value_to_f64_vec(&args[1])?;
2130            Ok(Some(Value::Float(crate::stats::cor(&x, &y)?)))
2131        }
2132        "cov" => {
2133            if args.len() != 2 { return Err("cov requires 2 arguments".into()); }
2134            let x = value_to_f64_vec(&args[0])?;
2135            let y = value_to_f64_vec(&args[1])?;
2136            Ok(Some(Value::Float(crate::stats::cov(&x, &y)?)))
2137        }
2138        // ── Distribution builtins ────────────────────────────────────
2139        "normal_cdf" => {
2140            if args.len() != 1 { return Err("normal_cdf requires 1 argument".into()); }
2141            let x = match &args[0] {
2142                Value::Float(f) => *f,
2143                Value::Int(i) => *i as f64,
2144                _ => return Err("normal_cdf requires a number".into()),
2145            };
2146            Ok(Some(Value::Float(crate::distributions::normal_cdf(x))))
2147        }
2148        "normal_pdf" => {
2149            if args.len() != 1 { return Err("normal_pdf requires 1 argument".into()); }
2150            let x = match &args[0] {
2151                Value::Float(f) => *f,
2152                Value::Int(i) => *i as f64,
2153                _ => return Err("normal_pdf requires a number".into()),
2154            };
2155            Ok(Some(Value::Float(crate::distributions::normal_pdf(x))))
2156        }
2157        "normal_ppf" => {
2158            if args.len() != 1 { return Err("normal_ppf requires 1 argument".into()); }
2159            let p = match &args[0] {
2160                Value::Float(f) => *f,
2161                Value::Int(i) => *i as f64,
2162                _ => return Err("normal_ppf requires a number".into()),
2163            };
2164            Ok(Some(Value::Float(crate::distributions::normal_ppf(p)?)))
2165        }
2166        "t_cdf" => {
2167            if args.len() != 2 { return Err("t_cdf requires 2 arguments (x, df)".into()); }
2168            let x = match &args[0] { Value::Float(f) => *f, Value::Int(i) => *i as f64, _ => return Err("t_cdf: x must be a number".into()) };
2169            let df = match &args[1] { Value::Float(f) => *f, Value::Int(i) => *i as f64, _ => return Err("t_cdf: df must be a number".into()) };
2170            Ok(Some(Value::Float(crate::distributions::t_cdf(x, df))))
2171        }
2172        "chi2_cdf" => {
2173            if args.len() != 2 { return Err("chi2_cdf requires 2 arguments (x, df)".into()); }
2174            let x = match &args[0] { Value::Float(f) => *f, Value::Int(i) => *i as f64, _ => return Err("chi2_cdf: x must be a number".into()) };
2175            let df = match &args[1] { Value::Float(f) => *f, Value::Int(i) => *i as f64, _ => return Err("chi2_cdf: df must be a number".into()) };
2176            Ok(Some(Value::Float(crate::distributions::chi2_cdf(x, df))))
2177        }
2178        "f_cdf" => {
2179            if args.len() != 3 { return Err("f_cdf requires 3 arguments (x, df1, df2)".into()); }
2180            let x = match &args[0] { Value::Float(f) => *f, Value::Int(i) => *i as f64, _ => return Err("f_cdf: x must be a number".into()) };
2181            let df1 = match &args[1] { Value::Float(f) => *f, Value::Int(i) => *i as f64, _ => return Err("f_cdf: df1 must be a number".into()) };
2182            let df2 = match &args[2] { Value::Float(f) => *f, Value::Int(i) => *i as f64, _ => return Err("f_cdf: df2 must be a number".into()) };
2183            Ok(Some(Value::Float(crate::distributions::f_cdf(x, df1, df2))))
2184        }
2185        // ── Hypothesis test builtins ─────────────────────────────────
2186        "t_test" => {
2187            if args.len() != 2 { return Err("t_test requires 2 arguments (data, mu)".into()); }
2188            let data = value_to_f64_vec(&args[0])?;
2189            let mu = match &args[1] { Value::Float(f) => *f, Value::Int(i) => *i as f64, _ => return Err("t_test: mu must be a number".into()) };
2190            let r = crate::hypothesis::t_test(&data, mu)?;
2191            let mut fields = std::collections::BTreeMap::new();
2192            fields.insert("t_statistic".into(), Value::Float(r.t_statistic));
2193            fields.insert("p_value".into(), Value::Float(r.p_value));
2194            fields.insert("df".into(), Value::Float(r.df));
2195            fields.insert("mean".into(), Value::Float(r.mean));
2196            fields.insert("se".into(), Value::Float(r.se));
2197            Ok(Some(Value::Struct { name: "TTestResult".into(), fields }))
2198        }
2199        "t_test_two_sample" => {
2200            if args.len() != 2 { return Err("t_test_two_sample requires 2 arguments".into()); }
2201            let x = value_to_f64_vec(&args[0])?;
2202            let y = value_to_f64_vec(&args[1])?;
2203            let r = crate::hypothesis::t_test_two_sample(&x, &y)?;
2204            let mut fields = std::collections::BTreeMap::new();
2205            fields.insert("t_statistic".into(), Value::Float(r.t_statistic));
2206            fields.insert("p_value".into(), Value::Float(r.p_value));
2207            fields.insert("df".into(), Value::Float(r.df));
2208            Ok(Some(Value::Struct { name: "TTestResult".into(), fields }))
2209        }
2210        "chi_squared_test" => {
2211            if args.len() != 2 { return Err("chi_squared_test requires 2 arguments".into()); }
2212            let obs = value_to_f64_vec(&args[0])?;
2213            let exp = value_to_f64_vec(&args[1])?;
2214            let r = crate::hypothesis::chi_squared_test(&obs, &exp)?;
2215            let mut fields = std::collections::BTreeMap::new();
2216            fields.insert("chi2".into(), Value::Float(r.chi2));
2217            fields.insert("p_value".into(), Value::Float(r.p_value));
2218            fields.insert("df".into(), Value::Float(r.df));
2219            Ok(Some(Value::Struct { name: "ChiSquaredResult".into(), fields }))
2220        }
2221        // ── Linalg builtins ──────────────────────────────────────────
2222        "det" => {
2223            if args.len() != 1 { return Err("det requires 1 Tensor argument".into()); }
2224            let t = value_to_tensor(&args[0])?;
2225            Ok(Some(Value::Float(t.det().map_err(|e| format!("{e}"))?)))
2226        }
2227        "solve" => {
2228            if args.len() != 2 { return Err("solve requires 2 Tensor arguments".into()); }
2229            let a = value_to_tensor(&args[0])?;
2230            let b = value_to_tensor(&args[1])?;
2231            Ok(Some(Value::Tensor(a.solve(b).map_err(|e| format!("{e}"))?)))
2232        }
2233        "lstsq" => {
2234            if args.len() != 2 { return Err("lstsq requires 2 Tensor arguments".into()); }
2235            let a = value_to_tensor(&args[0])?;
2236            let b = value_to_tensor(&args[1])?;
2237            Ok(Some(Value::Tensor(a.lstsq(b).map_err(|e| format!("{e}"))?)))
2238        }
2239        "trace" => {
2240            if args.len() != 1 { return Err("trace requires 1 Tensor argument".into()); }
2241            let t = value_to_tensor(&args[0])?;
2242            Ok(Some(Value::Float(t.trace().map_err(|e| format!("{e}"))?)))
2243        }
2244        "norm_frobenius" => {
2245            if args.len() != 1 { return Err("norm_frobenius requires 1 Tensor argument".into()); }
2246            let t = value_to_tensor(&args[0])?;
2247            Ok(Some(Value::Float(t.norm_frobenius().map_err(|e| format!("{e}"))?)))
2248        }
2249        "eigh" => {
2250            if args.len() != 1 { return Err("eigh requires 1 Tensor argument".into()); }
2251            let t = value_to_tensor(&args[0])?;
2252            let (vals, vecs) = t.eigh().map_err(|e| format!("{e}"))?;
2253            let val_values: Vec<Value> = vals.into_iter().map(Value::Float).collect();
2254            Ok(Some(Value::Tuple(Rc::new(vec![
2255                Value::Array(Rc::new(val_values)),
2256                Value::Tensor(vecs),
2257            ]))))
2258        }
2259        "matrix_rank" => {
2260            if args.len() != 1 { return Err("matrix_rank requires 1 Tensor argument".into()); }
2261            let t = value_to_tensor(&args[0])?;
2262            Ok(Some(Value::Int(t.matrix_rank().map_err(|e| format!("{e}"))? as i64)))
2263        }
2264        "kron" => {
2265            if args.len() != 2 { return Err("kron requires 2 Tensor arguments".into()); }
2266            let a = value_to_tensor(&args[0])?;
2267            let b = value_to_tensor(&args[1])?;
2268            Ok(Some(Value::Tensor(a.kron(b).map_err(|e| format!("{e}"))?)))
2269        }
2270        // ── ML builtins ──────────────────────────────────────────────
2271        "mse_loss" => {
2272            if args.len() != 2 { return Err("mse_loss requires 2 arguments".into()); }
2273            let pred = value_to_f64_vec(&args[0])?;
2274            let target = value_to_f64_vec(&args[1])?;
2275            Ok(Some(Value::Float(crate::ml::mse_loss(&pred, &target)?)))
2276        }
2277        "cross_entropy_loss" => {
2278            if args.len() != 2 { return Err("cross_entropy_loss requires 2 arguments".into()); }
2279            let pred = value_to_f64_vec(&args[0])?;
2280            let target = value_to_f64_vec(&args[1])?;
2281            Ok(Some(Value::Float(crate::ml::cross_entropy_loss(&pred, &target)?)))
2282        }
2283        "huber_loss" => {
2284            if args.len() != 3 { return Err("huber_loss requires 3 arguments".into()); }
2285            let pred = value_to_f64_vec(&args[0])?;
2286            let target = value_to_f64_vec(&args[1])?;
2287            let delta = match &args[2] { Value::Float(f) => *f, Value::Int(i) => *i as f64, _ => return Err("huber_loss: delta must be a number".into()) };
2288            Ok(Some(Value::Float(crate::ml::huber_loss(&pred, &target, delta)?)))
2289        }
2290        // ── Cumulative builtins ──────────────────────────────────────
2291        "cumsum" => {
2292            if args.len() != 1 { return Err("cumsum requires 1 argument".into()); }
2293            let data = value_to_f64_vec(&args[0])?;
2294            let result = crate::stats::cumsum(&data);
2295            let values: Vec<Value> = result.into_iter().map(Value::Float).collect();
2296            Ok(Some(Value::Array(Rc::new(values))))
2297        }
2298        "cumprod" => {
2299            if args.len() != 1 { return Err("cumprod requires 1 argument".into()); }
2300            let data = value_to_f64_vec(&args[0])?;
2301            let result = crate::stats::cumprod(&data);
2302            let values: Vec<Value> = result.into_iter().map(Value::Float).collect();
2303            Ok(Some(Value::Array(Rc::new(values))))
2304        }
2305        "cummax" => {
2306            if args.len() != 1 { return Err("cummax requires 1 argument".into()); }
2307            let data = value_to_f64_vec(&args[0])?;
2308            let result = crate::stats::cummax(&data);
2309            let values: Vec<Value> = result.into_iter().map(Value::Float).collect();
2310            Ok(Some(Value::Array(Rc::new(values))))
2311        }
2312        "cummin" => {
2313            if args.len() != 1 { return Err("cummin requires 1 argument".into()); }
2314            let data = value_to_f64_vec(&args[0])?;
2315            let result = crate::stats::cummin(&data);
2316            let values: Vec<Value> = result.into_iter().map(Value::Float).collect();
2317            Ok(Some(Value::Array(Rc::new(values))))
2318        }
2319        "lag" => {
2320            if args.len() != 2 { return Err("lag requires 2 arguments".into()); }
2321            let data = value_to_f64_vec(&args[0])?;
2322            let n = value_to_usize(&args[1])?;
2323            let result = crate::stats::lag(&data, n);
2324            let values: Vec<Value> = result.into_iter().map(Value::Float).collect();
2325            Ok(Some(Value::Array(Rc::new(values))))
2326        }
2327        "lead" => {
2328            if args.len() != 2 { return Err("lead requires 2 arguments".into()); }
2329            let data = value_to_f64_vec(&args[0])?;
2330            let n = value_to_usize(&args[1])?;
2331            let result = crate::stats::lead(&data, n);
2332            let values: Vec<Value> = result.into_iter().map(Value::Float).collect();
2333            Ok(Some(Value::Array(Rc::new(values))))
2334        }
2335        "rank" => {
2336            if args.len() != 1 { return Err("rank requires 1 argument".into()); }
2337            let data = value_to_f64_vec(&args[0])?;
2338            let result = crate::stats::rank(&data);
2339            let values: Vec<Value> = result.into_iter().map(Value::Float).collect();
2340            Ok(Some(Value::Array(Rc::new(values))))
2341        }
2342        "dense_rank" => {
2343            if args.len() != 1 { return Err("dense_rank requires 1 argument".into()); }
2344            let data = value_to_f64_vec(&args[0])?;
2345            let result = crate::stats::dense_rank(&data);
2346            let values: Vec<Value> = result.into_iter().map(Value::Float).collect();
2347            Ok(Some(Value::Array(Rc::new(values))))
2348        }
2349        "histogram" => {
2350            if args.len() != 2 { return Err("histogram requires 2 arguments".into()); }
2351            let data = value_to_f64_vec(&args[0])?;
2352            let n_bins = value_to_usize(&args[1])?;
2353            let (edges, counts) = crate::stats::histogram(&data, n_bins)?;
2354            let edge_values: Vec<Value> = edges.into_iter().map(Value::Float).collect();
2355            let count_values: Vec<Value> = counts.into_iter().map(|c| Value::Int(c as i64)).collect();
2356            Ok(Some(Value::Tuple(Rc::new(vec![
2357                Value::Array(Rc::new(edge_values)),
2358                Value::Array(Rc::new(count_values)),
2359            ]))))
2360        }
2361        // ── Additional stats builtins ───────────────────────────────
2362        "sample_variance" => {
2363            if args.len() != 1 { return Err("sample_variance requires 1 argument".into()); }
2364            let data = value_to_f64_vec(&args[0])?;
2365            Ok(Some(Value::Float(crate::stats::sample_variance(&data)?)))
2366        }
2367        "sample_sd" => {
2368            if args.len() != 1 { return Err("sample_sd requires 1 argument".into()); }
2369            let data = value_to_f64_vec(&args[0])?;
2370            Ok(Some(Value::Float(crate::stats::sample_sd(&data)?)))
2371        }
2372        "sample_cov" => {
2373            if args.len() != 2 { return Err("sample_cov requires 2 arguments".into()); }
2374            let x = value_to_f64_vec(&args[0])?;
2375            let y = value_to_f64_vec(&args[1])?;
2376            Ok(Some(Value::Float(crate::stats::sample_cov(&x, &y)?)))
2377        }
2378        "row_number" => {
2379            if args.len() != 1 { return Err("row_number requires 1 argument".into()); }
2380            let data = value_to_f64_vec(&args[0])?;
2381            let result = crate::stats::row_number(&data);
2382            let values: Vec<Value> = result.into_iter().map(Value::Float).collect();
2383            Ok(Some(Value::Array(Rc::new(values))))
2384        }
2385        // ── Distribution PPF builtins ───────────────────────────────
2386        "t_ppf" => {
2387            if args.len() != 2 { return Err("t_ppf requires 2 arguments (p, df)".into()); }
2388            let p = match &args[0] { Value::Float(f) => *f, Value::Int(i) => *i as f64, _ => return Err("t_ppf: p must be a number".into()) };
2389            let df = match &args[1] { Value::Float(f) => *f, Value::Int(i) => *i as f64, _ => return Err("t_ppf: df must be a number".into()) };
2390            Ok(Some(Value::Float(crate::distributions::t_ppf(p, df)?)))
2391        }
2392        "chi2_ppf" => {
2393            if args.len() != 2 { return Err("chi2_ppf requires 2 arguments (p, df)".into()); }
2394            let p = match &args[0] { Value::Float(f) => *f, Value::Int(i) => *i as f64, _ => return Err("chi2_ppf: p must be a number".into()) };
2395            let df = match &args[1] { Value::Float(f) => *f, Value::Int(i) => *i as f64, _ => return Err("chi2_ppf: df must be a number".into()) };
2396            Ok(Some(Value::Float(crate::distributions::chi2_ppf(p, df)?)))
2397        }
2398        "f_ppf" => {
2399            if args.len() != 3 { return Err("f_ppf requires 3 arguments (p, df1, df2)".into()); }
2400            let p = match &args[0] { Value::Float(f) => *f, Value::Int(i) => *i as f64, _ => return Err("f_ppf: p must be a number".into()) };
2401            let df1 = match &args[1] { Value::Float(f) => *f, Value::Int(i) => *i as f64, _ => return Err("f_ppf: df1 must be a number".into()) };
2402            let df2 = match &args[2] { Value::Float(f) => *f, Value::Int(i) => *i as f64, _ => return Err("f_ppf: df2 must be a number".into()) };
2403            Ok(Some(Value::Float(crate::distributions::f_ppf(p, df1, df2)?)))
2404        }
2405        // ── Discrete distribution builtins ──────────────────────────
2406        "binomial_pmf" => {
2407            if args.len() != 3 { return Err("binomial_pmf requires 3 arguments (k, n, p)".into()); }
2408            let k = value_to_usize(&args[0])? as u64;
2409            let n = value_to_usize(&args[1])? as u64;
2410            let p = match &args[2] { Value::Float(f) => *f, Value::Int(i) => *i as f64, _ => return Err("binomial_pmf: p must be a number".into()) };
2411            Ok(Some(Value::Float(crate::distributions::binomial_pmf(k, n, p))))
2412        }
2413        "binomial_cdf" => {
2414            if args.len() != 3 { return Err("binomial_cdf requires 3 arguments (k, n, p)".into()); }
2415            let k = value_to_usize(&args[0])? as u64;
2416            let n = value_to_usize(&args[1])? as u64;
2417            let p = match &args[2] { Value::Float(f) => *f, Value::Int(i) => *i as f64, _ => return Err("binomial_cdf: p must be a number".into()) };
2418            Ok(Some(Value::Float(crate::distributions::binomial_cdf(k, n, p))))
2419        }
2420        "poisson_pmf" => {
2421            if args.len() != 2 { return Err("poisson_pmf requires 2 arguments (k, lambda)".into()); }
2422            let k = value_to_usize(&args[0])? as u64;
2423            let lambda = match &args[1] { Value::Float(f) => *f, Value::Int(i) => *i as f64, _ => return Err("poisson_pmf: lambda must be a number".into()) };
2424            Ok(Some(Value::Float(crate::distributions::poisson_pmf(k, lambda))))
2425        }
2426        "poisson_cdf" => {
2427            if args.len() != 2 { return Err("poisson_cdf requires 2 arguments (k, lambda)".into()); }
2428            let k = value_to_usize(&args[0])? as u64;
2429            let lambda = match &args[1] { Value::Float(f) => *f, Value::Int(i) => *i as f64, _ => return Err("poisson_cdf: lambda must be a number".into()) };
2430            Ok(Some(Value::Float(crate::distributions::poisson_cdf(k, lambda))))
2431        }
2432        // ── Hypothesis test builtins (additional) ───────────────────
2433        "t_test_paired" => {
2434            if args.len() != 2 { return Err("t_test_paired requires 2 arguments".into()); }
2435            let x = value_to_f64_vec(&args[0])?;
2436            let y = value_to_f64_vec(&args[1])?;
2437            let r = crate::hypothesis::t_test_paired(&x, &y)?;
2438            let mut fields = std::collections::BTreeMap::new();
2439            fields.insert("t_statistic".into(), Value::Float(r.t_statistic));
2440            fields.insert("p_value".into(), Value::Float(r.p_value));
2441            fields.insert("df".into(), Value::Float(r.df));
2442            Ok(Some(Value::Struct { name: "TTestResult".into(), fields }))
2443        }
2444        "anova_oneway" => {
2445            if args.len() < 2 { return Err("anova_oneway requires at least 2 group arguments".into()); }
2446            let mut groups = Vec::new();
2447            let mut group_vecs = Vec::new();
2448            for a in args.iter() {
2449                group_vecs.push(value_to_f64_vec(a)?);
2450            }
2451            for gv in &group_vecs {
2452                groups.push(gv.as_slice());
2453            }
2454            let r = crate::hypothesis::anova_oneway(&groups)?;
2455            let mut fields = std::collections::BTreeMap::new();
2456            fields.insert("f_statistic".into(), Value::Float(r.f_statistic));
2457            fields.insert("p_value".into(), Value::Float(r.p_value));
2458            fields.insert("df_between".into(), Value::Float(r.df_between));
2459            fields.insert("df_within".into(), Value::Float(r.df_within));
2460            fields.insert("ss_between".into(), Value::Float(r.ss_between));
2461            fields.insert("ss_within".into(), Value::Float(r.ss_within));
2462            Ok(Some(Value::Struct { name: "AnovaResult".into(), fields }))
2463        }
2464        "f_test" => {
2465            if args.len() != 2 { return Err("f_test requires 2 arguments".into()); }
2466            let x = value_to_f64_vec(&args[0])?;
2467            let y = value_to_f64_vec(&args[1])?;
2468            let (f_stat, p_val) = crate::hypothesis::f_test(&x, &y)?;
2469            let mut fields = std::collections::BTreeMap::new();
2470            fields.insert("f_statistic".into(), Value::Float(f_stat));
2471            fields.insert("p_value".into(), Value::Float(p_val));
2472            Ok(Some(Value::Struct { name: "FTestResult".into(), fields }))
2473        }
2474        "lm" => {
2475            // lm(X, y, n, p) — linear model
2476            if args.len() != 4 { return Err("lm requires 4 arguments (X_flat, y, n, p)".into()); }
2477            let x_flat = value_to_f64_vec(&args[0])?;
2478            let y = value_to_f64_vec(&args[1])?;
2479            let n = value_to_usize(&args[2])?;
2480            let p = value_to_usize(&args[3])?;
2481            let r = crate::hypothesis::lm(&x_flat, &y, n, p)?;
2482            let coef_values: Vec<Value> = r.coefficients.into_iter().map(Value::Float).collect();
2483            let se_values: Vec<Value> = r.std_errors.into_iter().map(Value::Float).collect();
2484            let t_values: Vec<Value> = r.t_values.into_iter().map(Value::Float).collect();
2485            let p_values: Vec<Value> = r.p_values.into_iter().map(Value::Float).collect();
2486            let resid_values: Vec<Value> = r.residuals.into_iter().map(Value::Float).collect();
2487            let mut fields = std::collections::BTreeMap::new();
2488            fields.insert("coefficients".into(), Value::Array(Rc::new(coef_values)));
2489            fields.insert("std_errors".into(), Value::Array(Rc::new(se_values)));
2490            fields.insert("t_values".into(), Value::Array(Rc::new(t_values)));
2491            fields.insert("p_values".into(), Value::Array(Rc::new(p_values)));
2492            fields.insert("r_squared".into(), Value::Float(r.r_squared));
2493            fields.insert("adj_r_squared".into(), Value::Float(r.adj_r_squared));
2494            fields.insert("residuals".into(), Value::Array(Rc::new(resid_values)));
2495            fields.insert("f_statistic".into(), Value::Float(r.f_statistic));
2496            Ok(Some(Value::Struct { name: "LmResult".into(), fields }))
2497        }
2498        // ── ML builtins (additional) ────────────────────────────────
2499        "binary_cross_entropy" => {
2500            if args.len() != 2 { return Err("binary_cross_entropy requires 2 arguments".into()); }
2501            let pred = value_to_f64_vec(&args[0])?;
2502            let target = value_to_f64_vec(&args[1])?;
2503            Ok(Some(Value::Float(crate::ml::binary_cross_entropy(&pred, &target)?)))
2504        }
2505        "hinge_loss" => {
2506            if args.len() != 2 { return Err("hinge_loss requires 2 arguments".into()); }
2507            let pred = value_to_f64_vec(&args[0])?;
2508            let target = value_to_f64_vec(&args[1])?;
2509            Ok(Some(Value::Float(crate::ml::hinge_loss(&pred, &target)?)))
2510        }
2511        "confusion_matrix" => {
2512            if args.len() != 2 { return Err("confusion_matrix requires 2 arguments".into()); }
2513            let pred = value_to_f64_vec(&args[0])?;
2514            let actual = value_to_f64_vec(&args[1])?;
2515            let pred_bool: Vec<bool> = pred.iter().map(|&x| x > 0.5).collect();
2516            let actual_bool: Vec<bool> = actual.iter().map(|&x| x > 0.5).collect();
2517            let cm = crate::ml::confusion_matrix(&pred_bool, &actual_bool);
2518            let mut fields = std::collections::BTreeMap::new();
2519            fields.insert("tp".into(), Value::Int(cm.tp as i64));
2520            fields.insert("fp".into(), Value::Int(cm.fp as i64));
2521            fields.insert("tn".into(), Value::Int(cm.tn as i64));
2522            fields.insert("fn_count".into(), Value::Int(cm.fn_count as i64));
2523            fields.insert("precision".into(), Value::Float(crate::ml::precision(&cm)));
2524            fields.insert("recall".into(), Value::Float(crate::ml::recall(&cm)));
2525            fields.insert("f1_score".into(), Value::Float(crate::ml::f1_score(&cm)));
2526            fields.insert("accuracy".into(), Value::Float(crate::ml::accuracy(&cm)));
2527            Ok(Some(Value::Struct { name: "ConfusionMatrix".into(), fields }))
2528        }
2529        "auc_roc" => {
2530            if args.len() != 2 { return Err("auc_roc requires 2 arguments (scores, labels)".into()); }
2531            let scores = value_to_f64_vec(&args[0])?;
2532            let labels_f = value_to_f64_vec(&args[1])?;
2533            let labels: Vec<bool> = labels_f.iter().map(|&x| x > 0.5).collect();
2534            Ok(Some(Value::Float(crate::ml::auc_roc(&scores, &labels)?)))
2535        }
2536        // ── Embedding layer ─────────────────────────────────────────
2537        "embedding" => {
2538            if args.len() != 2 { return Err("embedding requires 2 arguments (weight, indices)".into()); }
2539            let weight = value_to_tensor(&args[0])?;
2540            let indices: Vec<i64> = match &args[1] {
2541                Value::Array(arr) => arr.iter().map(|v| match v {
2542                    Value::Int(i) => Ok(*i),
2543                    _ => Err("embedding: indices must be integers".to_string()),
2544                }).collect::<Result<Vec<_>, _>>()?,
2545                Value::Tensor(t) => t.to_vec().iter().map(|f| Ok::<i64, String>(*f as i64)).collect::<Result<Vec<_>, _>>()?,
2546                _ => return Err("embedding: indices must be an array or tensor".into()),
2547            };
2548            let result = crate::ml::embedding(weight, &indices)?;
2549            Ok(Some(Value::Tensor(result)))
2550        }
2551        // ── Deterministic batch indices ─────────────────────────────
2552        "batch_indices" => {
2553            if args.len() != 3 { return Err("batch_indices requires 3 arguments (dataset_size, batch_size, seed)".into()); }
2554            let ds = match &args[0] { Value::Int(i) => *i as usize, _ => return Err("batch_indices: dataset_size must be int".into()) };
2555            let bs = match &args[1] { Value::Int(i) => *i as usize, _ => return Err("batch_indices: batch_size must be int".into()) };
2556            let seed = match &args[2] { Value::Int(i) => *i as u64, _ => return Err("batch_indices: seed must be int".into()) };
2557            let result = crate::ml::batch_indices(ds, bs, seed);
2558            let arr: Vec<Value> = result.into_iter().map(|(s, e)| {
2559                Value::Array(Rc::new(vec![Value::Int(s as i64), Value::Int(e as i64)]))
2560            }).collect();
2561            Ok(Some(Value::Array(Rc::new(arr))))
2562        }
2563        // ── Tensor activation builtins ──────────────────────────────
2564        "sigmoid" => {
2565            if args.len() != 1 { return Err("sigmoid requires 1 argument".into()); }
2566            let t = value_to_tensor(&args[0])?;
2567            Ok(Some(Value::Tensor(t.sigmoid())))
2568        }
2569        "tanh_activation" => {
2570            if args.len() != 1 { return Err("tanh_activation requires 1 argument".into()); }
2571            let t = value_to_tensor(&args[0])?;
2572            Ok(Some(Value::Tensor(t.tanh_activation())))
2573        }
2574        "leaky_relu" => {
2575            if args.len() != 2 { return Err("leaky_relu requires 2 arguments (tensor, alpha)".into()); }
2576            let t = value_to_tensor(&args[0])?;
2577            let alpha = match &args[1] { Value::Float(f) => *f, Value::Int(i) => *i as f64, _ => return Err("leaky_relu: alpha must be a number".into()) };
2578            Ok(Some(Value::Tensor(t.leaky_relu(alpha))))
2579        }
2580        "silu" => {
2581            if args.len() != 1 { return Err("silu requires 1 argument".into()); }
2582            let t = value_to_tensor(&args[0])?;
2583            Ok(Some(Value::Tensor(t.silu())))
2584        }
2585        "mish" => {
2586            if args.len() != 1 { return Err("mish requires 1 argument".into()); }
2587            let t = value_to_tensor(&args[0])?;
2588            Ok(Some(Value::Tensor(t.mish())))
2589        }
2590        // ── relu: scalar + tensor unified ──────────────────────────────
2591        "relu" => {
2592            if args.len() != 1 { return Err("relu requires 1 argument".into()); }
2593            match &args[0] {
2594                Value::Float(f) => Ok(Some(Value::Float(f.max(0.0)))),
2595                Value::Int(i) => Ok(Some(Value::Int((*i).max(0)))),
2596                Value::Tensor(t) => Ok(Some(Value::Tensor(t.relu()))),
2597                _ => Err(format!("relu requires a number or Tensor, got {}", args[0].type_name())),
2598            }
2599        }
2600        // ── reshape: reshape a tensor ──────────────────────────────────
2601        "reshape" => {
2602            if args.len() != 2 { return Err("reshape requires 2 arguments (tensor, shape)".into()); }
2603            let t = value_to_tensor(&args[0])?;
2604            let shape = value_to_shape(&args[1])?;
2605            let result = t.reshape(&shape).map_err(|e| format!("{e}"))?;
2606            Ok(Some(Value::Tensor(result)))
2607        }
2608        // ── tensor_slice: slice a tensor along all dims ────────────────
2609        "tensor_slice" => {
2610            if args.len() != 3 { return Err("tensor_slice requires 3 arguments (tensor, starts, ends)".into()); }
2611            let t = value_to_tensor(&args[0])?;
2612            let starts = value_to_usize_vec(&args[1])?;
2613            let ends = value_to_usize_vec(&args[2])?;
2614            if starts.len() != ends.len() {
2615                return Err("tensor_slice: starts and ends must have same length".into());
2616            }
2617            let ranges: Vec<(usize, usize)> = starts.into_iter().zip(ends).collect();
2618            let result = t.slice(&ranges).map_err(|e| format!("{e}"))?;
2619            Ok(Some(Value::Tensor(result)))
2620        }
2621        // ── slice: slice tensor along one dim ──────────────────────────
2622        "slice" => {
2623            if args.len() != 4 { return Err("slice requires 4 arguments (tensor, dim, start, end)".into()); }
2624            let t = value_to_tensor(&args[0])?;
2625            let dim = match &args[1] { Value::Int(i) => *i as usize, _ => return Err("slice: dim must be an integer".into()) };
2626            let start = match &args[2] { Value::Int(i) => *i as usize, _ => return Err("slice: start must be an integer".into()) };
2627            let end = match &args[3] { Value::Int(i) => *i as usize, _ => return Err("slice: end must be an integer".into()) };
2628            if dim >= t.ndim() {
2629                return Err(format!("slice: dim {} out of bounds for tensor with {} dimensions", dim, t.ndim()));
2630            }
2631            let mut ranges: Vec<(usize, usize)> = t.shape().iter().map(|&s| (0, s)).collect();
2632            ranges[dim] = (start, end);
2633            let result = t.slice(&ranges).map_err(|e| format!("{e}"))?;
2634            Ok(Some(Value::Tensor(result)))
2635        }
2636        "argmax" => {
2637            if args.len() != 1 { return Err("argmax requires 1 argument".into()); }
2638            let t = value_to_tensor(&args[0])?;
2639            Ok(Some(Value::Int(t.argmax() as i64)))
2640        }
2641        "argmin" => {
2642            if args.len() != 1 { return Err("argmin requires 1 argument".into()); }
2643            let t = value_to_tensor(&args[0])?;
2644            Ok(Some(Value::Int(t.argmin() as i64)))
2645        }
2646        "clamp" => {
2647            if args.len() != 3 { return Err("clamp requires 3 arguments (tensor, min, max)".into()); }
2648            let t = value_to_tensor(&args[0])?;
2649            let min_v = match &args[1] { Value::Float(f) => *f, Value::Int(i) => *i as f64, _ => return Err("clamp: min must be a number".into()) };
2650            let max_v = match &args[2] { Value::Float(f) => *f, Value::Int(i) => *i as f64, _ => return Err("clamp: max must be a number".into()) };
2651            Ok(Some(Value::Tensor(t.clamp(min_v, max_v))))
2652        }
2653        "one_hot" => {
2654            if args.len() != 2 { return Err("one_hot requires 2 arguments (indices, depth)".into()); }
2655            let indices = value_to_usize_vec(&args[0])?;
2656            let depth = value_to_usize(&args[1])?;
2657            Ok(Some(Value::Tensor(Tensor::one_hot(&indices, depth).map_err(|e| format!("{e}"))?)))
2658        }
2659        // ── FFT builtins ────────────────────────────────────────────
2660        "rfft" => {
2661            if args.len() != 1 { return Err("rfft requires 1 argument".into()); }
2662            let data = value_to_f64_vec(&args[0])?;
2663            let result = crate::fft::rfft(&data);
2664            let pairs: Vec<Value> = result.iter().map(|&(re, im)| {
2665                Value::Tuple(Rc::new(vec![Value::Float(re), Value::Float(im)]))
2666            }).collect();
2667            Ok(Some(Value::Array(Rc::new(pairs))))
2668        }
2669        "psd" => {
2670            if args.len() != 1 { return Err("psd requires 1 argument".into()); }
2671            let data = value_to_f64_vec(&args[0])?;
2672            let result = crate::fft::psd(&data);
2673            let values: Vec<Value> = result.into_iter().map(Value::Float).collect();
2674            Ok(Some(Value::Array(Rc::new(values))))
2675        }
2676
2677        // ── B1: Weighted & robust statistics ──────────────────────────
2678        "weighted_mean" => {
2679            if args.len() != 2 { return Err("weighted_mean requires 2 arguments".into()); }
2680            let data = value_to_f64_vec(&args[0])?;
2681            let weights = value_to_f64_vec(&args[1])?;
2682            Ok(Some(Value::Float(crate::stats::weighted_mean(&data, &weights)?)))
2683        }
2684        "weighted_var" => {
2685            if args.len() != 2 { return Err("weighted_var requires 2 arguments".into()); }
2686            let data = value_to_f64_vec(&args[0])?;
2687            let weights = value_to_f64_vec(&args[1])?;
2688            Ok(Some(Value::Float(crate::stats::weighted_var(&data, &weights)?)))
2689        }
2690        "trimmed_mean" => {
2691            if args.len() != 2 { return Err("trimmed_mean requires 2 arguments".into()); }
2692            let data = value_to_f64_vec(&args[0])?;
2693            let prop = match &args[1] { Value::Float(f) => *f, Value::Int(i) => *i as f64, _ => return Err("trimmed_mean: proportion must be a number".into()) };
2694            Ok(Some(Value::Float(crate::stats::trimmed_mean(&data, prop)?)))
2695        }
2696        "winsorize" => {
2697            if args.len() != 2 { return Err("winsorize requires 2 arguments".into()); }
2698            let data = value_to_f64_vec(&args[0])?;
2699            let prop = match &args[1] { Value::Float(f) => *f, Value::Int(i) => *i as f64, _ => return Err("winsorize: proportion must be a number".into()) };
2700            let result = crate::stats::winsorize(&data, prop)?;
2701            let values: Vec<Value> = result.into_iter().map(Value::Float).collect();
2702            Ok(Some(Value::Array(Rc::new(values))))
2703        }
2704        "mad" => {
2705            if args.len() != 1 { return Err("mad requires 1 argument".into()); }
2706            let data = value_to_f64_vec(&args[0])?;
2707            Ok(Some(Value::Float(crate::stats::mad(&data)?)))
2708        }
2709        "mode" => {
2710            if args.len() != 1 { return Err("mode requires 1 argument".into()); }
2711            let data = value_to_f64_vec(&args[0])?;
2712            Ok(Some(Value::Float(crate::stats::mode(&data)?)))
2713        }
2714        "percentile_rank" => {
2715            if args.len() != 2 { return Err("percentile_rank requires 2 arguments".into()); }
2716            let data = value_to_f64_vec(&args[0])?;
2717            let value = match &args[1] { Value::Float(f) => *f, Value::Int(i) => *i as f64, _ => return Err("percentile_rank: value must be a number".into()) };
2718            Ok(Some(Value::Float(crate::stats::percentile_rank(&data, value)?)))
2719        }
2720
2721        // ── B4: ML training extensions ─────────────────────────────────
2722        "cat" => {
2723            if args.len() != 2 { return Err("cat requires 2 arguments (array of tensors, axis)".into()); }
2724            let tensors_arr = match &args[0] {
2725                Value::Array(arr) => arr.iter().map(|v| match v {
2726                    Value::Tensor(t) => Ok(t),
2727                    _ => Err("cat: first argument must be array of tensors".to_string()),
2728                }).collect::<Result<Vec<&Tensor>, String>>()?,
2729                _ => return Err("cat: first argument must be array of tensors".into()),
2730            };
2731            let axis = value_to_usize(&args[1])?;
2732            let refs: Vec<&Tensor> = tensors_arr;
2733            Ok(Some(Value::Tensor(Tensor::cat(&refs, axis).map_err(|e| format!("{e}"))?)))
2734        }
2735        "stack" => {
2736            if args.len() != 2 { return Err("stack requires 2 arguments (array of tensors, axis)".into()); }
2737            let tensors_arr = match &args[0] {
2738                Value::Array(arr) => arr.iter().map(|v| match v {
2739                    Value::Tensor(t) => Ok(t),
2740                    _ => Err("stack: first argument must be array of tensors".to_string()),
2741                }).collect::<Result<Vec<&Tensor>, String>>()?,
2742                _ => return Err("stack: first argument must be array of tensors".into()),
2743            };
2744            let axis = value_to_usize(&args[1])?;
2745            Ok(Some(Value::Tensor(Tensor::stack(&tensors_arr, axis).map_err(|e| format!("{e}"))?)))
2746        }
2747        "topk" => {
2748            if args.len() != 2 { return Err("topk requires 2 arguments (tensor, k)".into()); }
2749            let t = value_to_tensor(&args[0])?;
2750            let k = value_to_usize(&args[1])?;
2751            let (vals, idxs) = t.topk(k).map_err(|e| format!("{e}"))?;
2752            let idx_values: Vec<Value> = idxs.into_iter().map(|i| Value::Int(i as i64)).collect();
2753            Ok(Some(Value::Tuple(Rc::new(vec![Value::Tensor(vals), Value::Array(Rc::new(idx_values))]))))
2754        }
2755        "batch_norm" => {
2756            if args.len() != 6 { return Err("batch_norm requires 6 arguments".into()); }
2757            let x = value_to_f64_vec(&args[0])?;
2758            let mean = value_to_f64_vec(&args[1])?;
2759            let var = value_to_f64_vec(&args[2])?;
2760            let gamma = value_to_f64_vec(&args[3])?;
2761            let beta = value_to_f64_vec(&args[4])?;
2762            let eps = match &args[5] { Value::Float(f) => *f, Value::Int(i) => *i as f64, _ => return Err("batch_norm: eps must be a number".into()) };
2763            let result = crate::ml::batch_norm(&x, &mean, &var, &gamma, &beta, eps)?;
2764            let values: Vec<Value> = result.into_iter().map(Value::Float).collect();
2765            Ok(Some(Value::Array(Rc::new(values))))
2766        }
2767        "dropout_mask" => {
2768            if args.len() != 3 { return Err("dropout_mask requires 3 arguments (n, prob, seed)".into()); }
2769            let n = value_to_usize(&args[0])?;
2770            let prob = match &args[1] { Value::Float(f) => *f, Value::Int(i) => *i as f64, _ => return Err("dropout_mask: prob must be a number".into()) };
2771            let seed = match &args[2] { Value::Int(i) => *i as u64, _ => return Err("dropout_mask: seed must be an integer".into()) };
2772            let mask = crate::ml::dropout_mask(n, prob, seed);
2773            let values: Vec<Value> = mask.into_iter().map(Value::Float).collect();
2774            Ok(Some(Value::Array(Rc::new(values))))
2775        }
2776        "lr_step_decay" => {
2777            if args.len() != 4 { return Err("lr_step_decay requires 4 arguments".into()); }
2778            let lr = match &args[0] { Value::Float(f) => *f, Value::Int(i) => *i as f64, _ => return Err("lr_step_decay: lr must be a number".into()) };
2779            let rate = match &args[1] { Value::Float(f) => *f, Value::Int(i) => *i as f64, _ => return Err("lr_step_decay: rate must be a number".into()) };
2780            let epoch = value_to_usize(&args[2])?;
2781            let step = value_to_usize(&args[3])?;
2782            Ok(Some(Value::Float(crate::ml::lr_step_decay(lr, rate, epoch, step))))
2783        }
2784        "lr_cosine" => {
2785            if args.len() != 4 { return Err("lr_cosine requires 4 arguments".into()); }
2786            let max_lr = match &args[0] { Value::Float(f) => *f, Value::Int(i) => *i as f64, _ => return Err("lr_cosine: max_lr must be a number".into()) };
2787            let min_lr = match &args[1] { Value::Float(f) => *f, Value::Int(i) => *i as f64, _ => return Err("lr_cosine: min_lr must be a number".into()) };
2788            let epoch = value_to_usize(&args[2])?;
2789            let total = value_to_usize(&args[3])?;
2790            Ok(Some(Value::Float(crate::ml::lr_cosine(max_lr, min_lr, epoch, total))))
2791        }
2792        "lr_linear_warmup" => {
2793            if args.len() != 3 { return Err("lr_linear_warmup requires 3 arguments".into()); }
2794            let lr = match &args[0] { Value::Float(f) => *f, Value::Int(i) => *i as f64, _ => return Err("lr_linear_warmup: lr must be a number".into()) };
2795            let epoch = value_to_usize(&args[1])?;
2796            let warmup = value_to_usize(&args[2])?;
2797            Ok(Some(Value::Float(crate::ml::lr_linear_warmup(lr, epoch, warmup))))
2798        }
2799        "l1_penalty" => {
2800            if args.len() != 2 { return Err("l1_penalty requires 2 arguments".into()); }
2801            let params = value_to_f64_vec(&args[0])?;
2802            let lambda = match &args[1] { Value::Float(f) => *f, Value::Int(i) => *i as f64, _ => return Err("l1_penalty: lambda must be a number".into()) };
2803            Ok(Some(Value::Float(crate::ml::l1_penalty(&params, lambda))))
2804        }
2805        "l2_penalty" => {
2806            if args.len() != 2 { return Err("l2_penalty requires 2 arguments".into()); }
2807            let params = value_to_f64_vec(&args[0])?;
2808            let lambda = match &args[1] { Value::Float(f) => *f, Value::Int(i) => *i as f64, _ => return Err("l2_penalty: lambda must be a number".into()) };
2809            Ok(Some(Value::Float(crate::ml::l2_penalty(&params, lambda))))
2810        }
2811
2812        // ── B3: Linear algebra extensions ─────────────────────────────
2813        "cond" => {
2814            if args.len() != 1 { return Err("cond requires 1 Tensor argument".into()); }
2815            let t = value_to_tensor(&args[0])?;
2816            Ok(Some(Value::Float(t.cond().map_err(|e| format!("{e}"))?)))
2817        }
2818        "norm_1" => {
2819            if args.len() != 1 { return Err("norm_1 requires 1 Tensor argument".into()); }
2820            let t = value_to_tensor(&args[0])?;
2821            Ok(Some(Value::Float(t.norm_1().map_err(|e| format!("{e}"))?)))
2822        }
2823        "norm_inf" => {
2824            if args.len() != 1 { return Err("norm_inf requires 1 Tensor argument".into()); }
2825            let t = value_to_tensor(&args[0])?;
2826            Ok(Some(Value::Float(t.norm_inf().map_err(|e| format!("{e}"))?)))
2827        }
2828        "schur" => {
2829            if args.len() != 1 { return Err("schur requires 1 Tensor argument".into()); }
2830            let t = value_to_tensor(&args[0])?;
2831            let (q, t_mat) = t.schur().map_err(|e| format!("{e}"))?;
2832            Ok(Some(Value::Tuple(Rc::new(vec![Value::Tensor(q), Value::Tensor(t_mat)]))))
2833        }
2834        "matrix_exp" => {
2835            if args.len() != 1 { return Err("matrix_exp requires 1 Tensor argument".into()); }
2836            let t = value_to_tensor(&args[0])?;
2837            Ok(Some(Value::Tensor(t.matrix_exp().map_err(|e| format!("{e}"))?)))
2838        }
2839
2840        // ── B2: Rank correlations & partial correlation ────────────────
2841        "spearman_cor" => {
2842            if args.len() != 2 { return Err("spearman_cor requires 2 arguments".into()); }
2843            let x = value_to_f64_vec(&args[0])?;
2844            let y = value_to_f64_vec(&args[1])?;
2845            Ok(Some(Value::Float(crate::stats::spearman_cor(&x, &y)?)))
2846        }
2847        "kendall_cor" => {
2848            if args.len() != 2 { return Err("kendall_cor requires 2 arguments".into()); }
2849            let x = value_to_f64_vec(&args[0])?;
2850            let y = value_to_f64_vec(&args[1])?;
2851            Ok(Some(Value::Float(crate::stats::kendall_cor(&x, &y)?)))
2852        }
2853        "partial_cor" => {
2854            if args.len() != 3 { return Err("partial_cor requires 3 arguments".into()); }
2855            let x = value_to_f64_vec(&args[0])?;
2856            let y = value_to_f64_vec(&args[1])?;
2857            let z = value_to_f64_vec(&args[2])?;
2858            Ok(Some(Value::Float(crate::stats::partial_cor(&x, &y, &z)?)))
2859        }
2860        "cor_ci" => {
2861            if args.len() != 3 { return Err("cor_ci requires 3 arguments".into()); }
2862            let x = value_to_f64_vec(&args[0])?;
2863            let y = value_to_f64_vec(&args[1])?;
2864            let alpha = match &args[2] { Value::Float(f) => *f, Value::Int(i) => *i as f64, _ => return Err("cor_ci: alpha must be a number".into()) };
2865            let (lo, hi) = crate::stats::cor_ci(&x, &y, alpha)?;
2866            Ok(Some(Value::Tuple(Rc::new(vec![Value::Float(lo), Value::Float(hi)]))))
2867        }
2868
2869        // ── B6: Advanced FFT & Distributions ─────────────────────────
2870        "hann" => {
2871            if args.len() != 1 { return Err("hann requires 1 argument (n)".into()); }
2872            let n = match &args[0] { Value::Int(i) => *i as usize, _ => return Err("hann: n must be an integer".into()) };
2873            let w = crate::fft::hann_window(n);
2874            Ok(Some(Value::Array(Rc::new(w.into_iter().map(Value::Float).collect()))))
2875        }
2876        "hamming" => {
2877            if args.len() != 1 { return Err("hamming requires 1 argument (n)".into()); }
2878            let n = match &args[0] { Value::Int(i) => *i as usize, _ => return Err("hamming: n must be an integer".into()) };
2879            let w = crate::fft::hamming_window(n);
2880            Ok(Some(Value::Array(Rc::new(w.into_iter().map(Value::Float).collect()))))
2881        }
2882        "blackman" => {
2883            if args.len() != 1 { return Err("blackman requires 1 argument (n)".into()); }
2884            let n = match &args[0] { Value::Int(i) => *i as usize, _ => return Err("blackman: n must be an integer".into()) };
2885            let w = crate::fft::blackman_window(n);
2886            Ok(Some(Value::Array(Rc::new(w.into_iter().map(Value::Float).collect()))))
2887        }
2888        "fft_arbitrary" => {
2889            if args.len() != 1 { return Err("fft_arbitrary requires 1 argument (complex array)".into()); }
2890            let data = value_to_complex_vec(&args[0])?;
2891            let result = crate::fft::fft_arbitrary(&data);
2892            let pairs: Vec<Value> = result.iter().map(|&(re, im)| {
2893                Value::Tuple(Rc::new(vec![Value::Float(re), Value::Float(im)]))
2894            }).collect();
2895            Ok(Some(Value::Array(Rc::new(pairs))))
2896        }
2897        "fft_2d" => {
2898            if args.len() != 3 { return Err("fft_2d requires 3 arguments (data, rows, cols)".into()); }
2899            let data = value_to_complex_vec(&args[0])?;
2900            let rows = match &args[1] { Value::Int(i) => *i as usize, _ => return Err("fft_2d: rows must be an integer".into()) };
2901            let cols = match &args[2] { Value::Int(i) => *i as usize, _ => return Err("fft_2d: cols must be an integer".into()) };
2902            let result = crate::fft::fft_2d(&data, rows, cols)?;
2903            let pairs: Vec<Value> = result.iter().map(|&(re, im)| {
2904                Value::Tuple(Rc::new(vec![Value::Float(re), Value::Float(im)]))
2905            }).collect();
2906            Ok(Some(Value::Array(Rc::new(pairs))))
2907        }
2908        "ifft_2d" => {
2909            if args.len() != 3 { return Err("ifft_2d requires 3 arguments (data, rows, cols)".into()); }
2910            let data = value_to_complex_vec(&args[0])?;
2911            let rows = match &args[1] { Value::Int(i) => *i as usize, _ => return Err("ifft_2d: rows must be an integer".into()) };
2912            let cols = match &args[2] { Value::Int(i) => *i as usize, _ => return Err("ifft_2d: cols must be an integer".into()) };
2913            let result = crate::fft::ifft_2d(&data, rows, cols)?;
2914            let pairs: Vec<Value> = result.iter().map(|&(re, im)| {
2915                Value::Tuple(Rc::new(vec![Value::Float(re), Value::Float(im)]))
2916            }).collect();
2917            Ok(Some(Value::Array(Rc::new(pairs))))
2918        }
2919        "beta_pdf" => {
2920            if args.len() != 3 { return Err("beta_pdf requires 3 arguments (x, a, b)".into()); }
2921            let x = match &args[0] { Value::Float(f) => *f, Value::Int(i) => *i as f64, _ => return Err("beta_pdf: x must be a number".into()) };
2922            let a = match &args[1] { Value::Float(f) => *f, Value::Int(i) => *i as f64, _ => return Err("beta_pdf: a must be a number".into()) };
2923            let b = match &args[2] { Value::Float(f) => *f, Value::Int(i) => *i as f64, _ => return Err("beta_pdf: b must be a number".into()) };
2924            Ok(Some(Value::Float(crate::distributions::beta_pdf(x, a, b))))
2925        }
2926        "beta_cdf" => {
2927            if args.len() != 3 { return Err("beta_cdf requires 3 arguments (x, a, b)".into()); }
2928            let x = match &args[0] { Value::Float(f) => *f, Value::Int(i) => *i as f64, _ => return Err("beta_cdf: x must be a number".into()) };
2929            let a = match &args[1] { Value::Float(f) => *f, Value::Int(i) => *i as f64, _ => return Err("beta_cdf: a must be a number".into()) };
2930            let b = match &args[2] { Value::Float(f) => *f, Value::Int(i) => *i as f64, _ => return Err("beta_cdf: b must be a number".into()) };
2931            Ok(Some(Value::Float(crate::distributions::beta_cdf(x, a, b))))
2932        }
2933        "gamma_pdf" => {
2934            if args.len() != 3 { return Err("gamma_pdf requires 3 arguments (x, k, theta)".into()); }
2935            let x = match &args[0] { Value::Float(f) => *f, Value::Int(i) => *i as f64, _ => return Err("gamma_pdf: x must be a number".into()) };
2936            let k = match &args[1] { Value::Float(f) => *f, Value::Int(i) => *i as f64, _ => return Err("gamma_pdf: k must be a number".into()) };
2937            let theta = match &args[2] { Value::Float(f) => *f, Value::Int(i) => *i as f64, _ => return Err("gamma_pdf: theta must be a number".into()) };
2938            Ok(Some(Value::Float(crate::distributions::gamma_pdf(x, k, theta))))
2939        }
2940        "gamma_cdf" => {
2941            if args.len() != 3 { return Err("gamma_cdf requires 3 arguments (x, k, theta)".into()); }
2942            let x = match &args[0] { Value::Float(f) => *f, Value::Int(i) => *i as f64, _ => return Err("gamma_cdf: x must be a number".into()) };
2943            let k = match &args[1] { Value::Float(f) => *f, Value::Int(i) => *i as f64, _ => return Err("gamma_cdf: k must be a number".into()) };
2944            let theta = match &args[2] { Value::Float(f) => *f, Value::Int(i) => *i as f64, _ => return Err("gamma_cdf: theta must be a number".into()) };
2945            Ok(Some(Value::Float(crate::distributions::gamma_cdf(x, k, theta))))
2946        }
2947        "exp_pdf" => {
2948            if args.len() != 2 { return Err("exp_pdf requires 2 arguments (x, lambda)".into()); }
2949            let x = match &args[0] { Value::Float(f) => *f, Value::Int(i) => *i as f64, _ => return Err("exp_pdf: x must be a number".into()) };
2950            let lambda = match &args[1] { Value::Float(f) => *f, Value::Int(i) => *i as f64, _ => return Err("exp_pdf: lambda must be a number".into()) };
2951            Ok(Some(Value::Float(crate::distributions::exp_pdf(x, lambda))))
2952        }
2953        "exp_cdf" => {
2954            if args.len() != 2 { return Err("exp_cdf requires 2 arguments (x, lambda)".into()); }
2955            let x = match &args[0] { Value::Float(f) => *f, Value::Int(i) => *i as f64, _ => return Err("exp_cdf: x must be a number".into()) };
2956            let lambda = match &args[1] { Value::Float(f) => *f, Value::Int(i) => *i as f64, _ => return Err("exp_cdf: lambda must be a number".into()) };
2957            Ok(Some(Value::Float(crate::distributions::exp_cdf(x, lambda))))
2958        }
2959        "weibull_pdf" => {
2960            if args.len() != 3 { return Err("weibull_pdf requires 3 arguments (x, k, lambda)".into()); }
2961            let x = match &args[0] { Value::Float(f) => *f, Value::Int(i) => *i as f64, _ => return Err("weibull_pdf: x must be a number".into()) };
2962            let k = match &args[1] { Value::Float(f) => *f, Value::Int(i) => *i as f64, _ => return Err("weibull_pdf: k must be a number".into()) };
2963            let lambda = match &args[2] { Value::Float(f) => *f, Value::Int(i) => *i as f64, _ => return Err("weibull_pdf: lambda must be a number".into()) };
2964            Ok(Some(Value::Float(crate::distributions::weibull_pdf(x, k, lambda))))
2965        }
2966        "weibull_cdf" => {
2967            if args.len() != 3 { return Err("weibull_cdf requires 3 arguments (x, k, lambda)".into()); }
2968            let x = match &args[0] { Value::Float(f) => *f, Value::Int(i) => *i as f64, _ => return Err("weibull_cdf: x must be a number".into()) };
2969            let k = match &args[1] { Value::Float(f) => *f, Value::Int(i) => *i as f64, _ => return Err("weibull_cdf: k must be a number".into()) };
2970            let lambda = match &args[2] { Value::Float(f) => *f, Value::Int(i) => *i as f64, _ => return Err("weibull_cdf: lambda must be a number".into()) };
2971            Ok(Some(Value::Float(crate::distributions::weibull_cdf(x, k, lambda))))
2972        }
2973
2974        // ── B5: Analyst QoL extensions ─────────────────────────────
2975        "case_when" => {
2976            if args.len() != 3 { return Err("case_when requires 3 arguments (conditions, values, default)".into()); }
2977            let conditions = match &args[0] {
2978                Value::Array(arr) => arr.iter().map(|v| match v {
2979                    Value::Bool(b) => Ok(*b),
2980                    _ => Err("case_when conditions must be booleans".into()),
2981                }).collect::<Result<Vec<bool>, String>>()?,
2982                _ => return Err("case_when conditions must be an array".into()),
2983            };
2984            let values = match &args[1] {
2985                Value::Array(arr) => arr.as_ref().clone(),
2986                _ => return Err("case_when values must be an array".into()),
2987            };
2988            if conditions.len() != values.len() {
2989                return Err("case_when conditions and values must have same length".into());
2990            }
2991            for (i, &cond) in conditions.iter().enumerate() {
2992                if cond { return Ok(Some(values[i].clone())); }
2993            }
2994            Ok(Some(args[2].clone())) // default
2995        }
2996        "ntile" => {
2997            if args.len() != 2 { return Err("ntile requires 2 arguments (data, n)".into()); }
2998            let data = value_to_f64_vec(&args[0])?;
2999            let n = match &args[1] { Value::Int(i) => *i as usize, _ => return Err("ntile: n must be an integer".into()) };
3000            let result = crate::stats::ntile(&data, n)?;
3001            Ok(Some(Value::Array(Rc::new(result.into_iter().map(Value::Float).collect()))))
3002        }
3003        "percent_rank" => {
3004            if args.len() != 1 { return Err("percent_rank requires 1 argument".into()); }
3005            let data = value_to_f64_vec(&args[0])?;
3006            let result = crate::stats::percent_rank_fn(&data)?;
3007            Ok(Some(Value::Array(Rc::new(result.into_iter().map(Value::Float).collect()))))
3008        }
3009        "cume_dist" => {
3010            if args.len() != 1 { return Err("cume_dist requires 1 argument".into()); }
3011            let data = value_to_f64_vec(&args[0])?;
3012            let result = crate::stats::cume_dist(&data)?;
3013            Ok(Some(Value::Array(Rc::new(result.into_iter().map(Value::Float).collect()))))
3014        }
3015        "wls" => {
3016            if args.len() != 5 { return Err("wls requires 5 arguments (X, y, weights, n, p)".into()); }
3017            let x = value_to_f64_vec(&args[0])?;
3018            let y = value_to_f64_vec(&args[1])?;
3019            let w = value_to_f64_vec(&args[2])?;
3020            let n = match &args[3] { Value::Int(i) => *i as usize, _ => return Err("wls: n must be an integer".into()) };
3021            let p = match &args[4] { Value::Int(i) => *i as usize, _ => return Err("wls: p must be an integer".into()) };
3022            let r = crate::hypothesis::wls(&x, &y, &w, n, p)?;
3023            let fields = std::collections::BTreeMap::from([
3024                ("coefficients".to_string(), Value::Array(Rc::new(r.coefficients.into_iter().map(Value::Float).collect()))),
3025                ("r_squared".to_string(), Value::Float(r.r_squared)),
3026                ("residuals".to_string(), Value::Array(Rc::new(r.residuals.into_iter().map(Value::Float).collect()))),
3027            ]);
3028            Ok(Some(Value::Struct { name: "LmResult".to_string(), fields }))
3029        }
3030
3031        // ── B7: Non-parametric tests & multiple comparisons ────────
3032        "tukey_hsd" => {
3033            let groups: Vec<Vec<f64>> = args.iter()
3034                .map(|a| value_to_f64_vec(a))
3035                .collect::<Result<Vec<_>, _>>()?;
3036            let group_refs: Vec<&[f64]> = groups.iter().map(|g| g.as_slice()).collect();
3037            let results = crate::hypothesis::tukey_hsd(&group_refs)?;
3038            let result_values: Vec<Value> = results.iter().map(|pair| {
3039                let mut fields = std::collections::BTreeMap::new();
3040                fields.insert("group_i".into(), Value::Int(pair.group_i as i64));
3041                fields.insert("group_j".into(), Value::Int(pair.group_j as i64));
3042                fields.insert("mean_diff".into(), Value::Float(pair.mean_diff));
3043                fields.insert("q_statistic".into(), Value::Float(pair.q_statistic));
3044                fields.insert("p_value".into(), Value::Float(pair.p_value));
3045                Value::Struct { name: "TukeyHsdPair".into(), fields }
3046            }).collect();
3047            Ok(Some(Value::Array(Rc::new(result_values))))
3048        }
3049        "mann_whitney" => {
3050            if args.len() != 2 { return Err("mann_whitney requires 2 arguments".into()); }
3051            let x = value_to_f64_vec(&args[0])?;
3052            let y = value_to_f64_vec(&args[1])?;
3053            let r = crate::hypothesis::mann_whitney(&x, &y)?;
3054            let mut fields = std::collections::BTreeMap::new();
3055            fields.insert("u_statistic".into(), Value::Float(r.u_statistic));
3056            fields.insert("z_score".into(), Value::Float(r.z_score));
3057            fields.insert("p_value".into(), Value::Float(r.p_value));
3058            Ok(Some(Value::Struct { name: "MannWhitneyResult".into(), fields }))
3059        }
3060        "kruskal_wallis" => {
3061            let groups: Vec<Vec<f64>> = args.iter()
3062                .map(|a| value_to_f64_vec(a))
3063                .collect::<Result<Vec<_>, _>>()?;
3064            let group_refs: Vec<&[f64]> = groups.iter().map(|g| g.as_slice()).collect();
3065            let r = crate::hypothesis::kruskal_wallis(&group_refs)?;
3066            let mut fields = std::collections::BTreeMap::new();
3067            fields.insert("h_statistic".into(), Value::Float(r.h_statistic));
3068            fields.insert("p_value".into(), Value::Float(r.p_value));
3069            fields.insert("df".into(), Value::Float(r.df));
3070            Ok(Some(Value::Struct { name: "KruskalWallisResult".into(), fields }))
3071        }
3072        "wilcoxon_signed_rank" => {
3073            if args.len() != 2 { return Err("wilcoxon_signed_rank requires 2 arguments".into()); }
3074            let x = value_to_f64_vec(&args[0])?;
3075            let y = value_to_f64_vec(&args[1])?;
3076            let r = crate::hypothesis::wilcoxon_signed_rank(&x, &y)?;
3077            let mut fields = std::collections::BTreeMap::new();
3078            fields.insert("w_statistic".into(), Value::Float(r.w_statistic));
3079            fields.insert("z_score".into(), Value::Float(r.z_score));
3080            fields.insert("p_value".into(), Value::Float(r.p_value));
3081            Ok(Some(Value::Struct { name: "WilcoxonResult".into(), fields }))
3082        }
3083        "bonferroni" => {
3084            if args.len() != 1 { return Err("bonferroni requires 1 argument (p_values array)".into()); }
3085            let pvals = value_to_f64_vec(&args[0])?;
3086            let adj = crate::hypothesis::bonferroni(&pvals);
3087            Ok(Some(Value::Array(Rc::new(adj.into_iter().map(Value::Float).collect()))))
3088        }
3089        "fdr_bh" => {
3090            if args.len() != 1 { return Err("fdr_bh requires 1 argument (p_values array)".into()); }
3091            let pvals = value_to_f64_vec(&args[0])?;
3092            let adj = crate::hypothesis::fdr_bh(&pvals);
3093            Ok(Some(Value::Array(Rc::new(adj.into_iter().map(Value::Float).collect()))))
3094        }
3095        "logistic_regression" => {
3096            if args.len() != 4 { return Err("logistic_regression requires 4 arguments (X, y, n, p)".into()); }
3097            let x = value_to_f64_vec(&args[0])?;
3098            let y = value_to_f64_vec(&args[1])?;
3099            let n = match &args[2] { Value::Int(i) => *i as usize, _ => return Err("logistic_regression: n must be an integer".into()) };
3100            let p = match &args[3] { Value::Int(i) => *i as usize, _ => return Err("logistic_regression: p must be an integer".into()) };
3101            let r = crate::hypothesis::logistic_regression(&x, &y, n, p)?;
3102            let mut fields = std::collections::BTreeMap::new();
3103            fields.insert("coefficients".into(), Value::Array(Rc::new(r.coefficients.into_iter().map(Value::Float).collect())));
3104            fields.insert("std_errors".into(), Value::Array(Rc::new(r.std_errors.into_iter().map(Value::Float).collect())));
3105            fields.insert("z_values".into(), Value::Array(Rc::new(r.z_values.into_iter().map(Value::Float).collect())));
3106            fields.insert("p_values".into(), Value::Array(Rc::new(r.p_values.into_iter().map(Value::Float).collect())));
3107            fields.insert("log_likelihood".into(), Value::Float(r.log_likelihood));
3108            fields.insert("aic".into(), Value::Float(r.aic));
3109            fields.insert("iterations".into(), Value::Int(r.iterations as i64));
3110            Ok(Some(Value::Struct { name: "LogisticResult".into(), fields }))
3111        }
3112
3113        // ── Stationarity tests ────────────────────────────────────────
3114        "adf_test" => {
3115            if args.len() != 1 { return Err("adf_test requires 1 argument: data".into()); }
3116            let data = value_to_f64_vec(&args[0])?;
3117            let (t_stat, p_val) = crate::stationarity::adf_test(&data)?;
3118            let mut fields = std::collections::BTreeMap::new();
3119            fields.insert("statistic".into(), Value::Float(t_stat));
3120            fields.insert("p_value".into(), Value::Float(p_val));
3121            Ok(Some(Value::Struct { name: "AdfResult".into(), fields }))
3122        }
3123        "kpss_test" => {
3124            if args.len() != 1 { return Err("kpss_test requires 1 argument: data".into()); }
3125            let data = value_to_f64_vec(&args[0])?;
3126            let (stat, p_val) = crate::stationarity::kpss_test(&data)?;
3127            let mut fields = std::collections::BTreeMap::new();
3128            fields.insert("statistic".into(), Value::Float(stat));
3129            fields.insert("p_value".into(), Value::Float(p_val));
3130            Ok(Some(Value::Struct { name: "KpssResult".into(), fields }))
3131        }
3132        "pp_test" => {
3133            if args.len() != 1 { return Err("pp_test requires 1 argument: data".into()); }
3134            let data = value_to_f64_vec(&args[0])?;
3135            let (z_t, p_val) = crate::stationarity::pp_test(&data)?;
3136            let mut fields = std::collections::BTreeMap::new();
3137            fields.insert("statistic".into(), Value::Float(z_t));
3138            fields.insert("p_value".into(), Value::Float(p_val));
3139            Ok(Some(Value::Struct { name: "PpResult".into(), fields }))
3140        }
3141
3142        // Phase C4: Sorting & Tensor Indexing
3143        "argsort" => {
3144            if args.len() != 1 { return Err("argsort requires 1 arg: Tensor".into()); }
3145            let t = value_to_tensor(&args[0])?;
3146            Ok(Some(Value::Tensor(t.argsort())))
3147        }
3148        "gather" => {
3149            if args.len() != 3 { return Err("gather requires 3 args: tensor, dim, indices".into()); }
3150            let t = value_to_tensor(&args[0])?;
3151            let dim = value_to_usize(&args[1])?;
3152            let indices = value_to_tensor(&args[2])?;
3153            Ok(Some(Value::Tensor(t.gather(dim, &indices).map_err(|e| format!("{e}"))?)))
3154        }
3155        "scatter" => {
3156            if args.len() != 4 { return Err("scatter requires 4 args: tensor, dim, indices, src".into()); }
3157            let t = value_to_tensor(&args[0])?;
3158            let dim = value_to_usize(&args[1])?;
3159            let indices = value_to_tensor(&args[2])?;
3160            let src = value_to_tensor(&args[3])?;
3161            Ok(Some(Value::Tensor(t.scatter(dim, &indices, &src).map_err(|e| format!("{e}"))?)))
3162        }
3163        "index_select" => {
3164            if args.len() != 3 { return Err("index_select requires 3 args: tensor, dim, indices".into()); }
3165            let t = value_to_tensor(&args[0])?;
3166            let dim = value_to_usize(&args[1])?;
3167            let indices = value_to_tensor(&args[2])?;
3168            Ok(Some(Value::Tensor(t.index_select(dim, &indices).map_err(|e| format!("{e}"))?)))
3169        }
3170
3171        // Phase C6: Collection utilities
3172        "array_push" => {
3173            if args.len() != 2 { return Err("array_push requires 2 args: array, value".into()); }
3174            let mut arr_rc = match &args[0] { Value::Array(a) => Rc::clone(a), _ => return Err("array_push: first arg must be Array".into()) };
3175            // COW: Rc::make_mut only clones if refcount > 1.
3176            // For `arr = array_push(arr, val)` where old binding is overwritten,
3177            // refcount is 1 → zero-copy push (amortized O(1) instead of O(n)).
3178            Rc::make_mut(&mut arr_rc).push(args[1].clone());
3179            Ok(Some(Value::Array(arr_rc)))
3180        }
3181        "array_pop" => {
3182            if args.len() != 1 { return Err("array_pop requires 1 arg: array".into()); }
3183            let mut arr_rc = match &args[0] { Value::Array(a) => Rc::clone(a), _ => return Err("array_pop: expected Array".into()) };
3184            if arr_rc.is_empty() { return Err("array_pop: empty array".into()); }
3185            // COW: Rc::make_mut only clones if refcount > 1.
3186            let last = Rc::make_mut(&mut arr_rc).pop().unwrap();
3187            Ok(Some(Value::Tuple(Rc::new(vec![last, Value::Array(arr_rc)]))))
3188        }
3189        "array_contains" => {
3190            if args.len() != 2 { return Err("array_contains requires 2 args: array, value".into()); }
3191            let arr = match &args[0] { Value::Array(a) => a, _ => return Err("array_contains: first arg must be Array".into()) };
3192            let needle = &args[1];
3193            let found = arr.iter().any(|v| format!("{v}") == format!("{needle}"));
3194            Ok(Some(Value::Bool(found)))
3195        }
3196        "array_reverse" => {
3197            if args.len() != 1 { return Err("array_reverse requires 1 arg: array".into()); }
3198            let mut arr_rc = match &args[0] { Value::Array(a) => Rc::clone(a), _ => return Err("array_reverse: expected Array".into()) };
3199            // COW: Rc::make_mut only clones if refcount > 1.
3200            Rc::make_mut(&mut arr_rc).reverse();
3201            Ok(Some(Value::Array(arr_rc)))
3202        }
3203        "array_flatten" => {
3204            if args.len() != 1 { return Err("array_flatten requires 1 arg: array".into()); }
3205            let arr = match &args[0] { Value::Array(a) => a.clone(), _ => return Err("array_flatten: expected Array".into()) };
3206            let mut result = Vec::new();
3207            fn flatten_recursive(arr: &[Value], result: &mut Vec<Value>) {
3208                for v in arr {
3209                    match v {
3210                        Value::Array(inner) => flatten_recursive(inner, result),
3211                        _ => result.push(v.clone()),
3212                    }
3213                }
3214            }
3215            flatten_recursive(&arr, &mut result);
3216            Ok(Some(Value::Array(Rc::new(result))))
3217        }
3218        "array_len" => {
3219            if args.len() != 1 { return Err("array_len requires 1 arg: array".into()); }
3220            match &args[0] {
3221                Value::Array(a) => Ok(Some(Value::Int(a.len() as i64))),
3222                _ => Err("array_len: expected Array".into()),
3223            }
3224        }
3225        "array_slice" => {
3226            if args.len() != 3 { return Err("array_slice requires 3 args: array, start, end".into()); }
3227            let arr = match &args[0] { Value::Array(a) => a, _ => return Err("array_slice: expected Array".into()) };
3228            let start = match &args[1] { Value::Int(i) => *i as usize, _ => return Err("array_slice: start must be Int".into()) };
3229            let end = match &args[2] { Value::Int(i) => *i as usize, _ => return Err("array_slice: end must be Int".into()) };
3230            if start > end || end > arr.len() {
3231                return Err(format!("array_slice: bounds [{start}, {end}) out of range for len {}", arr.len()));
3232            }
3233            Ok(Some(Value::Array(Rc::new(arr[start..end].to_vec()))))
3234        }
3235
3236        // Phase C5: Map & Set constructors
3237        "Map.new" => {
3238            if !args.is_empty() { return Err("Map.new takes 0 arguments".into()); }
3239            Ok(Some(Value::Map(Rc::new(RefCell::new(crate::det_map::DetMap::new())))))
3240        }
3241        "Set.new" => {
3242            if !args.is_empty() { return Err("Set.new takes 0 arguments".into()); }
3243            Ok(Some(Value::Map(Rc::new(RefCell::new(crate::det_map::DetMap::new())))))
3244        }
3245
3246        // Phase C3: Bitwise operations
3247        "bit_and" => {
3248            if args.len() != 2 { return Err("bit_and requires 2 Int args".into()); }
3249            let a = match &args[0] { Value::Int(i) => *i, _ => return Err("bit_and: expected Int".into()) };
3250            let b = match &args[1] { Value::Int(i) => *i, _ => return Err("bit_and: expected Int".into()) };
3251            Ok(Some(Value::Int(a & b)))
3252        }
3253        "bit_or" => {
3254            if args.len() != 2 { return Err("bit_or requires 2 Int args".into()); }
3255            let a = match &args[0] { Value::Int(i) => *i, _ => return Err("bit_or: expected Int".into()) };
3256            let b = match &args[1] { Value::Int(i) => *i, _ => return Err("bit_or: expected Int".into()) };
3257            Ok(Some(Value::Int(a | b)))
3258        }
3259        "bit_xor" => {
3260            if args.len() != 2 { return Err("bit_xor requires 2 Int args".into()); }
3261            let a = match &args[0] { Value::Int(i) => *i, _ => return Err("bit_xor: expected Int".into()) };
3262            let b = match &args[1] { Value::Int(i) => *i, _ => return Err("bit_xor: expected Int".into()) };
3263            Ok(Some(Value::Int(a ^ b)))
3264        }
3265        "bit_not" => {
3266            if args.len() != 1 { return Err("bit_not requires 1 Int arg".into()); }
3267            let a = match &args[0] { Value::Int(i) => *i, _ => return Err("bit_not: expected Int".into()) };
3268            Ok(Some(Value::Int(!a)))
3269        }
3270        "bit_shl" => {
3271            if args.len() != 2 { return Err("bit_shl requires 2 Int args".into()); }
3272            let a = match &args[0] { Value::Int(i) => *i, _ => return Err("bit_shl: expected Int".into()) };
3273            let n = match &args[1] { Value::Int(i) => *i, _ => return Err("bit_shl: expected Int".into()) };
3274            if n < 0 || n > 63 { return Err("bit_shl: shift amount must be 0-63".into()); }
3275            Ok(Some(Value::Int(((a as u64) << n) as i64)))
3276        }
3277        "bit_shr" => {
3278            if args.len() != 2 { return Err("bit_shr requires 2 Int args".into()); }
3279            let a = match &args[0] { Value::Int(i) => *i, _ => return Err("bit_shr: expected Int".into()) };
3280            let n = match &args[1] { Value::Int(i) => *i, _ => return Err("bit_shr: expected Int".into()) };
3281            if n < 0 || n > 63 { return Err("bit_shr: shift amount must be 0-63".into()); }
3282            Ok(Some(Value::Int(((a as u64) >> n) as i64)))
3283        }
3284        "popcount" => {
3285            if args.len() != 1 { return Err("popcount requires 1 Int arg".into()); }
3286            let a = match &args[0] { Value::Int(i) => *i, _ => return Err("popcount: expected Int".into()) };
3287            Ok(Some(Value::Int((a as u64).count_ones() as i64)))
3288        }
3289
3290        // Phase C2: Optimizer constructors
3291        "Adam.new" => {
3292            if args.len() < 2 || args.len() > 4 {
3293                return Err("Adam.new requires 2-4 args: n_params, lr, [beta1], [beta2]".into());
3294            }
3295            let n = match &args[0] { Value::Int(i) => *i as usize, _ => return Err("Adam.new: n_params must be Int".into()) };
3296            let lr = match &args[1] { Value::Float(f) => *f, Value::Int(i) => *i as f64, _ => return Err("Adam.new: lr must be Float".into()) };
3297            let beta1 = if args.len() > 2 {
3298                match &args[2] { Value::Float(f) => *f, Value::Int(i) => *i as f64, _ => return Err("Adam.new: beta1 must be Float".into()) }
3299            } else { 0.9 };
3300            let beta2 = if args.len() > 3 {
3301                match &args[3] { Value::Float(f) => *f, Value::Int(i) => *i as f64, _ => return Err("Adam.new: beta2 must be Float".into()) }
3302            } else { 0.999 };
3303            let mut state = crate::ml::AdamState::new(n, lr);
3304            state.beta1 = beta1;
3305            state.beta2 = beta2;
3306            let erased: Rc<RefCell<dyn std::any::Any>> = Rc::new(RefCell::new(state));
3307            Ok(Some(Value::OptimizerState(erased)))
3308        }
3309        "Sgd.new" => {
3310            if args.len() < 2 || args.len() > 3 {
3311                return Err("Sgd.new requires 2-3 args: n_params, lr, [momentum]".into());
3312            }
3313            let n = match &args[0] { Value::Int(i) => *i as usize, _ => return Err("Sgd.new: n_params must be Int".into()) };
3314            let lr = match &args[1] { Value::Float(f) => *f, Value::Int(i) => *i as f64, _ => return Err("Sgd.new: lr must be Float".into()) };
3315            let momentum = if args.len() > 2 {
3316                match &args[2] { Value::Float(f) => *f, Value::Int(i) => *i as f64, _ => return Err("Sgd.new: momentum must be Float".into()) }
3317            } else { 0.0 };
3318            let state = crate::ml::SgdState::new(n, lr, momentum);
3319            let erased: Rc<RefCell<dyn std::any::Any>> = Rc::new(RefCell::new(state));
3320            Ok(Some(Value::OptimizerState(erased)))
3321        }
3322
3323        // ---- ML Autodiff Builtins ----
3324        // stop_gradient: returns x unchanged; in AD context, gradients don't flow through
3325        "stop_gradient" => {
3326            if args.len() != 1 { return Err("stop_gradient requires exactly 1 argument".into()); }
3327            Ok(Some(args[0].clone()))
3328        }
3329        // grad_checkpoint: returns x unchanged; semantic marker for memory checkpointing
3330        "grad_checkpoint" => {
3331            if args.len() != 1 { return Err("grad_checkpoint requires exactly 1 argument".into()); }
3332            Ok(Some(args[0].clone()))
3333        }
3334        // clip_grad: clips a gradient value to [min_val, max_val] range
3335        "clip_grad" => {
3336            if args.len() != 3 { return Err("clip_grad requires 3 arguments (value, min, max)".into()); }
3337            let val = match &args[0] {
3338                Value::Float(f) => *f,
3339                Value::Int(i) => *i as f64,
3340                _ => return Err("clip_grad requires numeric arguments".into()),
3341            };
3342            let min_val = match &args[1] { Value::Float(f) => *f, Value::Int(i) => *i as f64, _ => return Err("clip_grad min must be numeric".into()) };
3343            let max_val = match &args[2] { Value::Float(f) => *f, Value::Int(i) => *i as f64, _ => return Err("clip_grad max must be numeric".into()) };
3344            Ok(Some(Value::Float(val.max(min_val).min(max_val))))
3345        }
3346        // grad_scale: scales a gradient value by a scalar factor
3347        "grad_scale" => {
3348            if args.len() != 2 { return Err("grad_scale requires 2 arguments (value, scale)".into()); }
3349            let val = match &args[0] { Value::Float(f) => *f, Value::Int(i) => *i as f64, _ => return Err("grad_scale requires numeric first arg".into()) };
3350            let scale = match &args[1] { Value::Float(f) => *f, Value::Int(i) => *i as f64, _ => return Err("grad_scale requires numeric scale".into()) };
3351            Ok(Some(Value::Float(val * scale)))
3352        }
3353
3354        // ── v0.1 Broadcasting builtins ──────────────────────────────
3355
3356        "broadcast" => {
3357            if args.len() != 2 {
3358                return Err("broadcast requires 2 arguments (fn_name, tensor)".into());
3359            }
3360            let fn_name = match &args[0] {
3361                Value::String(s) => s.clone(),
3362                _ => return Err("broadcast: first argument must be a function name string".into()),
3363            };
3364            let t = value_to_tensor(&args[1])?;
3365
3366            // SIMD-accelerated path for known unary operations that can be
3367            // vectorized with AVX2 (bit-identical to scalar).
3368            match fn_name.as_str() {
3369                "sqrt" => return Ok(Some(Value::Tensor(t.map_simd(UnaryOp::Sqrt)))),
3370                "abs"  => return Ok(Some(Value::Tensor(t.map_simd(UnaryOp::Abs)))),
3371                "neg"  => return Ok(Some(Value::Tensor(t.map_simd(UnaryOp::Neg)))),
3372                "relu" => return Ok(Some(Value::Tensor(t.map_simd(UnaryOp::Relu)))),
3373                _ => {} // fall through to scalar path
3374            }
3375
3376            // Scalar path for transcendental functions (sin, cos, exp, etc.)
3377            // These cannot be trivially SIMD-vectorized while preserving
3378            // bit-identical results with libm scalar implementations.
3379            let f: Box<dyn Fn(f64) -> f64> = match fn_name.as_str() {
3380                "sin"     => Box::new(|x: f64| x.sin()),
3381                "cos"     => Box::new(|x: f64| x.cos()),
3382                "tan"     => Box::new(|x: f64| x.tan()),
3383                "asin"    => Box::new(|x: f64| x.asin()),
3384                "acos"    => Box::new(|x: f64| x.acos()),
3385                "atan"    => Box::new(|x: f64| x.atan()),
3386                "exp"     => Box::new(|x: f64| x.exp()),
3387                "ln"      => Box::new(|x: f64| x.ln()),
3388                "log"     => Box::new(|x: f64| x.ln()),
3389                "log2"    => Box::new(|x: f64| x.log2()),
3390                "log10"   => Box::new(|x: f64| x.log10()),
3391                "log1p"   => Box::new(|x: f64| x.ln_1p()),
3392                "expm1"   => Box::new(|x: f64| x.exp_m1()),
3393                "floor"   => Box::new(|x: f64| x.floor()),
3394                "ceil"    => Box::new(|x: f64| x.ceil()),
3395                "round"   => Box::new(|x: f64| x.round()),
3396                "sigmoid" => Box::new(|x: f64| 1.0 / (1.0 + (-x).exp())),
3397                "tanh"    => Box::new(|x: f64| x.tanh()),
3398                "sign"    => Box::new(|x: f64| {
3399                    if x > 0.0 { 1.0 } else if x < 0.0 { -1.0 } else { 0.0 }
3400                }),
3401                _ => return Err(format!("broadcast: unknown unary function '{fn_name}'")),
3402            };
3403            Ok(Some(Value::Tensor(t.map(f))))
3404        }
3405
3406        "broadcast2" => {
3407            if args.len() != 3 {
3408                return Err("broadcast2 requires 3 arguments (fn_name, tensor1, tensor2)".into());
3409            }
3410            let fn_name = match &args[0] {
3411                Value::String(s) => s.clone(),
3412                _ => return Err("broadcast2: first argument must be a function name string".into()),
3413            };
3414            let t1 = value_to_tensor(&args[1])?;
3415            let t2 = value_to_tensor(&args[2])?;
3416            let result = match fn_name.as_str() {
3417                "add"   => t1.add(&t2),
3418                "sub"   => t1.sub(&t2),
3419                "mul"   => t1.mul_elem(&t2),
3420                "div"   => t1.div_elem(&t2),
3421                "pow"   => t1.elem_pow(&t2),
3422                "min"   => t1.elem_min(&t2),
3423                "max"   => t1.elem_max(&t2),
3424                "atan2" => t1.elem_atan2(&t2),
3425                "hypot" => t1.elem_hypot(&t2),
3426                _ => return Err(format!("broadcast2: unknown binary function '{fn_name}'")),
3427            };
3428            match result {
3429                Ok(t) => Ok(Some(Value::Tensor(t))),
3430                Err(e) => Err(format!("broadcast2: {e}")),
3431            }
3432        }
3433
3434        // ── Peak RSS memory tracking ─────────────────────────────────
3435        "peak_rss" => {
3436            Ok(Some(Value::Int(peak_rss_kb() as i64)))
3437        }
3438
3439        // ── Fused broadcast operations (eliminate intermediate tensors) ──
3440        "broadcast_fma" => {
3441            // broadcast_fma(a, b, c) = a * b + c element-wise in one pass.
3442            // Eliminates the intermediate tensor that broadcast2("mul") would create.
3443            if args.len() != 3 {
3444                return Err("broadcast_fma requires 3 arguments (a, b, c)".into());
3445            }
3446            let a = value_to_tensor(&args[0])?;
3447            let b = value_to_tensor(&args[1])?;
3448            let c = value_to_tensor(&args[2])?;
3449            let result = a.fused_mul_add(&b, &c)
3450                .map_err(|e| format!("broadcast_fma: {e}"))?;
3451            Ok(Some(Value::Tensor(result)))
3452        }
3453
3454        // -- Phase 2: Tensor boolean/masking ops --------------------------------
3455        "tensor_where" => {
3456            let a = value_to_tensor(&args[0])?;
3457            let cond = value_to_tensor(&args[1])?;
3458            let other = value_to_tensor(&args[2])?;
3459            Ok(Some(Value::Tensor(a.tensor_where(cond, other).map_err(|e| format!("{e}"))?)))
3460        }
3461        "tensor_any" => {
3462            let t = value_to_tensor(&args[0])?;
3463            Ok(Some(Value::Bool(t.any())))
3464        }
3465        "tensor_all" => {
3466            let t = value_to_tensor(&args[0])?;
3467            Ok(Some(Value::Bool(t.all())))
3468        }
3469        "tensor_nonzero" => {
3470            let t = value_to_tensor(&args[0])?;
3471            Ok(Some(Value::Tensor(t.nonzero())))
3472        }
3473        "tensor_masked_fill" => {
3474            let t = value_to_tensor(&args[0])?;
3475            let mask = value_to_tensor(&args[1])?;
3476            let val = value_to_f64(&args[2])?;
3477            Ok(Some(Value::Tensor(t.masked_fill(mask, val).map_err(|e| format!("{e}"))?)))
3478        }
3479        // -- Phase 2: Axis reductions ------------------------------------------
3480        "tensor_mean_axis" => {
3481            let t = value_to_tensor(&args[0])?;
3482            let axis = value_to_usize(&args[1])?;
3483            let keepdim = matches!(args.get(2), Some(Value::Bool(true)));
3484            Ok(Some(Value::Tensor(t.mean_axis(axis, keepdim).map_err(|e| format!("{e}"))?)))
3485        }
3486        "tensor_var_axis" => {
3487            let t = value_to_tensor(&args[0])?;
3488            let axis = value_to_usize(&args[1])?;
3489            let keepdim = matches!(args.get(2), Some(Value::Bool(true)));
3490            Ok(Some(Value::Tensor(t.var_axis(axis, keepdim).map_err(|e| format!("{e}"))?)))
3491        }
3492        "tensor_std_axis" => {
3493            let t = value_to_tensor(&args[0])?;
3494            let axis = value_to_usize(&args[1])?;
3495            let keepdim = matches!(args.get(2), Some(Value::Bool(true)));
3496            Ok(Some(Value::Tensor(t.std_axis(axis, keepdim).map_err(|e| format!("{e}"))?)))
3497        }
3498        "tensor_prod_axis" => {
3499            let t = value_to_tensor(&args[0])?;
3500            let axis = value_to_usize(&args[1])?;
3501            let keepdim = matches!(args.get(2), Some(Value::Bool(true)));
3502            Ok(Some(Value::Tensor(t.prod_axis(axis, keepdim).map_err(|e| format!("{e}"))?)))
3503        }
3504        // -- Phase 2: Sort ops -------------------------------------------------
3505        "tensor_sort" => {
3506            let t = value_to_tensor(&args[0])?;
3507            let axis = value_to_usize(&args[1])?;
3508            let desc = matches!(args.get(2), Some(Value::Bool(true)));
3509            Ok(Some(Value::Tensor(t.sort_axis(axis, desc).map_err(|e| format!("{e}"))?)))
3510        }
3511        "tensor_argsort_axis" => {
3512            let t = value_to_tensor(&args[0])?;
3513            let axis = value_to_usize(&args[1])?;
3514            let desc = matches!(args.get(2), Some(Value::Bool(true)));
3515            Ok(Some(Value::Tensor(t.argsort_axis(axis, desc).map_err(|e| format!("{e}"))?)))
3516        }
3517        // -- Phase 2: Einsum ---------------------------------------------------
3518        "einsum" => {
3519            let notation = match &args[0] {
3520                Value::String(s) => s.as_str().to_string(),
3521                _ => return Err("einsum: first arg must be notation string".into()),
3522            };
3523            let tensors: Vec<&Tensor> = args[1..].iter()
3524                .map(value_to_tensor)
3525                .collect::<Result<_, _>>()?;
3526            let result = Tensor::einsum(&notation, &tensors).map_err(|e| format!("{e}"))?;
3527            Ok(Some(Value::Tensor(result)))
3528        }
3529        // -- Phase 2: Reshape enhancements -------------------------------------
3530        "tensor_unsqueeze" => {
3531            let t = value_to_tensor(&args[0])?;
3532            let dim = value_to_usize(&args[1])?;
3533            Ok(Some(Value::Tensor(t.unsqueeze(dim).map_err(|e| format!("{e}"))?)))
3534        }
3535        "tensor_squeeze" => {
3536            let t = value_to_tensor(&args[0])?;
3537            let dim = args.get(1).map(|v| value_to_usize(v)).transpose()?;
3538            Ok(Some(Value::Tensor(t.squeeze(dim).map_err(|e| format!("{e}"))?)))
3539        }
3540        "tensor_flatten" => {
3541            let t = value_to_tensor(&args[0])?;
3542            let start = value_to_usize(&args[1])?;
3543            let end = value_to_usize(&args[2])?;
3544            Ok(Some(Value::Tensor(t.flatten(start, end).map_err(|e| format!("{e}"))?)))
3545        }
3546        "tensor_chunk" => {
3547            let t = value_to_tensor(&args[0])?;
3548            let n = value_to_usize(&args[1])?;
3549            let dim = value_to_usize(&args[2])?;
3550            let chunks = t.chunk(n, dim).map_err(|e| format!("{e}"))?;
3551            Ok(Some(Value::Array(Rc::new(chunks.into_iter().map(Value::Tensor).collect()))))
3552        }
3553        // -- Phase 3: SVD, PCA, Pseudoinverse ----------------------------------
3554        "svd" => {
3555            let t = value_to_tensor(&args[0])?;
3556            let (u, s, vt) = t.svd().map_err(|e| format!("{e}"))?;
3557            let s_tensor = Tensor::from_vec(s, &[u.shape()[1]]).map_err(|e| format!("{e}"))?;
3558            Ok(Some(Value::Tuple(Rc::new(vec![
3559                Value::Tensor(u),
3560                Value::Tensor(s_tensor),
3561                Value::Tensor(vt),
3562            ]))))
3563        }
3564        "pinv" => {
3565            let t = value_to_tensor(&args[0])?;
3566            Ok(Some(Value::Tensor(t.pinv().map_err(|e| format!("{e}"))?)))
3567        }
3568        "pca" => {
3569            let t = value_to_tensor(&args[0])?;
3570            let n_components = value_to_usize(&args[1])?;
3571            let (transformed, components, variance) = crate::ml::pca(&t, n_components).map_err(|e| format!("{e}"))?;
3572            let vlen = variance.len();
3573            let var_tensor = Tensor::from_vec(variance, &[vlen]).map_err(|e| format!("{e}"))?;
3574            Ok(Some(Value::Tuple(Rc::new(vec![
3575                Value::Tensor(transformed),
3576                Value::Tensor(components),
3577                Value::Tensor(var_tensor),
3578            ]))))
3579        }
3580        // -- Phase 7: Sparse operations ----------------------------------------
3581        "sparse_add" => {
3582            let a = value_to_sparse(&args[0])?;
3583            let b = value_to_sparse(&args[1])?;
3584            Ok(Some(Value::SparseTensor(crate::sparse::sparse_add(a, b).map_err(|e| e)?)))
3585        }
3586        "sparse_sub" => {
3587            let a = value_to_sparse(&args[0])?;
3588            let b = value_to_sparse(&args[1])?;
3589            Ok(Some(Value::SparseTensor(crate::sparse::sparse_sub(a, b).map_err(|e| e)?)))
3590        }
3591        "sparse_matmul" => {
3592            let a = value_to_sparse(&args[0])?;
3593            let b = value_to_sparse(&args[1])?;
3594            Ok(Some(Value::SparseTensor(crate::sparse::sparse_matmul(a, b).map_err(|e| e)?)))
3595        }
3596        "sparse_transpose" => {
3597            let a = value_to_sparse(&args[0])?;
3598            Ok(Some(Value::SparseTensor(crate::sparse::sparse_transpose(a))))
3599        }
3600        // -- Phase 9: Clustering -----------------------------------------------
3601        "kmeans" => {
3602            let data = value_to_f64_vec(&args[0])?;
3603            let n_samples = value_to_usize(&args[1])?;
3604            let n_features = value_to_usize(&args[2])?;
3605            let k = value_to_usize(&args[3])?;
3606            let max_iter = value_to_usize(&args[4])?;
3607            let seed = match &args[5] { Value::Int(v) => *v as u64, _ => 42 };
3608            let (centroids, labels, inertia) = crate::clustering::kmeans(&data, n_samples, n_features, k, max_iter, seed);
3609            let label_vals: Vec<Value> = labels.iter().map(|&l| Value::Int(l as i64)).collect();
3610            let centroid_t = Tensor::from_vec(centroids, &[k, n_features]).map_err(|e| format!("{e}"))?;
3611            Ok(Some(Value::Tuple(Rc::new(vec![
3612                Value::Tensor(centroid_t),
3613                Value::Array(Rc::new(label_vals)),
3614                Value::Float(inertia),
3615            ]))))
3616        }
3617        "dbscan" => {
3618            let data = value_to_f64_vec(&args[0])?;
3619            let n_samples = value_to_usize(&args[1])?;
3620            let n_features = value_to_usize(&args[2])?;
3621            let eps = value_to_f64(&args[3])?;
3622            let min_samples = value_to_usize(&args[4])?;
3623            let labels = crate::clustering::dbscan(&data, n_samples, n_features, eps, min_samples);
3624            let label_vals: Vec<Value> = labels.iter().map(|&l| Value::Int(l)).collect();
3625            Ok(Some(Value::Array(Rc::new(label_vals))))
3626        }
3627        // -- Phase 10: Categorical encoding ------------------------------------
3628        "label_encode" => {
3629            // label_encode: convert array of strings to (sorted_levels, codes)
3630            // Uses BTreeSet for deterministic sorted order
3631            let strs: Vec<String> = match &args[0] {
3632                Value::Array(arr) => arr.iter().map(|v| match v {
3633                    Value::String(s) => Ok(s.as_str().to_string()),
3634                    _ => Err("label_encode: expected array of strings".to_string()),
3635                }).collect::<Result<_, _>>()?,
3636                _ => return Err("label_encode: expected array".into()),
3637            };
3638            let mut level_set = std::collections::BTreeSet::new();
3639            for s in &strs { level_set.insert(s.clone()); }
3640            let levels: Vec<String> = level_set.into_iter().collect();
3641            let level_map: std::collections::BTreeMap<&str, u32> = levels.iter().enumerate()
3642                .map(|(i, s)| (s.as_str(), i as u32)).collect();
3643            let codes: Vec<u32> = strs.iter().map(|s| level_map[s.as_str()]).collect();
3644            let level_vals: Vec<Value> = levels.iter().map(|s| Value::String(Rc::new(s.clone()))).collect();
3645            let code_vals: Vec<Value> = codes.iter().map(|&c| Value::Int(c as i64)).collect();
3646            Ok(Some(Value::Tuple(Rc::new(vec![
3647                Value::Array(Rc::new(level_vals)),
3648                Value::Array(Rc::new(code_vals)),
3649            ]))))
3650        }
3651        // -- Phase 11: Time series ---------------------------------------------
3652        "acf" => {
3653            let data = value_to_f64_vec(&args[0])?;
3654            let max_lag = value_to_usize(&args[1])?;
3655            let result = crate::timeseries::acf(&data, max_lag);
3656            Ok(Some(Value::Array(Rc::new(result.into_iter().map(Value::Float).collect()))))
3657        }
3658        "ewma" => {
3659            let data = value_to_f64_vec(&args[0])?;
3660            let alpha = value_to_f64(&args[1])?;
3661            let result = crate::timeseries::ewma(&data, alpha);
3662            Ok(Some(Value::Array(Rc::new(result.into_iter().map(Value::Float).collect()))))
3663        }
3664        "diff" => {
3665            let data = value_to_f64_vec(&args[0])?;
3666            let periods = value_to_usize(&args[1])?;
3667            let result = crate::timeseries::diff(&data, periods);
3668            Ok(Some(Value::Array(Rc::new(result.into_iter().map(Value::Float).collect()))))
3669        }
3670        // -- Phase 5: Optimization root finding --------------------------------
3671        "bisect" => {
3672            // bisect expects a closure as first arg — handled by executor, not here
3673            Ok(None)
3674        }
3675        // -- Phase 6: Interpolation --------------------------------------------
3676        "polyfit" => {
3677            let x = value_to_f64_vec(&args[0])?;
3678            let y = value_to_f64_vec(&args[1])?;
3679            let degree = value_to_usize(&args[2])?;
3680            let coeffs = crate::interpolate::polyfit(&x, &y, degree).map_err(|e| e)?;
3681            Ok(Some(Value::Array(Rc::new(coeffs.into_iter().map(Value::Float).collect()))))
3682        }
3683        "polyval" => {
3684            let coeffs = value_to_f64_vec(&args[0])?;
3685            let x = value_to_f64_vec(&args[1])?;
3686            let result = crate::interpolate::polyval(&coeffs, &x);
3687            Ok(Some(Value::Array(Rc::new(result.into_iter().map(Value::Float).collect()))))
3688        }
3689
3690        // ── Phase 2 Beta Hardening: getenv ──────────────────────────────
3691        "getenv" => {
3692            if args.len() != 1 { return Err("getenv requires 1 argument (name)".into()); }
3693            let name = match &args[0] {
3694                Value::String(s) => s.as_str().to_string(),
3695                _ => return Err("getenv: argument must be String".into()),
3696            };
3697            let val = std::env::var(&name).unwrap_or_default();
3698            Ok(Some(Value::String(Rc::new(val))))
3699        }
3700
3701        // ── Phase 2 Beta Hardening: Functional map builtins ─────────────
3702        "map_new" => {
3703            if !args.is_empty() { return Err("map_new takes 0 arguments".into()); }
3704            Ok(Some(Value::Map(Rc::new(RefCell::new(crate::det_map::DetMap::new())))))
3705        }
3706        "map_set" => {
3707            if args.len() != 3 { return Err("map_set requires 3 args: map, key, value".into()); }
3708            let m = match &args[0] {
3709                Value::Map(m) => m,
3710                _ => return Err("map_set: first argument must be Map".into()),
3711            };
3712            let mut new_map = m.borrow().clone();
3713            new_map.insert(args[1].clone(), args[2].clone());
3714            Ok(Some(Value::Map(Rc::new(RefCell::new(new_map)))))
3715        }
3716        "map_get" => {
3717            if args.len() != 2 { return Err("map_get requires 2 args: map, key".into()); }
3718            let m = match &args[0] {
3719                Value::Map(m) => m,
3720                _ => return Err("map_get: first argument must be Map".into()),
3721            };
3722            match m.borrow().get(&args[1]) {
3723                Some(v) => Ok(Some(v.clone())),
3724                None => Ok(Some(Value::Void)),
3725            }
3726        }
3727        "map_keys" => {
3728            if args.len() != 1 { return Err("map_keys requires 1 arg: map".into()); }
3729            let m = match &args[0] {
3730                Value::Map(m) => m,
3731                _ => return Err("map_keys: argument must be Map".into()),
3732            };
3733            Ok(Some(Value::Array(Rc::new(m.borrow().keys()))))
3734        }
3735        "map_values" => {
3736            if args.len() != 1 { return Err("map_values requires 1 arg: map".into()); }
3737            let m = match &args[0] {
3738                Value::Map(m) => m,
3739                _ => return Err("map_values: argument must be Map".into()),
3740            };
3741            Ok(Some(Value::Array(Rc::new(m.borrow().values_vec()))))
3742        }
3743        "map_contains" => {
3744            if args.len() != 2 { return Err("map_contains requires 2 args: map, key".into()); }
3745            let m = match &args[0] {
3746                Value::Map(m) => m,
3747                _ => return Err("map_contains: first argument must be Map".into()),
3748            };
3749            Ok(Some(Value::Bool(m.borrow().contains_key(&args[1]))))
3750        }
3751
3752        // -- Phase 3: Numerical integration ------------------------------------
3753        "trapezoid" | "trapz" => {
3754            if args.len() != 2 {
3755                return Err("trapezoid requires 2 arguments (xs, ys)".into());
3756            }
3757            let xs = value_to_f64_vec(&args[0])?;
3758            let ys = value_to_f64_vec(&args[1])?;
3759            let result = crate::integrate::trapezoid(&xs, &ys)?;
3760            Ok(Some(Value::Float(result)))
3761        }
3762        "simpson" | "simps" => {
3763            if args.len() != 2 {
3764                return Err("simpson requires 2 arguments (xs, ys)".into());
3765            }
3766            let xs = value_to_f64_vec(&args[0])?;
3767            let ys = value_to_f64_vec(&args[1])?;
3768            let result = crate::integrate::simpson(&xs, &ys)?;
3769            Ok(Some(Value::Float(result)))
3770        }
3771        "cumtrapz" => {
3772            if args.len() != 2 {
3773                return Err("cumtrapz requires 2 arguments (xs, ys)".into());
3774            }
3775            let xs = value_to_f64_vec(&args[0])?;
3776            let ys = value_to_f64_vec(&args[1])?;
3777            let result = crate::integrate::cumtrapz(&xs, &ys)?;
3778            Ok(Some(Value::Array(Rc::new(
3779                result.into_iter().map(Value::Float).collect(),
3780            ))))
3781        }
3782        // -- Phase 3: Numerical differentiation --------------------------------
3783        "diff_central" => {
3784            if args.len() != 2 {
3785                return Err("diff_central requires 2 arguments (xs, ys)".into());
3786            }
3787            let xs = value_to_f64_vec(&args[0])?;
3788            let ys = value_to_f64_vec(&args[1])?;
3789            let result = crate::differentiate::diff_central(&xs, &ys)?;
3790            Ok(Some(Value::Array(Rc::new(
3791                result.into_iter().map(Value::Float).collect(),
3792            ))))
3793        }
3794        "diff_forward" => {
3795            if args.len() != 2 {
3796                return Err("diff_forward requires 2 arguments (xs, ys)".into());
3797            }
3798            let xs = value_to_f64_vec(&args[0])?;
3799            let ys = value_to_f64_vec(&args[1])?;
3800            let result = crate::differentiate::diff_forward(&xs, &ys)?;
3801            Ok(Some(Value::Array(Rc::new(
3802                result.into_iter().map(Value::Float).collect(),
3803            ))))
3804        }
3805        "gradient_1d" => {
3806            if args.len() != 2 {
3807                return Err("gradient_1d requires 2 arguments (ys, dx)".into());
3808            }
3809            let ys = value_to_f64_vec(&args[0])?;
3810            let dx = value_to_f64(&args[1])?;
3811            let result = crate::differentiate::gradient_1d(&ys, dx);
3812            Ok(Some(Value::Array(Rc::new(
3813                result.into_iter().map(Value::Float).collect(),
3814            ))))
3815        }
3816        // -- Phase 3: Constrained optimization ---------------------------------
3817        "penalty_objective" => {
3818            if args.len() != 3 {
3819                return Err(
3820                    "penalty_objective requires 3 arguments (f_val, constraint_violations, penalty)"
3821                        .into(),
3822                );
3823            }
3824            let f_val = value_to_f64(&args[0])?;
3825            let violations = value_to_f64_vec(&args[1])?;
3826            let penalty = value_to_f64(&args[2])?;
3827            let result = crate::optimize::penalty_objective(f_val, &violations, penalty);
3828            Ok(Some(Value::Float(result)))
3829        }
3830        "project_box" => {
3831            if args.len() != 3 {
3832                return Err("project_box requires 3 arguments (x, lower, upper)".into());
3833            }
3834            let x = value_to_f64_vec(&args[0])?;
3835            let lower = value_to_f64_vec(&args[1])?;
3836            let upper = value_to_f64_vec(&args[2])?;
3837            let result = crate::optimize::project_box(&x, &lower, &upper)?;
3838            Ok(Some(Value::Array(Rc::new(
3839                result.into_iter().map(Value::Float).collect(),
3840            ))))
3841        }
3842        "projected_gd_step" => {
3843            if args.len() != 5 {
3844                return Err(
3845                    "projected_gd_step requires 5 arguments (x, grad, lr, lower, upper)".into(),
3846                );
3847            }
3848            let x = value_to_f64_vec(&args[0])?;
3849            let grad = value_to_f64_vec(&args[1])?;
3850            let lr = value_to_f64(&args[2])?;
3851            let lower = value_to_f64_vec(&args[3])?;
3852            let upper = value_to_f64_vec(&args[4])?;
3853            let result = crate::optimize::projected_gd_step(&x, &grad, lr, &lower, &upper)?;
3854            Ok(Some(Value::Array(Rc::new(
3855                result.into_iter().map(Value::Float).collect(),
3856            ))))
3857        }
3858
3859        // ── LSTM cell ───────────────────────────────────────────────
3860        "lstm_cell" => {
3861            if args.len() != 7 {
3862                return Err("lstm_cell requires 7 Tensor arguments: x, h_prev, c_prev, w_ih, w_hh, b_ih, b_hh".into());
3863            }
3864            let x = value_to_tensor(&args[0])?;
3865            let h_prev = value_to_tensor(&args[1])?;
3866            let c_prev = value_to_tensor(&args[2])?;
3867            let w_ih = value_to_tensor(&args[3])?;
3868            let w_hh = value_to_tensor(&args[4])?;
3869            let b_ih = value_to_tensor(&args[5])?;
3870            let b_hh = value_to_tensor(&args[6])?;
3871            let (h_new, c_new) = crate::ml::lstm_cell(x, h_prev, c_prev, w_ih, w_hh, b_ih, b_hh)?;
3872            Ok(Some(Value::Tuple(Rc::new(vec![
3873                Value::Tensor(h_new),
3874                Value::Tensor(c_new),
3875            ]))))
3876        }
3877
3878        // ── GRU cell ────────────────────────────────────────────────
3879        "gru_cell" => {
3880            if args.len() != 6 {
3881                return Err("gru_cell requires 6 Tensor arguments: x, h_prev, w_ih, w_hh, b_ih, b_hh".into());
3882            }
3883            let x = value_to_tensor(&args[0])?;
3884            let h_prev = value_to_tensor(&args[1])?;
3885            let w_ih = value_to_tensor(&args[2])?;
3886            let w_hh = value_to_tensor(&args[3])?;
3887            let b_ih = value_to_tensor(&args[4])?;
3888            let b_hh = value_to_tensor(&args[5])?;
3889            let h_new = crate::ml::gru_cell(x, h_prev, w_ih, w_hh, b_ih, b_hh)?;
3890            Ok(Some(Value::Tensor(h_new)))
3891        }
3892
3893        // ── Multi-Head Attention ────────────────────────────────────
3894        "multi_head_attention" => {
3895            if args.len() != 12 {
3896                return Err("multi_head_attention requires 12 arguments: q, k, v, w_q, w_k, w_v, w_o, b_q, b_k, b_v, b_o, num_heads".into());
3897            }
3898            let q = value_to_tensor(&args[0])?;
3899            let k = value_to_tensor(&args[1])?;
3900            let v = value_to_tensor(&args[2])?;
3901            let w_q = value_to_tensor(&args[3])?;
3902            let w_k = value_to_tensor(&args[4])?;
3903            let w_v = value_to_tensor(&args[5])?;
3904            let w_o = value_to_tensor(&args[6])?;
3905            let b_q = value_to_tensor(&args[7])?;
3906            let b_k = value_to_tensor(&args[8])?;
3907            let b_v = value_to_tensor(&args[9])?;
3908            let b_o = value_to_tensor(&args[10])?;
3909            let num_heads = value_to_usize(&args[11])?;
3910            let out = crate::ml::multi_head_attention(
3911                q, k, v, w_q, w_k, w_v, w_o, b_q, b_k, b_v, b_o, num_heads,
3912            )?;
3913            Ok(Some(Value::Tensor(out)))
3914        }
3915
3916        // ── AR fit (Yule-Walker) ────────────────────────────────────
3917        "ar_fit" => {
3918            if args.len() != 2 {
3919                return Err("ar_fit requires 2 arguments: data (array), p (int)".into());
3920            }
3921            let data = value_to_f64_vec(&args[0])?;
3922            let p = value_to_usize(&args[1])?;
3923            let coeffs = crate::timeseries::ar_fit(&data, p)?;
3924            Ok(Some(Value::Array(Rc::new(
3925                coeffs.into_iter().map(Value::Float).collect(),
3926            ))))
3927        }
3928
3929        // ── ARIMA differencing ──────────────────────────────────────
3930        "arima_diff" => {
3931            if args.len() != 2 {
3932                return Err("arima_diff requires 2 arguments: data (array), d (int)".into());
3933            }
3934            let data = value_to_f64_vec(&args[0])?;
3935            let d = value_to_usize(&args[1])?;
3936            let result = crate::timeseries::arima_diff(&data, d);
3937            Ok(Some(Value::Array(Rc::new(
3938                result.into_iter().map(Value::Float).collect(),
3939            ))))
3940        }
3941
3942        // ── AR forecast ─────────────────────────────────────────────
3943        "ar_forecast" => {
3944            if args.len() != 3 {
3945                return Err("ar_forecast requires 3 arguments: coeffs (array), history (array), steps (int)".into());
3946            }
3947            let coeffs = value_to_f64_vec(&args[0])?;
3948            let history = value_to_f64_vec(&args[1])?;
3949            let steps = value_to_usize(&args[2])?;
3950            let result = crate::timeseries::ar_forecast(&coeffs, &history, steps)?;
3951            Ok(Some(Value::Array(Rc::new(
3952                result.into_iter().map(Value::Float).collect(),
3953            ))))
3954        }
3955
3956        // ── Phase 5: Preprocessing builtins ─────────────────────────────
3957        "fillna" => {
3958            if args.len() != 2 { return Err("fillna requires 2 arguments (array, fill_value)".into()); }
3959            let arr = match &args[0] {
3960                Value::Array(a) => a.as_ref().clone(),
3961                _ => return Err(format!("fillna: first argument must be Array, got {}", args[0].type_name())),
3962            };
3963            let fill = &args[1];
3964            let result: Vec<Value> = arr.iter().map(|v| {
3965                match v {
3966                    Value::Na => fill.clone(),
3967                    Value::Void => fill.clone(),
3968                    Value::Float(f) if f.is_nan() => fill.clone(),
3969                    other => other.clone(),
3970                }
3971            }).collect();
3972            Ok(Some(Value::Array(Rc::new(result))))
3973        }
3974
3975        "is_na" => {
3976            if args.len() != 1 { return Err("is_na requires 1 argument".into()); }
3977            let result = matches!(&args[0], Value::Na);
3978            Ok(Some(Value::Bool(result)))
3979        }
3980
3981        "is_not_null" => {
3982            if args.len() != 1 { return Err("is_not_null requires 1 argument".into()); }
3983            let result = match &args[0] {
3984                Value::Na => false,
3985                Value::Void => false,
3986                Value::Float(f) => !f.is_nan(),
3987                _ => true,
3988            };
3989            Ok(Some(Value::Bool(result)))
3990        }
3991
3992        "drop_na" => {
3993            if args.len() != 1 { return Err("drop_na requires 1 argument (array)".into()); }
3994            let arr = match &args[0] {
3995                Value::Array(a) => a.as_ref().clone(),
3996                _ => return Err(format!("drop_na: first argument must be Array, got {}", args[0].type_name())),
3997            };
3998            let result: Vec<Value> = arr.into_iter().filter(|v| !matches!(v, Value::Na)).collect();
3999            Ok(Some(Value::Array(Rc::new(result))))
4000        }
4001
4002        "interpolate_linear" => {
4003            if args.len() != 1 { return Err("interpolate_linear requires 1 argument (array of f64)".into()); }
4004            let data = value_to_f64_vec(&args[0])?;
4005            let n = data.len();
4006            if n == 0 {
4007                return Ok(Some(Value::Array(Rc::new(vec![]))));
4008            }
4009            let mut result = data.clone();
4010
4011            // Find first and last non-NaN for edge fill
4012            let first_valid = result.iter().position(|x| !x.is_nan());
4013            let last_valid = result.iter().rposition(|x| !x.is_nan());
4014
4015            if let (Some(fv), Some(lv)) = (first_valid, last_valid) {
4016                // Backward-fill leading NaNs
4017                for i in 0..fv {
4018                    result[i] = result[fv];
4019                }
4020                // Forward-fill trailing NaNs
4021                for i in (lv + 1)..n {
4022                    result[i] = result[lv];
4023                }
4024                // Linearly interpolate interior NaNs
4025                let mut i = fv + 1;
4026                while i < lv {
4027                    if result[i].is_nan() {
4028                        // Find the next non-NaN
4029                        let start = i - 1;
4030                        let mut end = i + 1;
4031                        while end < n && result[end].is_nan() {
4032                            end += 1;
4033                        }
4034                        let v0 = result[start];
4035                        let v1 = result[end];
4036                        let span = (end - start) as f64;
4037                        for j in (start + 1)..end {
4038                            let t = (j - start) as f64 / span;
4039                            // Linear interpolation: v0 + t * (v1 - v0)
4040                            // Using Kahan-style: compute as v0*(1-t) + v1*t
4041                            use cjc_repro::KahanAccumulatorF64;
4042                            let mut acc = KahanAccumulatorF64::new();
4043                            acc.add(v0 * (1.0 - t));
4044                            acc.add(v1 * t);
4045                            result[j] = acc.finalize();
4046                        }
4047                        i = end + 1;
4048                    } else {
4049                        i += 1;
4050                    }
4051                }
4052            }
4053            // If no valid values, result stays all NaN
4054
4055            Ok(Some(Value::Array(Rc::new(
4056                result.into_iter().map(Value::Float).collect(),
4057            ))))
4058        }
4059
4060        "coalesce" => {
4061            if args.is_empty() { return Err("coalesce requires at least 1 argument".into()); }
4062            // Scalar mode: coalesce(val1, val2, ...) → first non-NA/non-Void
4063            let first_is_array = matches!(&args[0], Value::Array(_));
4064            if args.len() == 2 && first_is_array {
4065                // Array mode: coalesce(array_a, array_b) → element-wise
4066                let a = match &args[0] {
4067                    Value::Array(a) => a.as_ref().clone(),
4068                    _ => unreachable!(),
4069                };
4070                let b = match &args[1] {
4071                    Value::Array(b) => b.as_ref().clone(),
4072                    _ => return Err(format!("coalesce: second argument must be Array, got {}", args[1].type_name())),
4073                };
4074                if a.len() != b.len() {
4075                    return Err(format!("coalesce: arrays must have equal length, got {} and {}", a.len(), b.len()));
4076                }
4077                let result: Vec<Value> = a.iter().zip(b.iter()).map(|(va, vb)| {
4078                    let is_null_a = matches!(va, Value::Na) || matches!(va, Value::Void) || matches!(va, Value::Float(f) if f.is_nan());
4079                    if is_null_a { vb.clone() } else { va.clone() }
4080                }).collect();
4081                Ok(Some(Value::Array(Rc::new(result))))
4082            } else {
4083                // Scalar mode: return first non-NA, non-Void value
4084                for arg in args {
4085                    let is_null = matches!(arg, Value::Na) || matches!(arg, Value::Void) || matches!(arg, Value::Float(f) if f.is_nan());
4086                    if !is_null { return Ok(Some(arg.clone())); }
4087                }
4088                // All null → return last arg (or NA)
4089                Ok(Some(args.last().cloned().unwrap_or(Value::Na)))
4090            }
4091        }
4092
4093        "cut" => {
4094            if args.len() != 2 { return Err("cut requires 2 arguments (array, breaks)".into()); }
4095            let data = value_to_f64_vec(&args[0])?;
4096            let breaks = value_to_f64_vec(&args[1])?;
4097            if breaks.is_empty() {
4098                return Err("cut: breaks array must not be empty".into());
4099            }
4100            let mut sorted_breaks = breaks.clone();
4101            sorted_breaks.sort_by(f64::total_cmp);
4102            let labels: Vec<Value> = data.iter().map(|&x| {
4103                // Find the bin
4104                let label = if x <= sorted_breaks[0] {
4105                    format!("(-inf,{}]", sorted_breaks[0])
4106                } else if x > sorted_breaks[sorted_breaks.len() - 1] {
4107                    format!("({},inf)", sorted_breaks[sorted_breaks.len() - 1])
4108                } else {
4109                    let mut found = String::new();
4110                    for i in 1..sorted_breaks.len() {
4111                        if x <= sorted_breaks[i] {
4112                            found = format!("({},{}]", sorted_breaks[i - 1], sorted_breaks[i]);
4113                            break;
4114                        }
4115                    }
4116                    if found.is_empty() {
4117                        // x is exactly at the last break
4118                        format!("({},inf)", sorted_breaks[sorted_breaks.len() - 1])
4119                    } else {
4120                        found
4121                    }
4122                };
4123                Value::String(Rc::new(label))
4124            }).collect();
4125            Ok(Some(Value::Array(Rc::new(labels))))
4126        }
4127
4128        "qcut" => {
4129            if args.len() != 2 { return Err("qcut requires 2 arguments (array, n_bins)".into()); }
4130            let data = value_to_f64_vec(&args[0])?;
4131            let n = value_to_usize(&args[1])?;
4132            if n == 0 {
4133                return Err("qcut: n_bins must be > 0".into());
4134            }
4135            // Compute quantile break points
4136            let mut breaks = Vec::with_capacity(n - 1);
4137            for i in 1..n {
4138                let p = i as f64 / n as f64;
4139                let q = crate::stats::quantile(&data, p)?;
4140                breaks.push(q);
4141            }
4142            // Deduplicate breaks (can happen with repeated values)
4143            breaks.dedup_by(|a, b| *a == *b);
4144
4145            // Re-dispatch to cut logic
4146            let mut sorted_breaks = breaks.clone();
4147            sorted_breaks.sort_by(f64::total_cmp);
4148            let labels: Vec<Value> = data.iter().map(|&x| {
4149                let label = if sorted_breaks.is_empty() || x <= sorted_breaks[0] {
4150                    format!("(-inf,{}]", sorted_breaks.first().copied().unwrap_or(x))
4151                } else if x > sorted_breaks[sorted_breaks.len() - 1] {
4152                    format!("({},inf)", sorted_breaks[sorted_breaks.len() - 1])
4153                } else {
4154                    let mut found = String::new();
4155                    for i in 1..sorted_breaks.len() {
4156                        if x <= sorted_breaks[i] {
4157                            found = format!("({},{}]", sorted_breaks[i - 1], sorted_breaks[i]);
4158                            break;
4159                        }
4160                    }
4161                    if found.is_empty() {
4162                        format!("({},inf)", sorted_breaks[sorted_breaks.len() - 1])
4163                    } else {
4164                        found
4165                    }
4166                };
4167                Value::String(Rc::new(label))
4168            }).collect();
4169            Ok(Some(Value::Array(Rc::new(labels))))
4170        }
4171
4172        "min_max_scale" => {
4173            if args.len() != 3 { return Err("min_max_scale requires 3 arguments (array, low, high)".into()); }
4174            let data = value_to_f64_vec(&args[0])?;
4175            let low = value_to_f64(&args[1])?;
4176            let high = value_to_f64(&args[2])?;
4177            if data.is_empty() {
4178                return Ok(Some(Value::Array(Rc::new(vec![]))));
4179            }
4180            let mut min_val = f64::INFINITY;
4181            let mut max_val = f64::NEG_INFINITY;
4182            for &x in &data {
4183                if x < min_val { min_val = x; }
4184                if x > max_val { max_val = x; }
4185            }
4186            let range = max_val - min_val;
4187            let result: Vec<Value> = if range == 0.0 {
4188                // All values are the same — map to midpoint
4189                let mid = (low + high) / 2.0;
4190                data.iter().map(|_| Value::Float(mid)).collect()
4191            } else {
4192                data.iter().map(|&x| {
4193                    use cjc_repro::KahanAccumulatorF64;
4194                    let t = (x - min_val) / range;
4195                    let mut acc = KahanAccumulatorF64::new();
4196                    acc.add(low * (1.0 - t));
4197                    acc.add(high * t);
4198                    Value::Float(acc.finalize())
4199                }).collect()
4200            };
4201            Ok(Some(Value::Array(Rc::new(result))))
4202        }
4203
4204        "robust_scale" => {
4205            if args.len() != 1 { return Err("robust_scale requires 1 argument (array)".into()); }
4206            let data = value_to_f64_vec(&args[0])?;
4207            if data.is_empty() {
4208                return Ok(Some(Value::Array(Rc::new(vec![]))));
4209            }
4210            let med = crate::stats::median(&data)?;
4211            let iqr_val = crate::stats::iqr(&data)?;
4212            let result: Vec<Value> = if iqr_val == 0.0 {
4213                data.iter().map(|&x| Value::Float(x - med)).collect()
4214            } else {
4215                data.iter().map(|&x| Value::Float((x - med) / iqr_val)).collect()
4216            };
4217            Ok(Some(Value::Array(Rc::new(result))))
4218        }
4219
4220        // ── Categorical / Factor builtins ───────────────────────────────────
4221        "as_factor" => {
4222            // Convert a string array to a Factor struct: { levels: [String], codes: [Int] }
4223            if args.len() != 1 { return Err("as_factor requires 1 argument (string array)".into()); }
4224            let arr = match &args[0] {
4225                Value::Array(a) => a.as_ref().clone(),
4226                _ => return Err("as_factor: argument must be Array of strings".into()),
4227            };
4228            // Build levels in order of first appearance (deterministic)
4229            let mut levels: Vec<String> = Vec::new();
4230            let mut level_index = std::collections::BTreeMap::new();
4231            let mut codes: Vec<Value> = Vec::with_capacity(arr.len());
4232            for item in &arr {
4233                let s = match item {
4234                    Value::String(s) => s.as_str().to_string(),
4235                    _ => format!("{}", item),
4236                };
4237                let idx = if let Some(&idx) = level_index.get(&s) {
4238                    idx
4239                } else {
4240                    let idx = levels.len() as i64;
4241                    level_index.insert(s.clone(), idx);
4242                    levels.push(s);
4243                    idx
4244                };
4245                codes.push(Value::Int(idx));
4246            }
4247            let level_values: Vec<Value> = levels.into_iter()
4248                .map(|s| Value::String(Rc::new(s)))
4249                .collect();
4250            let mut fields = std::collections::BTreeMap::new();
4251            fields.insert("__type".to_string(), Value::String(Rc::new("Factor".to_string())));
4252            fields.insert("levels".to_string(), Value::Array(Rc::new(level_values)));
4253            fields.insert("codes".to_string(), Value::Array(Rc::new(codes)));
4254            Ok(Some(Value::Struct { name: "Factor".to_string(), fields }))
4255        }
4256
4257        "factor_levels" => {
4258            // Extract the levels array from a Factor struct
4259            if args.len() != 1 { return Err("factor_levels requires 1 argument (Factor)".into()); }
4260            match &args[0] {
4261                Value::Struct { name, fields } if name == "Factor" => {
4262                    match fields.get("levels") {
4263                        Some(v) => Ok(Some(v.clone())),
4264                        None => Err("factor_levels: Factor missing 'levels' field".into()),
4265                    }
4266                }
4267                _ => Err("factor_levels: argument must be a Factor".into()),
4268            }
4269        }
4270
4271        "factor_codes" => {
4272            // Extract the codes array from a Factor struct
4273            if args.len() != 1 { return Err("factor_codes requires 1 argument (Factor)".into()); }
4274            match &args[0] {
4275                Value::Struct { name, fields } if name == "Factor" => {
4276                    match fields.get("codes") {
4277                        Some(v) => Ok(Some(v.clone())),
4278                        None => Err("factor_codes: Factor missing 'codes' field".into()),
4279                    }
4280                }
4281                _ => Err("factor_codes: argument must be a Factor".into()),
4282            }
4283        }
4284
4285        "fct_relevel" => {
4286            // Reorder factor levels: fct_relevel(factor, new_order_array)
4287            if args.len() != 2 { return Err("fct_relevel requires 2 arguments (Factor, new_level_order)".into()); }
4288            let (old_levels, old_codes) = match &args[0] {
4289                Value::Struct { name, fields } if name == "Factor" => {
4290                    let levels = match fields.get("levels") {
4291                        Some(Value::Array(a)) => a.as_ref().clone(),
4292                        _ => return Err("fct_relevel: Factor missing 'levels'".into()),
4293                    };
4294                    let codes = match fields.get("codes") {
4295                        Some(Value::Array(a)) => a.as_ref().clone(),
4296                        _ => return Err("fct_relevel: Factor missing 'codes'".into()),
4297                    };
4298                    (levels, codes)
4299                }
4300                _ => return Err("fct_relevel: first argument must be a Factor".into()),
4301            };
4302            let new_order = match &args[1] {
4303                Value::Array(a) => a.as_ref().clone(),
4304                _ => return Err("fct_relevel: second argument must be array of level strings".into()),
4305            };
4306            // Build old level strings
4307            let old_strs: Vec<String> = old_levels.iter().map(|v| match v {
4308                Value::String(s) => s.as_str().to_string(),
4309                _ => format!("{}", v),
4310            }).collect();
4311            // Build new order strings
4312            let new_strs: Vec<String> = new_order.iter().map(|v| match v {
4313                Value::String(s) => s.as_str().to_string(),
4314                _ => format!("{}", v),
4315            }).collect();
4316            // Build mapping: old_index → new_index
4317            let mut remap = std::collections::BTreeMap::new();
4318            for (old_idx, s) in old_strs.iter().enumerate() {
4319                if let Some(new_idx) = new_strs.iter().position(|ns| ns == s) {
4320                    remap.insert(old_idx as i64, new_idx as i64);
4321                }
4322            }
4323            // Recode
4324            let new_codes: Vec<Value> = old_codes.iter().map(|v| {
4325                if let Value::Int(c) = v {
4326                    Value::Int(remap.get(c).copied().unwrap_or(*c))
4327                } else { v.clone() }
4328            }).collect();
4329            let new_level_values: Vec<Value> = new_strs.into_iter()
4330                .map(|s| Value::String(Rc::new(s)))
4331                .collect();
4332            let mut fields = std::collections::BTreeMap::new();
4333            fields.insert("__type".to_string(), Value::String(Rc::new("Factor".to_string())));
4334            fields.insert("levels".to_string(), Value::Array(Rc::new(new_level_values)));
4335            fields.insert("codes".to_string(), Value::Array(Rc::new(new_codes)));
4336            Ok(Some(Value::Struct { name: "Factor".to_string(), fields }))
4337        }
4338
4339        "fct_lump" => {
4340            // Lump rare factor levels into "Other": fct_lump(factor, n)
4341            // Keeps the top `n` most frequent levels, lumps rest into "Other"
4342            if args.len() != 2 { return Err("fct_lump requires 2 arguments (Factor, n)".into()); }
4343            let (old_levels, old_codes) = match &args[0] {
4344                Value::Struct { name, fields } if name == "Factor" => {
4345                    let levels = match fields.get("levels") {
4346                        Some(Value::Array(a)) => a.as_ref().clone(),
4347                        _ => return Err("fct_lump: Factor missing 'levels'".into()),
4348                    };
4349                    let codes = match fields.get("codes") {
4350                        Some(Value::Array(a)) => a.as_ref().clone(),
4351                        _ => return Err("fct_lump: Factor missing 'codes'".into()),
4352                    };
4353                    (levels, codes)
4354                }
4355                _ => return Err("fct_lump: first argument must be a Factor".into()),
4356            };
4357            let n = match &args[1] {
4358                Value::Int(n) => *n as usize,
4359                _ => return Err("fct_lump: second argument must be Int".into()),
4360            };
4361            // Count frequency of each code
4362            let mut freq = std::collections::BTreeMap::new();
4363            for v in &old_codes {
4364                if let Value::Int(c) = v { *freq.entry(*c).or_insert(0usize) += 1; }
4365            }
4366            // Sort by frequency descending (then by code for determinism)
4367            let mut freq_vec: Vec<(i64, usize)> = freq.into_iter().collect();
4368            freq_vec.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(&b.0)));
4369            // Top n codes to keep
4370            let keep_codes: std::collections::BTreeSet<i64> = freq_vec.iter().take(n).map(|(c, _)| *c).collect();
4371            // Build new levels: kept levels + "Other"
4372            let old_strs: Vec<String> = old_levels.iter().map(|v| match v {
4373                Value::String(s) => s.as_str().to_string(),
4374                _ => format!("{}", v),
4375            }).collect();
4376            let mut new_levels: Vec<String> = Vec::new();
4377            let mut code_remap = std::collections::BTreeMap::new();
4378            for (old_idx, s) in old_strs.iter().enumerate() {
4379                if keep_codes.contains(&(old_idx as i64)) {
4380                    let new_idx = new_levels.len() as i64;
4381                    code_remap.insert(old_idx as i64, new_idx);
4382                    new_levels.push(s.clone());
4383                }
4384            }
4385            let other_idx = new_levels.len() as i64;
4386            new_levels.push("Other".to_string());
4387            // Recode
4388            let new_codes: Vec<Value> = old_codes.iter().map(|v| {
4389                if let Value::Int(c) = v {
4390                    Value::Int(*code_remap.get(c).unwrap_or(&other_idx))
4391                } else { v.clone() }
4392            }).collect();
4393            let new_level_values: Vec<Value> = new_levels.into_iter()
4394                .map(|s| Value::String(Rc::new(s)))
4395                .collect();
4396            let mut fields = std::collections::BTreeMap::new();
4397            fields.insert("__type".to_string(), Value::String(Rc::new("Factor".to_string())));
4398            fields.insert("levels".to_string(), Value::Array(Rc::new(new_level_values)));
4399            fields.insert("codes".to_string(), Value::Array(Rc::new(new_codes)));
4400            Ok(Some(Value::Struct { name: "Factor".to_string(), fields }))
4401        }
4402
4403        "fct_count" => {
4404            // Count observations per level: returns array of (level, count) tuples
4405            if args.len() != 1 { return Err("fct_count requires 1 argument (Factor)".into()); }
4406            let (levels, codes) = match &args[0] {
4407                Value::Struct { name, fields } if name == "Factor" => {
4408                    let levels = match fields.get("levels") {
4409                        Some(Value::Array(a)) => a.as_ref().clone(),
4410                        _ => return Err("fct_count: Factor missing 'levels'".into()),
4411                    };
4412                    let codes = match fields.get("codes") {
4413                        Some(Value::Array(a)) => a.as_ref().clone(),
4414                        _ => return Err("fct_count: Factor missing 'codes'".into()),
4415                    };
4416                    (levels, codes)
4417                }
4418                _ => return Err("fct_count: argument must be a Factor".into()),
4419            };
4420            let mut freq = std::collections::BTreeMap::new();
4421            for v in &codes {
4422                if let Value::Int(c) = v { *freq.entry(*c).or_insert(0i64) += 1; }
4423            }
4424            let result: Vec<Value> = levels.iter().enumerate().map(|(i, lev)| {
4425                let count = freq.get(&(i as i64)).copied().unwrap_or(0);
4426                Value::Tuple(Rc::new(vec![lev.clone(), Value::Int(count)]))
4427            }).collect();
4428            Ok(Some(Value::Array(Rc::new(result))))
4429        }
4430
4431        // ── Normality Tests ──────────────────────────────────────────────────
4432        "jarque_bera" => {
4433            if args.len() != 1 { return Err("jarque_bera requires 1 argument (data array)".into()); }
4434            let data = value_to_f64_vec(&args[0])?;
4435            let r = crate::hypothesis::jarque_bera(&data)?;
4436            let mut fields = std::collections::BTreeMap::new();
4437            fields.insert("statistic".to_string(), Value::Float(r.statistic));
4438            fields.insert("p_value".to_string(), Value::Float(r.p_value));
4439            Ok(Some(Value::Struct { name: "JarqueBeraResult".to_string(), fields }))
4440        }
4441
4442        "anderson_darling" => {
4443            if args.len() != 1 { return Err("anderson_darling requires 1 argument (data array)".into()); }
4444            let data = value_to_f64_vec(&args[0])?;
4445            let r = crate::hypothesis::anderson_darling(&data)?;
4446            let mut fields = std::collections::BTreeMap::new();
4447            fields.insert("statistic".to_string(), Value::Float(r.statistic));
4448            fields.insert("p_value".to_string(), Value::Float(r.p_value));
4449            Ok(Some(Value::Struct { name: "AndersonDarlingResult".to_string(), fields }))
4450        }
4451
4452        "ks_test" => {
4453            if args.len() != 1 { return Err("ks_test requires 1 argument (data array)".into()); }
4454            let data = value_to_f64_vec(&args[0])?;
4455            let r = crate::hypothesis::ks_test_normal(&data)?;
4456            let mut fields = std::collections::BTreeMap::new();
4457            fields.insert("statistic".to_string(), Value::Float(r.statistic));
4458            fields.insert("p_value".to_string(), Value::Float(r.p_value));
4459            Ok(Some(Value::Struct { name: "KSResult".to_string(), fields }))
4460        }
4461
4462        // ── Effect Sizes ────────────────────────────────────────────────────
4463        "cohens_d" => {
4464            if args.len() != 2 { return Err("cohens_d requires 2 arguments (x, y)".into()); }
4465            let x = value_to_f64_vec(&args[0])?;
4466            let y = value_to_f64_vec(&args[1])?;
4467            let d = crate::hypothesis::cohens_d(&x, &y)?;
4468            Ok(Some(Value::Float(d)))
4469        }
4470
4471        "eta_squared" => {
4472            if args.len() < 2 { return Err("eta_squared requires at least 2 group arguments".into()); }
4473            let groups: Vec<Vec<f64>> = args.iter().map(|a| value_to_f64_vec(a)).collect::<Result<Vec<_>, _>>()?;
4474            let refs: Vec<&[f64]> = groups.iter().map(|g| g.as_slice()).collect();
4475            let es = crate::hypothesis::eta_squared(&refs)?;
4476            Ok(Some(Value::Float(es)))
4477        }
4478
4479        "cramers_v" => {
4480            // cramers_v(table_flat, nrows, ncols)
4481            if args.len() != 3 { return Err("cramers_v requires 3 arguments (table, nrows, ncols)".into()); }
4482            let table = value_to_f64_vec(&args[0])?;
4483            let nrows = match &args[1] { Value::Int(n) => *n as usize, _ => return Err("cramers_v: nrows must be Int".into()) };
4484            let ncols = match &args[2] { Value::Int(n) => *n as usize, _ => return Err("cramers_v: ncols must be Int".into()) };
4485            let v = crate::hypothesis::cramers_v(&table, nrows, ncols)?;
4486            Ok(Some(Value::Float(v)))
4487        }
4488
4489        // ── Variance Tests ──────────────────────────────────────────────────
4490        "levene_test" => {
4491            if args.len() < 2 { return Err("levene_test requires at least 2 group arguments".into()); }
4492            let groups: Vec<Vec<f64>> = args.iter().map(|a| value_to_f64_vec(a)).collect::<Result<Vec<_>, _>>()?;
4493            let refs: Vec<&[f64]> = groups.iter().map(|g| g.as_slice()).collect();
4494            let (w, p) = crate::hypothesis::levene_test(&refs)?;
4495            let mut fields = std::collections::BTreeMap::new();
4496            fields.insert("statistic".to_string(), Value::Float(w));
4497            fields.insert("p_value".to_string(), Value::Float(p));
4498            Ok(Some(Value::Struct { name: "LeveneResult".to_string(), fields }))
4499        }
4500
4501        "bartlett_test" => {
4502            if args.len() < 2 { return Err("bartlett_test requires at least 2 group arguments".into()); }
4503            let groups: Vec<Vec<f64>> = args.iter().map(|a| value_to_f64_vec(a)).collect::<Result<Vec<_>, _>>()?;
4504            let refs: Vec<&[f64]> = groups.iter().map(|g| g.as_slice()).collect();
4505            let (t, p) = crate::hypothesis::bartlett_test(&refs)?;
4506            let mut fields = std::collections::BTreeMap::new();
4507            fields.insert("statistic".to_string(), Value::Float(t));
4508            fields.insert("p_value".to_string(), Value::Float(p));
4509            Ok(Some(Value::Struct { name: "BartlettResult".to_string(), fields }))
4510        }
4511
4512        // ── Sampling & Cross-Validation builtins ────────────────────────────
4513        "latin_hypercube" => {
4514            // latin_hypercube(n, dims, seed) → Tensor [n, dims]
4515            if args.len() != 3 { return Err("latin_hypercube requires 3 arguments (n, dims, seed)".into()); }
4516            let n = match &args[0] { Value::Int(n) => *n as usize, _ => return Err("latin_hypercube: n must be Int".into()) };
4517            let dims = match &args[1] { Value::Int(d) => *d as usize, _ => return Err("latin_hypercube: dims must be Int".into()) };
4518            let seed = match &args[2] { Value::Int(s) => *s as u64, _ => return Err("latin_hypercube: seed must be Int".into()) };
4519            let t = crate::distributions::latin_hypercube_sample(n, dims, seed);
4520            Ok(Some(Value::Tensor(t)))
4521        }
4522
4523        "sobol_sequence" => {
4524            // sobol_sequence(n, dims) → Tensor [n, dims]
4525            if args.len() != 2 { return Err("sobol_sequence requires 2 arguments (n, dims)".into()); }
4526            let n = match &args[0] { Value::Int(n) => *n as usize, _ => return Err("sobol_sequence: n must be Int".into()) };
4527            let dims = match &args[1] { Value::Int(d) => *d as usize, _ => return Err("sobol_sequence: dims must be Int".into()) };
4528            let t = crate::distributions::sobol_sequence(n, dims);
4529            Ok(Some(Value::Tensor(t)))
4530        }
4531
4532        "train_test_split" => {
4533            // train_test_split(n, test_fraction, seed) → (train_indices, test_indices)
4534            if args.len() != 3 { return Err("train_test_split requires 3 arguments (n, test_fraction, seed)".into()); }
4535            let n = match &args[0] { Value::Int(n) => *n as usize, _ => return Err("train_test_split: n must be Int".into()) };
4536            let frac = match &args[1] { Value::Float(f) => *f, Value::Int(i) => *i as f64, _ => return Err("train_test_split: test_fraction must be Float".into()) };
4537            let seed = match &args[2] { Value::Int(s) => *s as u64, _ => return Err("train_test_split: seed must be Int".into()) };
4538            let (train, test) = crate::ml::train_test_split(n, frac, seed);
4539            let train_vals: Vec<Value> = train.into_iter().map(|i| Value::Int(i as i64)).collect();
4540            let test_vals: Vec<Value> = test.into_iter().map(|i| Value::Int(i as i64)).collect();
4541            Ok(Some(Value::Tuple(Rc::new(vec![
4542                Value::Array(Rc::new(train_vals)),
4543                Value::Array(Rc::new(test_vals)),
4544            ]))))
4545        }
4546
4547        "kfold_indices" => {
4548            // kfold_indices(n, k, seed) → array of (train_indices, test_indices) tuples
4549            if args.len() != 3 { return Err("kfold_indices requires 3 arguments (n, k, seed)".into()); }
4550            let n = match &args[0] { Value::Int(n) => *n as usize, _ => return Err("kfold_indices: n must be Int".into()) };
4551            let k = match &args[1] { Value::Int(k) => *k as usize, _ => return Err("kfold_indices: k must be Int".into()) };
4552            let seed = match &args[2] { Value::Int(s) => *s as u64, _ => return Err("kfold_indices: seed must be Int".into()) };
4553            let folds = crate::ml::kfold_indices(n, k, seed);
4554            let result: Vec<Value> = folds.into_iter().map(|(train, test)| {
4555                let train_vals: Vec<Value> = train.into_iter().map(|i| Value::Int(i as i64)).collect();
4556                let test_vals: Vec<Value> = test.into_iter().map(|i| Value::Int(i as i64)).collect();
4557                Value::Tuple(Rc::new(vec![
4558                    Value::Array(Rc::new(train_vals)),
4559                    Value::Array(Rc::new(test_vals)),
4560                ]))
4561            }).collect();
4562            Ok(Some(Value::Array(Rc::new(result))))
4563        }
4564
4565        "bootstrap" => {
4566            // bootstrap(data, n_resamples, stat_fn, seed) → Struct { point, ci_lower, ci_upper, se }
4567            // stat_fn: 0=mean, 1=median
4568            if args.len() != 4 { return Err("bootstrap requires 4 arguments (data, n_resamples, stat_fn, seed)".into()); }
4569            let data = match &args[0] {
4570                Value::Array(a) => {
4571                    let mut v = Vec::with_capacity(a.len());
4572                    for val in a.iter() {
4573                        match val {
4574                            Value::Float(f) => v.push(*f),
4575                            Value::Int(i) => v.push(*i as f64),
4576                            _ => return Err("bootstrap: data elements must be numeric".into()),
4577                        }
4578                    }
4579                    v
4580                }
4581                _ => return Err("bootstrap: data must be an array".into()),
4582            };
4583            let n_resamples = match &args[1] { Value::Int(n) => *n as usize, _ => return Err("bootstrap: n_resamples must be Int".into()) };
4584            let stat_fn = match &args[2] { Value::Int(s) => *s as usize, _ => return Err("bootstrap: stat_fn must be Int (0=mean, 1=median)".into()) };
4585            let seed = match &args[3] { Value::Int(s) => *s as u64, _ => return Err("bootstrap: seed must be Int".into()) };
4586            let (point, ci_lower, ci_upper, se) = crate::ml::bootstrap(&data, n_resamples, stat_fn, seed)?;
4587            let mut fields = std::collections::BTreeMap::new();
4588            fields.insert("point".to_string(), Value::Float(point));
4589            fields.insert("ci_lower".to_string(), Value::Float(ci_lower));
4590            fields.insert("ci_upper".to_string(), Value::Float(ci_upper));
4591            fields.insert("se".to_string(), Value::Float(se));
4592            Ok(Some(Value::Struct {
4593                name: "BootstrapResult".into(),
4594                fields,
4595            }))
4596        }
4597        "permutation_test" => {
4598            // permutation_test(x, y, n_perms, seed) → Struct { observed_diff, p_value }
4599            if args.len() != 4 { return Err("permutation_test requires 4 arguments (x, y, n_perms, seed)".into()); }
4600            let extract_floats = |val: &Value, name: &str| -> Result<Vec<f64>, String> {
4601                match val {
4602                    Value::Array(a) => {
4603                        let mut v = Vec::with_capacity(a.len());
4604                        for el in a.iter() {
4605                            match el {
4606                                Value::Float(f) => v.push(*f),
4607                                Value::Int(i) => v.push(*i as f64),
4608                                _ => return Err(format!("permutation_test: {} elements must be numeric", name)),
4609                            }
4610                        }
4611                        Ok(v)
4612                    }
4613                    _ => Err(format!("permutation_test: {} must be an array", name)),
4614                }
4615            };
4616            let x = extract_floats(&args[0], "x")?;
4617            let y = extract_floats(&args[1], "y")?;
4618            let n_perms = match &args[2] { Value::Int(n) => *n as usize, _ => return Err("permutation_test: n_perms must be Int".into()) };
4619            let seed = match &args[3] { Value::Int(s) => *s as u64, _ => return Err("permutation_test: seed must be Int".into()) };
4620            let (observed, p_value) = crate::ml::permutation_test(&x, &y, n_perms, seed)?;
4621            let mut fields = std::collections::BTreeMap::new();
4622            fields.insert("observed_diff".to_string(), Value::Float(observed));
4623            fields.insert("p_value".to_string(), Value::Float(p_value));
4624            Ok(Some(Value::Struct {
4625                name: "PermutationResult".into(),
4626                fields,
4627            }))
4628        }
4629
4630        "stratified_split" => {
4631            // stratified_split(labels, test_fraction, seed) → (train_indices, test_indices)
4632            if args.len() != 3 { return Err("stratified_split requires 3 arguments (labels, test_fraction, seed)".into()); }
4633            let labels = match &args[0] {
4634                Value::Array(a) => {
4635                    let mut v = Vec::with_capacity(a.len());
4636                    for val in a.iter() {
4637                        match val {
4638                            Value::Int(i) => v.push(*i),
4639                            _ => return Err("stratified_split: labels must be integer array".into()),
4640                        }
4641                    }
4642                    v
4643                }
4644                _ => return Err("stratified_split: labels must be an array".into()),
4645            };
4646            let frac = match &args[1] { Value::Float(f) => *f, Value::Int(i) => *i as f64, _ => return Err("stratified_split: test_fraction must be Float".into()) };
4647            let seed = match &args[2] { Value::Int(s) => *s as u64, _ => return Err("stratified_split: seed must be Int".into()) };
4648            let (train, test) = crate::ml::stratified_split(&labels, frac, seed);
4649            let train_vals: Vec<Value> = train.into_iter().map(|i| Value::Int(i as i64)).collect();
4650            let test_vals: Vec<Value> = test.into_iter().map(|i| Value::Int(i as i64)).collect();
4651            Ok(Some(Value::Tuple(Rc::new(vec![
4652                Value::Array(Rc::new(train_vals)),
4653                Value::Array(Rc::new(test_vals)),
4654            ]))))
4655        }
4656
4657        // ── Extended Date/Time builtins ────────────────────────────────
4658        "parse_date" => {
4659            if args.len() != 2 { return Err("parse_date requires 2 arguments (string, format)".into()); }
4660            let s = match &args[0] { Value::String(s) => s.as_str().to_string(), _ => return Err("parse_date: first arg must be String".into()) };
4661            let fmt = match &args[1] { Value::String(s) => s.as_str().to_string(), _ => return Err("parse_date: second arg must be String".into()) };
4662            Ok(Some(Value::Int(crate::datetime::parse_date(&s, &fmt))))
4663        }
4664        "date_format" => {
4665            if args.len() != 2 { return Err("date_format requires 2 arguments (timestamp, format)".into()); }
4666            let ts = match &args[0] { Value::Int(v) => *v, Value::Float(v) => *v as i64, _ => return Err("date_format: first arg must be numeric".into()) };
4667            let fmt = match &args[1] { Value::String(s) => s.as_str().to_string(), _ => return Err("date_format: second arg must be String".into()) };
4668            Ok(Some(Value::String(Rc::new(crate::datetime::date_format_custom(ts, &fmt)))))
4669        }
4670        "year" => {
4671            if args.len() != 1 { return Err("year requires 1 argument (timestamp)".into()); }
4672            let ts = match &args[0] { Value::Int(v) => *v, Value::Float(v) => *v as i64, _ => return Err("year: arg must be numeric".into()) };
4673            Ok(Some(Value::Int(crate::datetime::datetime_year(ts))))
4674        }
4675        "month" => {
4676            if args.len() != 1 { return Err("month requires 1 argument (timestamp)".into()); }
4677            let ts = match &args[0] { Value::Int(v) => *v, Value::Float(v) => *v as i64, _ => return Err("month: arg must be numeric".into()) };
4678            Ok(Some(Value::Int(crate::datetime::datetime_month(ts))))
4679        }
4680        "day" => {
4681            if args.len() != 1 { return Err("day requires 1 argument (timestamp)".into()); }
4682            let ts = match &args[0] { Value::Int(v) => *v, Value::Float(v) => *v as i64, _ => return Err("day: arg must be numeric".into()) };
4683            Ok(Some(Value::Int(crate::datetime::datetime_day(ts))))
4684        }
4685        "hour" => {
4686            if args.len() != 1 { return Err("hour requires 1 argument (timestamp)".into()); }
4687            let ts = match &args[0] { Value::Int(v) => *v, Value::Float(v) => *v as i64, _ => return Err("hour: arg must be numeric".into()) };
4688            Ok(Some(Value::Int(crate::datetime::datetime_hour(ts))))
4689        }
4690        "minute" => {
4691            if args.len() != 1 { return Err("minute requires 1 argument (timestamp)".into()); }
4692            let ts = match &args[0] { Value::Int(v) => *v, Value::Float(v) => *v as i64, _ => return Err("minute: arg must be numeric".into()) };
4693            Ok(Some(Value::Int(crate::datetime::datetime_minute(ts))))
4694        }
4695        "second" => {
4696            if args.len() != 1 { return Err("second requires 1 argument (timestamp)".into()); }
4697            let ts = match &args[0] { Value::Int(v) => *v, Value::Float(v) => *v as i64, _ => return Err("second: arg must be numeric".into()) };
4698            Ok(Some(Value::Int(crate::datetime::datetime_second(ts))))
4699        }
4700        "date_diff" => {
4701            if args.len() != 3 { return Err("date_diff requires 3 arguments (ts1, ts2, unit)".into()); }
4702            let ts1 = match &args[0] { Value::Int(v) => *v, Value::Float(v) => *v as i64, _ => return Err("date_diff: first arg must be numeric".into()) };
4703            let ts2 = match &args[1] { Value::Int(v) => *v, Value::Float(v) => *v as i64, _ => return Err("date_diff: second arg must be numeric".into()) };
4704            let unit = match &args[2] { Value::String(s) => s.as_str().to_string(), _ => return Err("date_diff: third arg must be String".into()) };
4705            match crate::datetime::date_diff_units(ts1, ts2, &unit) {
4706                Ok(v) => Ok(Some(Value::Int(v))),
4707                Err(e) => Err(e),
4708            }
4709        }
4710        "date_add" => {
4711            if args.len() != 3 { return Err("date_add requires 3 arguments (ts, amount, unit)".into()); }
4712            let ts = match &args[0] { Value::Int(v) => *v, Value::Float(v) => *v as i64, _ => return Err("date_add: first arg must be numeric".into()) };
4713            let amount = match &args[1] { Value::Int(v) => *v, Value::Float(v) => *v as i64, _ => return Err("date_add: second arg must be numeric".into()) };
4714            let unit = match &args[2] { Value::String(s) => s.as_str().to_string(), _ => return Err("date_add: third arg must be String".into()) };
4715            match crate::datetime::date_add_units(ts, amount, &unit) {
4716                Ok(v) => Ok(Some(Value::Int(v))),
4717                Err(e) => Err(e),
4718            }
4719        }
4720        "fill_na" => {
4721            // Alias for fillna
4722            if args.len() != 2 { return Err("fill_na requires 2 arguments (array, fill_value)".into()); }
4723            let arr = match &args[0] {
4724                Value::Array(a) => a.as_ref().clone(),
4725                _ => return Err("fill_na: first arg must be Array".into()),
4726            };
4727            let fill = &args[1];
4728            let result: Vec<Value> = arr.iter().map(|v| {
4729                if matches!(v, Value::Na) { fill.clone() } else { v.clone() }
4730            }).collect();
4731            Ok(Some(Value::Array(Rc::new(result))))
4732        }
4733
4734        // ── Regularized regression builtins ──────────────────────────────
4735        "ridge_regression" => {
4736            dispatch_ridge_regression(&args)
4737        }
4738        "lasso_regression" => {
4739            dispatch_lasso_regression(&args)
4740        }
4741        "elastic_net" => {
4742            dispatch_elastic_net(&args)
4743        }
4744
4745        _ => Ok(None), // Not a shared builtin
4746    }
4747}
4748
4749/// Extract a SparseCsr reference from a Value.
4750fn value_to_sparse(val: &Value) -> Result<&crate::sparse::SparseCsr, String> {
4751    match val {
4752        Value::SparseTensor(s) => Ok(s),
4753        _ => Err(format!("expected SparseTensor, got {}", val.type_name())),
4754    }
4755}
4756
4757// ---------------------------------------------------------------------------
4758// Peak RSS memory tracking (platform-specific)
4759// ---------------------------------------------------------------------------
4760
4761/// Returns peak resident set size in kilobytes.
4762///
4763/// Platform support:
4764/// - **Windows**: `GetProcessMemoryInfo` → `PeakWorkingSetSize`
4765/// - **Linux**: Reads `/proc/self/status` → `VmHWM`
4766/// - **macOS**: `getrusage(RUSAGE_SELF)` → `ru_maxrss` (in bytes on macOS)
4767/// - **Other**: Returns 0
4768pub fn peak_rss_kb() -> u64 {
4769    #[cfg(target_os = "windows")]
4770    {
4771        peak_rss_windows()
4772    }
4773    #[cfg(target_os = "linux")]
4774    {
4775        peak_rss_linux()
4776    }
4777    #[cfg(target_os = "macos")]
4778    {
4779        peak_rss_macos()
4780    }
4781    #[cfg(not(any(target_os = "windows", target_os = "linux", target_os = "macos")))]
4782    {
4783        0
4784    }
4785}
4786
4787#[cfg(target_os = "windows")]
4788fn peak_rss_windows() -> u64 {
4789    use std::mem::{size_of, MaybeUninit};
4790
4791    #[repr(C)]
4792    #[allow(non_snake_case)]
4793    struct ProcessMemoryCounters {
4794        cb: u32,
4795        PageFaultCount: u32,
4796        PeakWorkingSetSize: usize,
4797        WorkingSetSize: usize,
4798        QuotaPeakPagedPoolUsage: usize,
4799        QuotaPagedPoolUsage: usize,
4800        QuotaPeakNonPagedPoolUsage: usize,
4801        QuotaNonPagedPoolUsage: usize,
4802        PagefileUsage: usize,
4803        PeakPagefileUsage: usize,
4804    }
4805
4806    extern "system" {
4807        fn GetCurrentProcess() -> isize;
4808        fn K32GetProcessMemoryInfo(
4809            hProcess: isize,
4810            ppsmemCounters: *mut ProcessMemoryCounters,
4811            cb: u32,
4812        ) -> i32;
4813    }
4814
4815    unsafe {
4816        let mut pmc = MaybeUninit::<ProcessMemoryCounters>::zeroed();
4817        let pmc_ref = pmc.as_mut_ptr();
4818        (*pmc_ref).cb = size_of::<ProcessMemoryCounters>() as u32;
4819        let handle = GetCurrentProcess();
4820        if K32GetProcessMemoryInfo(handle, pmc_ref, (*pmc_ref).cb) != 0 {
4821            let pmc = pmc.assume_init();
4822            (pmc.PeakWorkingSetSize / 1024) as u64
4823        } else {
4824            0
4825        }
4826    }
4827}
4828
4829#[cfg(target_os = "linux")]
4830fn peak_rss_linux() -> u64 {
4831    // Read VmHWM from /proc/self/status (peak resident set size in kB).
4832    if let Ok(contents) = std::fs::read_to_string("/proc/self/status") {
4833        for line in contents.lines() {
4834            if line.starts_with("VmHWM:") {
4835                let parts: Vec<&str> = line.split_whitespace().collect();
4836                if parts.len() >= 2 {
4837                    if let Ok(kb) = parts[1].parse::<u64>() {
4838                        return kb;
4839                    }
4840                }
4841            }
4842        }
4843    }
4844    0
4845}
4846
4847#[cfg(target_os = "macos")]
4848fn peak_rss_macos() -> u64 {
4849    #[repr(C)]
4850    struct Rusage {
4851        ru_utime: [i64; 2],  // timeval (tv_sec, tv_usec)
4852        ru_stime: [i64; 2],  // timeval
4853        ru_maxrss: i64,      // max resident set size (bytes on macOS)
4854        // ... remaining fields omitted (we only need maxrss)
4855        _padding: [i64; 11],
4856    }
4857
4858    extern "C" {
4859        fn getrusage(who: i32, usage: *mut Rusage) -> i32;
4860    }
4861
4862    unsafe {
4863        let mut usage = std::mem::MaybeUninit::<Rusage>::zeroed();
4864        if getrusage(0 /* RUSAGE_SELF */, usage.as_mut_ptr()) == 0 {
4865            let usage = usage.assume_init();
4866            // macOS reports in bytes, convert to KB
4867            (usage.ru_maxrss as u64) / 1024
4868        } else {
4869            0
4870        }
4871    }
4872}
4873
4874// ---------------------------------------------------------------------------
4875// Regularized regression (ridge, lasso, elastic_net)
4876// ---------------------------------------------------------------------------
4877
4878/// Extract a 2D tensor (n_samples x n_features) from a Value.
4879fn regression_extract_x(val: &Value) -> Result<(Vec<f64>, usize, usize), String> {
4880    let t = value_to_tensor(val)?;
4881    let shape = t.shape();
4882    if shape.len() != 2 {
4883        return Err(format!("regression: X must be a 2D tensor, got {}D", shape.len()));
4884    }
4885    let n = shape[0];
4886    let p = shape[1];
4887    Ok((t.to_vec(), n, p))
4888}
4889
4890/// Extract a 1D tensor (n_samples) from a Value.
4891fn regression_extract_y(val: &Value) -> Result<Vec<f64>, String> {
4892    let t = value_to_tensor(val)?;
4893    let shape = t.shape();
4894    if shape.len() != 1 {
4895        return Err(format!("regression: y must be a 1D tensor, got {}D", shape.len()));
4896    }
4897    Ok(t.to_vec())
4898}
4899
4900/// Standardize X columns: return (standardized_data, means, stds).
4901/// Columns with std=0 get std=1 (no scaling).
4902fn standardize_x(data: &[f64], n: usize, p: usize) -> (Vec<f64>, Vec<f64>, Vec<f64>) {
4903    let mut means = vec![0.0; p];
4904    let mut stds = vec![0.0; p];
4905
4906    // Compute column means
4907    for j in 0..p {
4908        let mut acc = cjc_repro::KahanAccumulatorF64::new();
4909        for i in 0..n {
4910            acc.add(data[i * p + j]);
4911        }
4912        means[j] = acc.finalize() / n as f64;
4913    }
4914
4915    // Compute column stds
4916    for j in 0..p {
4917        let mut acc = cjc_repro::KahanAccumulatorF64::new();
4918        for i in 0..n {
4919            let d = data[i * p + j] - means[j];
4920            acc.add(d * d);
4921        }
4922        let variance = acc.finalize() / n as f64;
4923        stds[j] = if variance > 0.0 { variance.sqrt() } else { 1.0 };
4924    }
4925
4926    // Standardize
4927    let mut standardized = vec![0.0; n * p];
4928    for i in 0..n {
4929        for j in 0..p {
4930            standardized[i * p + j] = (data[i * p + j] - means[j]) / stds[j];
4931        }
4932    }
4933
4934    (standardized, means, stds)
4935}
4936
4937/// Compute R-squared given y and predictions.
4938fn compute_r_squared(y: &[f64], predictions: &[f64]) -> f64 {
4939    let n = y.len();
4940    let mut y_acc = cjc_repro::KahanAccumulatorF64::new();
4941    for &v in y { y_acc.add(v); }
4942    let y_mean = y_acc.finalize() / n as f64;
4943
4944    let mut ss_res = cjc_repro::KahanAccumulatorF64::new();
4945    let mut ss_tot = cjc_repro::KahanAccumulatorF64::new();
4946    for i in 0..n {
4947        let r = y[i] - predictions[i];
4948        ss_res.add(r * r);
4949        let t = y[i] - y_mean;
4950        ss_tot.add(t * t);
4951    }
4952    if ss_tot.finalize() == 0.0 { 1.0 } else { 1.0 - ss_res.finalize() / ss_tot.finalize() }
4953}
4954
4955/// Build the result struct for regression.
4956fn regression_result_struct(
4957    name: &str,
4958    coefficients: Vec<f64>,
4959    intercept: f64,
4960    r_squared: f64,
4961    alpha: f64,
4962    n_iter: Option<i64>,
4963    converged: Option<bool>,
4964) -> Value {
4965    let p = coefficients.len();
4966    let coef_tensor = Tensor::from_vec(coefficients, &[p]).unwrap();
4967    let mut fields = std::collections::BTreeMap::new();
4968    fields.insert("coefficients".into(), Value::Tensor(coef_tensor));
4969    fields.insert("intercept".into(), Value::Float(intercept));
4970    fields.insert("r_squared".into(), Value::Float(r_squared));
4971    fields.insert("alpha".into(), Value::Float(alpha));
4972    if let Some(ni) = n_iter {
4973        fields.insert("n_iter".into(), Value::Int(ni));
4974    }
4975    if let Some(conv) = converged {
4976        fields.insert("converged".into(), Value::Bool(conv));
4977    }
4978    Value::Struct { name: name.to_string(), fields }
4979}
4980
4981/// Ridge regression: closed-form (XtX + alpha*I)^-1 Xt y.
4982fn dispatch_ridge_regression(args: &[Value]) -> Result<Option<Value>, String> {
4983    if args.len() != 3 {
4984        return Err("ridge_regression requires 3 arguments (X, y, alpha)".into());
4985    }
4986    let (x_data, n, p) = regression_extract_x(&args[0])?;
4987    let y = regression_extract_y(&args[1])?;
4988    if y.len() != n {
4989        return Err(format!("ridge_regression: X has {} rows but y has {} elements", n, y.len()));
4990    }
4991    let alpha = match &args[2] {
4992        Value::Float(v) => *v,
4993        Value::Int(v) => *v as f64,
4994        _ => return Err("ridge_regression: alpha must be numeric".into()),
4995    };
4996
4997    // Standardize X
4998    let (xs, means, stds) = standardize_x(&x_data, n, p);
4999
5000    // Compute y_mean and center y
5001    let mut y_acc = cjc_repro::KahanAccumulatorF64::new();
5002    for &v in &y { y_acc.add(v); }
5003    let y_mean = y_acc.finalize() / n as f64;
5004    let yc: Vec<f64> = y.iter().map(|v| v - y_mean).collect();
5005
5006    // Compute XtX + alpha*I (p x p)
5007    let mut xtx = vec![0.0; p * p];
5008    for j1 in 0..p {
5009        for j2 in j1..p {
5010            let mut acc = cjc_repro::KahanAccumulatorF64::new();
5011            for i in 0..n {
5012                acc.add(xs[i * p + j1] * xs[i * p + j2]);
5013            }
5014            xtx[j1 * p + j2] = acc.finalize();
5015            xtx[j2 * p + j1] = acc.finalize();
5016        }
5017        xtx[j1 * p + j1] += alpha;
5018    }
5019
5020    // Compute Xt * yc (p x 1)
5021    let mut xty = vec![0.0; p];
5022    for j in 0..p {
5023        let mut acc = cjc_repro::KahanAccumulatorF64::new();
5024        for i in 0..n {
5025            acc.add(xs[i * p + j] * yc[i]);
5026        }
5027        xty[j] = acc.finalize();
5028    }
5029
5030    // Solve (XtX + alpha*I) * beta = Xty via Cholesky or direct solve
5031    let beta_std = solve_linear_system(&xtx, &xty, p)?;
5032
5033    // Convert back to original scale
5034    let mut coefficients = vec![0.0; p];
5035    let mut intercept = y_mean;
5036    for j in 0..p {
5037        coefficients[j] = beta_std[j] / stds[j];
5038        intercept -= coefficients[j] * means[j];
5039    }
5040
5041    // Compute predictions and R-squared
5042    let mut predictions = vec![0.0; n];
5043    for i in 0..n {
5044        let mut acc = cjc_repro::KahanAccumulatorF64::new();
5045        acc.add(intercept);
5046        for j in 0..p {
5047            acc.add(coefficients[j] * x_data[i * p + j]);
5048        }
5049        predictions[i] = acc.finalize();
5050    }
5051    let r_squared = compute_r_squared(&y, &predictions);
5052
5053    Ok(Some(regression_result_struct("RidgeResult", coefficients, intercept, r_squared, alpha, None, None)))
5054}
5055
5056/// Lasso regression via coordinate descent.
5057fn dispatch_lasso_regression(args: &[Value]) -> Result<Option<Value>, String> {
5058    if args.len() < 3 || args.len() > 5 {
5059        return Err("lasso_regression requires 3-5 arguments (X, y, alpha, [max_iter], [tol])".into());
5060    }
5061    let (x_data, n, p) = regression_extract_x(&args[0])?;
5062    let y = regression_extract_y(&args[1])?;
5063    if y.len() != n {
5064        return Err(format!("lasso_regression: X has {} rows but y has {} elements", n, y.len()));
5065    }
5066    let alpha = match &args[2] {
5067        Value::Float(v) => *v,
5068        Value::Int(v) => *v as f64,
5069        _ => return Err("lasso_regression: alpha must be numeric".into()),
5070    };
5071    let max_iter = if args.len() > 3 {
5072        match &args[3] { Value::Int(v) => *v as usize, Value::Float(v) => *v as usize, _ => 1000 }
5073    } else { 1000 };
5074    let tol = if args.len() > 4 {
5075        match &args[4] { Value::Float(v) => *v, Value::Int(v) => *v as f64, _ => 1e-4 }
5076    } else { 1e-4 };
5077
5078    let (coefficients, intercept, n_iter, converged) =
5079        coordinate_descent(&x_data, &y, n, p, alpha, 1.0, max_iter, tol);
5080
5081    // Compute predictions and R-squared
5082    let mut predictions = vec![0.0; n];
5083    for i in 0..n {
5084        let mut acc = cjc_repro::KahanAccumulatorF64::new();
5085        acc.add(intercept);
5086        for j in 0..p {
5087            acc.add(coefficients[j] * x_data[i * p + j]);
5088        }
5089        predictions[i] = acc.finalize();
5090    }
5091    let r_squared = compute_r_squared(&y, &predictions);
5092
5093    Ok(Some(regression_result_struct("LassoResult", coefficients, intercept, r_squared, alpha, Some(n_iter as i64), Some(converged))))
5094}
5095
5096/// ElasticNet regression via coordinate descent.
5097fn dispatch_elastic_net(args: &[Value]) -> Result<Option<Value>, String> {
5098    if args.len() < 4 || args.len() > 6 {
5099        return Err("elastic_net requires 4-6 arguments (X, y, alpha, l1_ratio, [max_iter], [tol])".into());
5100    }
5101    let (x_data, n, p) = regression_extract_x(&args[0])?;
5102    let y = regression_extract_y(&args[1])?;
5103    if y.len() != n {
5104        return Err(format!("elastic_net: X has {} rows but y has {} elements", n, y.len()));
5105    }
5106    let alpha = match &args[2] {
5107        Value::Float(v) => *v,
5108        Value::Int(v) => *v as f64,
5109        _ => return Err("elastic_net: alpha must be numeric".into()),
5110    };
5111    let l1_ratio = match &args[3] {
5112        Value::Float(v) => *v,
5113        Value::Int(v) => *v as f64,
5114        _ => return Err("elastic_net: l1_ratio must be numeric".into()),
5115    };
5116    let max_iter = if args.len() > 4 {
5117        match &args[4] { Value::Int(v) => *v as usize, Value::Float(v) => *v as usize, _ => 1000 }
5118    } else { 1000 };
5119    let tol = if args.len() > 5 {
5120        match &args[5] { Value::Float(v) => *v, Value::Int(v) => *v as f64, _ => 1e-4 }
5121    } else { 1e-4 };
5122
5123    let (coefficients, intercept, n_iter, converged) =
5124        coordinate_descent(&x_data, &y, n, p, alpha, l1_ratio, max_iter, tol);
5125
5126    // Compute predictions and R-squared
5127    let mut predictions = vec![0.0; n];
5128    for i in 0..n {
5129        let mut acc = cjc_repro::KahanAccumulatorF64::new();
5130        acc.add(intercept);
5131        for j in 0..p {
5132            acc.add(coefficients[j] * x_data[i * p + j]);
5133        }
5134        predictions[i] = acc.finalize();
5135    }
5136    let r_squared = compute_r_squared(&y, &predictions);
5137
5138    let mut fields = std::collections::BTreeMap::new();
5139    fields.insert("coefficients".into(), Value::Tensor(Tensor::from_vec(coefficients, &[p]).unwrap()));
5140    fields.insert("intercept".into(), Value::Float(intercept));
5141    fields.insert("r_squared".into(), Value::Float(r_squared));
5142    fields.insert("alpha".into(), Value::Float(alpha));
5143    fields.insert("l1_ratio".into(), Value::Float(l1_ratio));
5144    fields.insert("n_iter".into(), Value::Int(n_iter as i64));
5145    fields.insert("converged".into(), Value::Bool(converged));
5146
5147    Ok(Some(Value::Struct { name: "ElasticNetResult".to_string(), fields }))
5148}
5149
5150/// Coordinate descent for Lasso / ElasticNet.
5151///
5152/// Minimizes: 0.5/n * ||y - Xb - b0||^2 + alpha * l1_ratio * ||b||_1
5153///          + 0.5 * alpha * (1-l1_ratio) * ||b||_2^2
5154///
5155/// Returns (coefficients_in_original_scale, intercept, n_iter, converged).
5156fn coordinate_descent(
5157    x_data: &[f64], y: &[f64], n: usize, p: usize,
5158    alpha: f64, l1_ratio: f64, max_iter: usize, tol: f64,
5159) -> (Vec<f64>, f64, usize, bool) {
5160    // Standardize X
5161    let (xs, means, stds) = standardize_x(x_data, n, p);
5162
5163    // Center y
5164    let mut y_acc = cjc_repro::KahanAccumulatorF64::new();
5165    for &v in y { y_acc.add(v); }
5166    let y_mean = y_acc.finalize() / n as f64;
5167    let yc: Vec<f64> = y.iter().map(|v| v - y_mean).collect();
5168
5169    // Precompute X^T columns norms (each is n because standardized)
5170    // but we still compute for correctness with numerical precision
5171    let mut col_norms_sq = vec![0.0; p];
5172    for j in 0..p {
5173        let mut acc = cjc_repro::KahanAccumulatorF64::new();
5174        for i in 0..n {
5175            let v = xs[i * p + j];
5176            acc.add(v * v);
5177        }
5178        col_norms_sq[j] = acc.finalize();
5179    }
5180
5181    let l1_pen = alpha * l1_ratio * n as f64;
5182    let l2_pen = alpha * (1.0 - l1_ratio) * n as f64;
5183
5184    let mut beta = vec![0.0; p];
5185    let mut residual = yc.clone();
5186    let mut converged = false;
5187    let mut n_iter = 0usize;
5188
5189    for iter in 0..max_iter {
5190        let mut max_delta: f64 = 0.0;
5191        for j in 0..p {
5192            let old_beta = beta[j];
5193
5194            // Compute partial residual correlation
5195            let mut acc = cjc_repro::KahanAccumulatorF64::new();
5196            for i in 0..n {
5197                acc.add(xs[i * p + j] * residual[i]);
5198            }
5199            let rho = acc.finalize() + col_norms_sq[j] * old_beta;
5200
5201            // Soft-thresholding
5202            let new_beta = soft_threshold(rho, l1_pen) / (col_norms_sq[j] + l2_pen);
5203
5204            // Update residual
5205            let delta = new_beta - old_beta;
5206            if delta != 0.0 {
5207                for i in 0..n {
5208                    residual[i] -= xs[i * p + j] * delta;
5209                }
5210            }
5211            beta[j] = new_beta;
5212            let abs_delta = delta.abs();
5213            if abs_delta > max_delta { max_delta = abs_delta; }
5214        }
5215        n_iter = iter + 1;
5216        if max_delta < tol {
5217            converged = true;
5218            break;
5219        }
5220    }
5221
5222    // Convert back to original scale
5223    let mut coefficients = vec![0.0; p];
5224    let mut intercept = y_mean;
5225    for j in 0..p {
5226        coefficients[j] = beta[j] / stds[j];
5227        intercept -= coefficients[j] * means[j];
5228    }
5229
5230    (coefficients, intercept, n_iter, converged)
5231}
5232
5233/// Soft-thresholding operator for Lasso/ElasticNet.
5234fn soft_threshold(rho: f64, lambda: f64) -> f64 {
5235    if rho > lambda {
5236        rho - lambda
5237    } else if rho < -lambda {
5238        rho + lambda
5239    } else {
5240        0.0
5241    }
5242}
5243
5244/// Solve a positive-definite linear system A*x = b via Cholesky decomposition.
5245/// A is p x p stored row-major, b is p x 1.
5246fn solve_linear_system(a: &[f64], b: &[f64], p: usize) -> Result<Vec<f64>, String> {
5247    // Cholesky decomposition: A = L * L^T
5248    let mut l = vec![0.0; p * p];
5249    for j in 0..p {
5250        let mut sum = cjc_repro::KahanAccumulatorF64::new();
5251        for k in 0..j {
5252            sum.add(l[j * p + k] * l[j * p + k]);
5253        }
5254        let diag = a[j * p + j] - sum.finalize();
5255        if diag <= 0.0 {
5256            return Err("ridge_regression: matrix not positive definite (try increasing alpha)".into());
5257        }
5258        l[j * p + j] = diag.sqrt();
5259        for i in (j + 1)..p {
5260            let mut sum2 = cjc_repro::KahanAccumulatorF64::new();
5261            for k in 0..j {
5262                sum2.add(l[i * p + k] * l[j * p + k]);
5263            }
5264            l[i * p + j] = (a[i * p + j] - sum2.finalize()) / l[j * p + j];
5265        }
5266    }
5267
5268    // Forward substitution: L * z = b
5269    let mut z = vec![0.0; p];
5270    for i in 0..p {
5271        let mut acc = cjc_repro::KahanAccumulatorF64::new();
5272        for k in 0..i {
5273            acc.add(l[i * p + k] * z[k]);
5274        }
5275        z[i] = (b[i] - acc.finalize()) / l[i * p + i];
5276    }
5277
5278    // Back substitution: L^T * x = z
5279    let mut x = vec![0.0; p];
5280    for i in (0..p).rev() {
5281        let mut acc = cjc_repro::KahanAccumulatorF64::new();
5282        for k in (i + 1)..p {
5283            acc.add(l[k * p + i] * x[k]);
5284        }
5285        x[i] = (z[i] - acc.finalize()) / l[i * p + i];
5286    }
5287
5288    Ok(x)
5289}
5290
5291// ---------------------------------------------------------------------------
5292// Tests
5293// ---------------------------------------------------------------------------
5294
5295#[cfg(test)]
5296mod tests {
5297    use super::*;
5298
5299    #[test]
5300    fn test_peak_rss_returns_nonzero() {
5301        let rss = peak_rss_kb();
5302        // On any platform we support, peak RSS should be > 0 for a running process.
5303        assert!(rss > 0, "peak_rss_kb() should return non-zero, got {rss}");
5304    }
5305
5306    #[test]
5307    fn test_peak_rss_builtin_dispatch() {
5308        let result = dispatch_builtin("peak_rss", &[]);
5309        match result {
5310            Ok(Some(Value::Int(kb))) => {
5311                assert!(kb > 0, "peak_rss should return positive value, got {kb}");
5312            }
5313            other => panic!("Expected Ok(Some(Int)), got: {other:?}"),
5314        }
5315    }
5316
5317    #[test]
5318    fn test_complex_constructor() {
5319        let result = dispatch_builtin("Complex", &[Value::Float(3.0), Value::Float(4.0)]);
5320        match result {
5321            Ok(Some(Value::Complex(c))) => {
5322                assert_eq!(c.re, 3.0);
5323                assert_eq!(c.im, 4.0);
5324            }
5325            _ => panic!("expected Complex value"),
5326        }
5327    }
5328
5329    #[test]
5330    fn test_complex_constructor_from_ints() {
5331        let result = dispatch_builtin("Complex", &[Value::Int(1), Value::Int(-2)]);
5332        match result {
5333            Ok(Some(Value::Complex(c))) => {
5334                assert_eq!(c.re, 1.0);
5335                assert_eq!(c.im, -2.0);
5336            }
5337            _ => panic!("expected Complex value"),
5338        }
5339    }
5340
5341    #[test]
5342    fn test_complex_real_only() {
5343        let result = dispatch_builtin("Complex", &[Value::Float(5.0)]);
5344        match result {
5345            Ok(Some(Value::Complex(c))) => {
5346                assert_eq!(c.re, 5.0);
5347                assert_eq!(c.im, 0.0);
5348            }
5349            _ => panic!("expected Complex value"),
5350        }
5351    }
5352
5353    #[test]
5354    fn test_to_string() {
5355        let result = dispatch_builtin("to_string", &[Value::Int(42)]);
5356        match result {
5357            Ok(Some(Value::String(s))) => assert_eq!(s.as_str(), "42"),
5358            _ => panic!("expected String value"),
5359        }
5360    }
5361
5362    #[test]
5363    fn test_len_array() {
5364        let arr = Value::Array(Rc::new(vec![Value::Int(1), Value::Int(2), Value::Int(3)]));
5365        let result = dispatch_builtin("len", &[arr]);
5366        assert!(matches!(result, Ok(Some(Value::Int(3)))));
5367    }
5368
5369    #[test]
5370    fn test_len_string() {
5371        let s = Value::String(Rc::new("hello".to_string()));
5372        let result = dispatch_builtin("len", &[s]);
5373        assert!(matches!(result, Ok(Some(Value::Int(5)))));
5374    }
5375
5376    #[test]
5377    fn test_assert_pass() {
5378        let result = dispatch_builtin("assert", &[Value::Bool(true)]);
5379        assert!(matches!(result, Ok(Some(Value::Void))));
5380    }
5381
5382    #[test]
5383    fn test_assert_fail() {
5384        let result = dispatch_builtin("assert", &[Value::Bool(false)]);
5385        assert!(result.is_err());
5386    }
5387
5388    #[test]
5389    fn test_assert_eq_pass() {
5390        let result = dispatch_builtin("assert_eq", &[Value::Int(42), Value::Int(42)]);
5391        assert!(matches!(result, Ok(Some(Value::Void))));
5392    }
5393
5394    #[test]
5395    fn test_assert_eq_fail() {
5396        let result = dispatch_builtin("assert_eq", &[Value::Int(1), Value::Int(2)]);
5397        assert!(result.is_err());
5398    }
5399
5400    #[test]
5401    fn test_sqrt() {
5402        let result = dispatch_builtin("sqrt", &[Value::Float(4.0)]);
5403        match result {
5404            Ok(Some(Value::Float(v))) => assert_eq!(v, 2.0),
5405            _ => panic!("expected Float"),
5406        }
5407    }
5408
5409    #[test]
5410    fn test_abs_float() {
5411        let result = dispatch_builtin("abs", &[Value::Float(-3.14)]);
5412        match result {
5413            Ok(Some(Value::Float(v))) => assert_eq!(v, 3.14),
5414            _ => panic!("expected Float"),
5415        }
5416    }
5417
5418    #[test]
5419    fn test_abs_int() {
5420        let result = dispatch_builtin("abs", &[Value::Int(-42)]);
5421        assert!(matches!(result, Ok(Some(Value::Int(42)))));
5422    }
5423
5424    #[test]
5425    fn test_floor() {
5426        let result = dispatch_builtin("floor", &[Value::Float(3.7)]);
5427        match result {
5428            Ok(Some(Value::Float(v))) => assert_eq!(v, 3.0),
5429            _ => panic!("expected Float"),
5430        }
5431    }
5432
5433    #[test]
5434    fn test_int_conversion() {
5435        let result = dispatch_builtin("int", &[Value::Float(3.9)]);
5436        assert!(matches!(result, Ok(Some(Value::Int(3)))));
5437    }
5438
5439    #[test]
5440    fn test_float_conversion() {
5441        let result = dispatch_builtin("float", &[Value::Int(42)]);
5442        match result {
5443            Ok(Some(Value::Float(v))) => assert_eq!(v, 42.0),
5444            _ => panic!("expected Float"),
5445        }
5446    }
5447
5448    #[test]
5449    fn test_isnan() {
5450        let result = dispatch_builtin("isnan", &[Value::Float(f64::NAN)]);
5451        assert!(matches!(result, Ok(Some(Value::Bool(true)))));
5452
5453        let result = dispatch_builtin("isnan", &[Value::Float(1.0)]);
5454        assert!(matches!(result, Ok(Some(Value::Bool(false)))));
5455
5456        let result = dispatch_builtin("isnan", &[Value::Int(0)]);
5457        assert!(matches!(result, Ok(Some(Value::Bool(false)))));
5458    }
5459
5460    #[test]
5461    fn test_isinf() {
5462        let result = dispatch_builtin("isinf", &[Value::Float(f64::INFINITY)]);
5463        assert!(matches!(result, Ok(Some(Value::Bool(true)))));
5464
5465        let result = dispatch_builtin("isinf", &[Value::Float(1.0)]);
5466        assert!(matches!(result, Ok(Some(Value::Bool(false)))));
5467    }
5468
5469    #[test]
5470    fn test_push() {
5471        let arr = Value::Array(Rc::new(vec![Value::Int(1)]));
5472        let result = dispatch_builtin("push", &[arr, Value::Int(2)]);
5473        match result {
5474            Ok(Some(Value::Array(a))) => {
5475                assert_eq!(a.len(), 2);
5476                assert!(matches!(&a[1], Value::Int(2)));
5477            }
5478            _ => panic!("expected Array"),
5479        }
5480    }
5481
5482    #[test]
5483    fn test_sort() {
5484        let arr = Value::Array(Rc::new(vec![
5485            Value::Float(3.0),
5486            Value::Float(1.0),
5487            Value::Float(2.0),
5488        ]));
5489        let result = dispatch_builtin("sort", &[arr]);
5490        match result {
5491            Ok(Some(Value::Array(a))) => {
5492                assert!(matches!(&a[0], Value::Float(v) if *v == 1.0));
5493                assert!(matches!(&a[1], Value::Float(v) if *v == 2.0));
5494                assert!(matches!(&a[2], Value::Float(v) if *v == 3.0));
5495            }
5496            _ => panic!("expected sorted Array"),
5497        }
5498    }
5499
5500    #[test]
5501    fn test_unknown_builtin_returns_none() {
5502        let result = dispatch_builtin("unknown_function", &[]);
5503        assert!(matches!(result, Ok(None)));
5504    }
5505
5506    #[test]
5507    fn test_tensor_zeros() {
5508        let shape = Value::Array(Rc::new(vec![Value::Int(2), Value::Int(3)]));
5509        let result = dispatch_builtin("Tensor.zeros", &[shape]);
5510        match result {
5511            Ok(Some(Value::Tensor(t))) => {
5512                assert_eq!(t.shape(), &[2, 3]);
5513            }
5514            _ => panic!("expected Tensor"),
5515        }
5516    }
5517
5518    #[test]
5519    fn test_values_equal() {
5520        assert!(values_equal(&Value::Int(42), &Value::Int(42)));
5521        assert!(!values_equal(&Value::Int(1), &Value::Int(2)));
5522        assert!(values_equal(&Value::Float(3.14), &Value::Float(3.14)));
5523        assert!(values_equal(&Value::Bool(true), &Value::Bool(true)));
5524        assert!(values_equal(&Value::Void, &Value::Void));
5525        assert!(!values_equal(&Value::Int(1), &Value::Float(1.0)));
5526    }
5527
5528    #[test]
5529    fn test_value_to_shape() {
5530        let arr = Value::Array(Rc::new(vec![Value::Int(2), Value::Int(3), Value::Int(4)]));
5531        assert_eq!(value_to_shape(&arr).unwrap(), vec![2, 3, 4]);
5532    }
5533
5534    #[test]
5535    fn test_value_to_shape_negative_rejected() {
5536        let arr = Value::Array(Rc::new(vec![Value::Int(-1)]));
5537        assert!(value_to_shape(&arr).is_err());
5538    }
5539
5540    #[test]
5541    fn test_value_to_f64_vec() {
5542        let arr = Value::Array(Rc::new(vec![
5543            Value::Float(1.0),
5544            Value::Int(2),
5545            Value::Float(3.5),
5546        ]));
5547        assert_eq!(value_to_f64_vec(&arr).unwrap(), vec![1.0, 2.0, 3.5]);
5548    }
5549
5550    #[test]
5551    fn test_log_float() {
5552        let result = dispatch_builtin("log", &[Value::Float(1.0)]);
5553        match result {
5554            Ok(Some(Value::Float(v))) => assert!((v - 0.0).abs() < 1e-15),
5555            _ => panic!("expected Float"),
5556        }
5557    }
5558
5559    #[test]
5560    fn test_log_e() {
5561        let result = dispatch_builtin("log", &[Value::Float(std::f64::consts::E)]);
5562        match result {
5563            Ok(Some(Value::Float(v))) => assert!((v - 1.0).abs() < 1e-15),
5564            _ => panic!("expected Float"),
5565        }
5566    }
5567
5568    #[test]
5569    fn test_exp_float() {
5570        let result = dispatch_builtin("exp", &[Value::Float(0.0)]);
5571        match result {
5572            Ok(Some(Value::Float(v))) => assert!((v - 1.0).abs() < 1e-15),
5573            _ => panic!("expected Float"),
5574        }
5575    }
5576
5577    #[test]
5578    fn test_exp_one() {
5579        let result = dispatch_builtin("exp", &[Value::Float(1.0)]);
5580        match result {
5581            Ok(Some(Value::Float(v))) => assert!((v - std::f64::consts::E).abs() < 1e-15),
5582            _ => panic!("expected Float"),
5583        }
5584    }
5585
5586    #[test]
5587    fn test_categorical_sample_deterministic() {
5588        let probs = Tensor::from_vec(vec![0.0, 0.0, 1.0], &[3]).unwrap();
5589        // u=0.5, cumsum: 0.0, 0.0, 1.0 → picks index 2
5590        assert_eq!(categorical_sample_with_u(&probs, 0.5).unwrap(), 2);
5591    }
5592
5593    #[test]
5594    fn test_categorical_sample_first() {
5595        let probs = Tensor::from_vec(vec![0.5, 0.3, 0.2], &[3]).unwrap();
5596        // u=0.1 → picks index 0 (cumsum 0.5 > 0.1)
5597        assert_eq!(categorical_sample_with_u(&probs, 0.1).unwrap(), 0);
5598    }
5599
5600    #[test]
5601    fn test_categorical_sample_middle() {
5602        let probs = Tensor::from_vec(vec![0.2, 0.5, 0.3], &[3]).unwrap();
5603        // u=0.6 → cumsum 0.2, 0.7 → picks index 1
5604        assert_eq!(categorical_sample_with_u(&probs, 0.6).unwrap(), 1);
5605    }
5606
5607    #[test]
5608    fn test_categorical_sample_last() {
5609        let probs = Tensor::from_vec(vec![0.2, 0.3, 0.5], &[3]).unwrap();
5610        // u=0.99 → cumsum 0.2, 0.5, 1.0 → picks index 2
5611        assert_eq!(categorical_sample_with_u(&probs, 0.99).unwrap(), 2);
5612    }
5613}