Skip to main content

r/interpreter/builtins/
math.rs

1use std::collections::BTreeSet;
2
3#[cfg(feature = "linalg")]
4use derive_more::{Display, Error};
5use itertools::Itertools;
6#[cfg(feature = "linalg")]
7use nalgebra::DMatrix;
8#[cfg(feature = "linalg")]
9use ndarray::{Array1, Array2, ShapeBuilder};
10
11use crate::interpreter::coerce::{f64_to_i32, usize_to_f64};
12use crate::interpreter::value::*;
13#[cfg(feature = "linalg")]
14use crate::interpreter::BuiltinContext;
15#[cfg(feature = "linalg")]
16use crate::parser::ast::Expr;
17use minir_macros::builtin;
18#[cfg(feature = "linalg")]
19use minir_macros::interpreter_builtin;
20
21type DimNameVec = Vec<Option<String>>;
22type MatrixDimNames = (Option<DimNameVec>, Option<DimNameVec>);
23
24// region: MathError
25
26/// Structured error type for math/linear algebra operations.
27#[cfg(feature = "linalg")]
28#[derive(Debug, Display, Error)]
29pub enum MathError {
30    #[display("matrix shape error: {}", source)]
31    Shape {
32        #[error(source)]
33        source: ndarray::ShapeError,
34    },
35}
36
37#[cfg(feature = "linalg")]
38impl From<MathError> for RError {
39    fn from(e: MathError) -> Self {
40        RError::from_source(RErrorKind::Other, e)
41    }
42}
43
44// endregion
45
46// === Math functions ===
47
48/// Absolute value.
49///
50/// @param x numeric vector
51/// @return numeric vector of absolute values
52#[builtin(min_args = 1)]
53fn builtin_abs(args: &[RValue], _: &[(String, RValue)]) -> Result<RValue, RError> {
54    math_unary(args, f64::abs)
55}
56
57/// Square root.
58///
59/// @param x numeric vector
60/// @return numeric vector of square roots
61#[builtin(min_args = 1)]
62fn builtin_sqrt(args: &[RValue], _: &[(String, RValue)]) -> Result<RValue, RError> {
63    math_unary(args, f64::sqrt)
64}
65
66/// Exponential function (e^x).
67///
68/// @param x numeric vector
69/// @return numeric vector of e raised to each element
70#[builtin(min_args = 1)]
71fn builtin_exp(args: &[RValue], _: &[(String, RValue)]) -> Result<RValue, RError> {
72    math_unary(args, f64::exp)
73}
74
75/// Logarithm. With one argument, computes the natural log. With a second
76/// positional argument or named `base` argument, computes log in that base
77/// via the change-of-base formula: log(x, base) = ln(x) / ln(base).
78///
79/// @param x numeric vector
80/// @param base the base of the logarithm (default: e, i.e. natural log)
81/// @return numeric vector of logarithms
82#[builtin(min_args = 1)]
83fn builtin_log(args: &[RValue], named: &[(String, RValue)]) -> Result<RValue, RError> {
84    let base: Option<f64> = named
85        .iter()
86        .find(|(n, _)| n == "base")
87        .and_then(|(_, v)| v.as_vector()?.as_double_scalar())
88        .or_else(|| args.get(1)?.as_vector()?.as_double_scalar());
89
90    match base {
91        Some(b) => {
92            let ln_base = b.ln();
93            match args.first() {
94                Some(RValue::Vector(v)) => {
95                    let result: Vec<Option<f64>> = v
96                        .to_doubles()
97                        .iter()
98                        .map(|x| x.map(|f| f.ln() / ln_base))
99                        .collect();
100                    Ok(RValue::vec(Vector::Double(result.into())))
101                }
102                _ => Err(RError::new(
103                    RErrorKind::Argument,
104                    "non-numeric argument to mathematical function".to_string(),
105                )),
106            }
107        }
108        None => math_unary(args, f64::ln),
109    }
110}
111
112/// Base-2 logarithm.
113///
114/// @param x numeric vector
115/// @return numeric vector of base-2 logarithms
116#[builtin(min_args = 1)]
117fn builtin_log2(args: &[RValue], _: &[(String, RValue)]) -> Result<RValue, RError> {
118    math_unary(args, f64::log2)
119}
120
121/// Base-10 logarithm.
122///
123/// @param x numeric vector
124/// @return numeric vector of base-10 logarithms
125#[builtin(min_args = 1)]
126fn builtin_log10(args: &[RValue], _: &[(String, RValue)]) -> Result<RValue, RError> {
127    math_unary(args, f64::log10)
128}
129
130/// Ceiling (smallest integer not less than x).
131///
132/// @param x numeric vector
133/// @return numeric vector rounded up to integers
134#[builtin(min_args = 1)]
135fn builtin_ceiling(args: &[RValue], _: &[(String, RValue)]) -> Result<RValue, RError> {
136    math_unary(args, f64::ceil)
137}
138
139/// Floor (largest integer not greater than x).
140///
141/// @param x numeric vector
142/// @return numeric vector rounded down to integers
143#[builtin(min_args = 1)]
144fn builtin_floor(args: &[RValue], _: &[(String, RValue)]) -> Result<RValue, RError> {
145    math_unary(args, f64::floor)
146}
147
148/// Truncation (round toward zero).
149///
150/// @param x numeric vector
151/// @return numeric vector truncated toward zero
152#[builtin(min_args = 1)]
153fn builtin_trunc(args: &[RValue], _: &[(String, RValue)]) -> Result<RValue, RError> {
154    math_unary(args, f64::trunc)
155}
156
157/// Sine (in radians).
158///
159/// @param x numeric vector
160/// @return numeric vector of sines
161#[builtin(min_args = 1)]
162fn builtin_sin(args: &[RValue], _: &[(String, RValue)]) -> Result<RValue, RError> {
163    math_unary(args, f64::sin)
164}
165
166/// Cosine (in radians).
167///
168/// @param x numeric vector
169/// @return numeric vector of cosines
170#[builtin(min_args = 1)]
171fn builtin_cos(args: &[RValue], _: &[(String, RValue)]) -> Result<RValue, RError> {
172    math_unary(args, f64::cos)
173}
174
175/// Tangent (in radians).
176///
177/// @param x numeric vector
178/// @return numeric vector of tangents
179#[builtin(min_args = 1)]
180fn builtin_tan(args: &[RValue], _: &[(String, RValue)]) -> Result<RValue, RError> {
181    math_unary(args, f64::tan)
182}
183
184/// Sign of each element (-1, 0, or 1).
185///
186/// R's sign() returns 0 for zero inputs (unlike Rust's f64::signum which returns 1.0).
187/// For integer inputs, returns an integer vector; for double inputs, returns a double vector.
188///
189/// @param x numeric vector
190/// @return numeric vector of signs
191#[builtin(min_args = 1)]
192fn builtin_sign(args: &[RValue], _: &[(String, RValue)]) -> Result<RValue, RError> {
193    match args.first() {
194        Some(RValue::Vector(v)) => match &v.inner {
195            Vector::Integer(vals) => {
196                let result: Vec<Option<i64>> = vals
197                    .iter()
198                    .map(|x| {
199                        x.map(|i| match i.cmp(&0) {
200                            std::cmp::Ordering::Greater => 1i64,
201                            std::cmp::Ordering::Equal => 0i64,
202                            std::cmp::Ordering::Less => -1i64,
203                        })
204                    })
205                    .collect();
206                Ok(RValue::vec(Vector::Integer(result.into())))
207            }
208            _ => {
209                let result: Vec<Option<f64>> = v
210                    .to_doubles()
211                    .iter()
212                    .map(|x| {
213                        x.map(|f| {
214                            if f.is_nan() {
215                                f64::NAN
216                            } else if f > 0.0 {
217                                1.0
218                            } else if f < 0.0 {
219                                -1.0
220                            } else {
221                                0.0
222                            }
223                        })
224                    })
225                    .collect();
226                Ok(RValue::vec(Vector::Double(result.into())))
227            }
228        },
229        _ => Err(RError::new(
230            RErrorKind::Argument,
231            "non-numeric argument to mathematical function".to_string(),
232        )),
233    }
234}
235
236use super::math_unary_op as math_unary;
237
238// region: Inverse trigonometric
239
240/// Inverse sine (arc sine).
241///
242/// @param x numeric vector with values in [-1, 1]
243/// @return numeric vector of angles in radians
244#[builtin(min_args = 1)]
245fn builtin_asin(args: &[RValue], _: &[(String, RValue)]) -> Result<RValue, RError> {
246    math_unary(args, f64::asin)
247}
248
249/// Inverse cosine (arc cosine).
250///
251/// @param x numeric vector with values in [-1, 1]
252/// @return numeric vector of angles in radians
253#[builtin(min_args = 1)]
254fn builtin_acos(args: &[RValue], _: &[(String, RValue)]) -> Result<RValue, RError> {
255    math_unary(args, f64::acos)
256}
257
258/// Inverse tangent (arc tangent).
259///
260/// @param x numeric vector
261/// @return numeric vector of angles in radians
262#[builtin(min_args = 1)]
263fn builtin_atan(args: &[RValue], _: &[(String, RValue)]) -> Result<RValue, RError> {
264    math_unary(args, f64::atan)
265}
266
267/// Two-argument inverse tangent.
268///
269/// Computes atan(y/x) but uses the signs of both arguments to determine
270/// the correct quadrant. Returns angles in [-pi, pi].
271///
272/// @param y numeric vector (y-coordinates)
273/// @param x numeric vector (x-coordinates)
274/// @return numeric vector of angles in radians
275#[builtin(min_args = 2)]
276fn builtin_atan2(args: &[RValue], _: &[(String, RValue)]) -> Result<RValue, RError> {
277    math_binary(args, f64::atan2)
278}
279
280// endregion
281
282// region: Hyperbolic
283
284/// Hyperbolic sine.
285///
286/// @param x numeric vector
287/// @return numeric vector of hyperbolic sines
288#[builtin(min_args = 1)]
289fn builtin_sinh(args: &[RValue], _: &[(String, RValue)]) -> Result<RValue, RError> {
290    math_unary(args, f64::sinh)
291}
292
293/// Hyperbolic cosine.
294///
295/// @param x numeric vector
296/// @return numeric vector of hyperbolic cosines
297#[builtin(min_args = 1)]
298fn builtin_cosh(args: &[RValue], _: &[(String, RValue)]) -> Result<RValue, RError> {
299    math_unary(args, f64::cosh)
300}
301
302/// Hyperbolic tangent.
303///
304/// @param x numeric vector
305/// @return numeric vector of hyperbolic tangents
306#[builtin(min_args = 1)]
307fn builtin_tanh(args: &[RValue], _: &[(String, RValue)]) -> Result<RValue, RError> {
308    math_unary(args, f64::tanh)
309}
310
311// endregion
312
313// region: Inverse hyperbolic
314
315/// Inverse hyperbolic sine.
316///
317/// @param x numeric vector
318/// @return numeric vector of inverse hyperbolic sines
319#[builtin(min_args = 1)]
320fn builtin_asinh(args: &[RValue], _: &[(String, RValue)]) -> Result<RValue, RError> {
321    math_unary(args, f64::asinh)
322}
323
324/// Inverse hyperbolic cosine.
325///
326/// @param x numeric vector with values >= 1
327/// @return numeric vector of inverse hyperbolic cosines
328#[builtin(min_args = 1)]
329fn builtin_acosh(args: &[RValue], _: &[(String, RValue)]) -> Result<RValue, RError> {
330    math_unary(args, f64::acosh)
331}
332
333/// Inverse hyperbolic tangent.
334///
335/// @param x numeric vector with values in (-1, 1)
336/// @return numeric vector of inverse hyperbolic tangents
337#[builtin(min_args = 1)]
338fn builtin_atanh(args: &[RValue], _: &[(String, RValue)]) -> Result<RValue, RError> {
339    math_unary(args, f64::atanh)
340}
341
342// endregion
343
344// region: Numerically stable exp/log variants
345
346/// Numerically stable exp(x) - 1.
347///
348/// More accurate than `exp(x) - 1` for x near zero, where catastrophic
349/// cancellation would otherwise lose precision.
350///
351/// @param x numeric vector
352/// @return numeric vector of exp(x) - 1
353#[builtin(min_args = 1)]
354fn builtin_expm1(args: &[RValue], _: &[(String, RValue)]) -> Result<RValue, RError> {
355    math_unary(args, f64::exp_m1)
356}
357
358/// Numerically stable log(1 + x).
359///
360/// More accurate than `log(1 + x)` for x near zero, where catastrophic
361/// cancellation would otherwise lose precision.
362///
363/// @param x numeric vector with values > -1
364/// @return numeric vector of log(1 + x)
365#[builtin(min_args = 1)]
366fn builtin_log1p(args: &[RValue], _: &[(String, RValue)]) -> Result<RValue, RError> {
367    math_unary(args, f64::ln_1p)
368}
369
370// endregion
371
372// region: Gamma-related and combinatorial
373
374/// Gamma function.
375///
376/// Computes the gamma function, which extends factorial to real numbers:
377/// gamma(n) = (n-1)! for positive integers.
378///
379/// @param x numeric vector
380/// @return numeric vector of gamma values
381#[builtin(min_args = 1)]
382fn builtin_gamma(args: &[RValue], _: &[(String, RValue)]) -> Result<RValue, RError> {
383    math_unary(args, libm::tgamma)
384}
385
386/// Natural logarithm of the absolute value of the gamma function.
387///
388/// More numerically stable than log(abs(gamma(x))) for large x.
389///
390/// @param x numeric vector
391/// @return numeric vector of log-gamma values
392#[builtin(min_args = 1)]
393fn builtin_lgamma(args: &[RValue], _: &[(String, RValue)]) -> Result<RValue, RError> {
394    math_unary(args, libm::lgamma)
395}
396
397/// Beta function: gamma(a) * gamma(b) / gamma(a + b).
398///
399/// @param a numeric vector
400/// @param b numeric vector
401/// @return numeric vector of beta function values
402#[builtin(min_args = 2)]
403fn builtin_beta(args: &[RValue], _: &[(String, RValue)]) -> Result<RValue, RError> {
404    math_binary(args, |a, b| {
405        (libm::lgamma(a) + libm::lgamma(b) - libm::lgamma(a + b)).exp()
406    })
407}
408
409/// Natural logarithm of the beta function.
410///
411/// More numerically stable than log(beta(a, b)) for large a or b.
412///
413/// @param a numeric vector
414/// @param b numeric vector
415/// @return numeric vector of log-beta values
416#[builtin(min_args = 2)]
417fn builtin_lbeta(args: &[RValue], _: &[(String, RValue)]) -> Result<RValue, RError> {
418    math_binary(args, |a, b| {
419        libm::lgamma(a) + libm::lgamma(b) - libm::lgamma(a + b)
420    })
421}
422
423/// Factorial: n! = 1 * 2 * ... * n.
424///
425/// Uses gamma(n + 1) for non-integer and large values.
426///
427/// @param x numeric vector (non-negative)
428/// @return numeric vector of factorials
429#[builtin(min_args = 1)]
430fn builtin_factorial(args: &[RValue], _: &[(String, RValue)]) -> Result<RValue, RError> {
431    math_unary(args, |x| libm::tgamma(x + 1.0))
432}
433
434/// Binomial coefficient: choose(n, k) = n! / (k! * (n - k)!).
435///
436/// Uses the log-gamma formulation for numerical stability.
437///
438/// @param n numeric vector
439/// @param k numeric vector
440/// @return numeric vector of binomial coefficients
441#[builtin(min_args = 2)]
442fn builtin_choose(args: &[RValue], _: &[(String, RValue)]) -> Result<RValue, RError> {
443    math_binary(args, |n, k| {
444        if k < 0.0 || k > n {
445            return 0.0;
446        }
447        // For integer-valued inputs, use the log-gamma identity:
448        // choose(n, k) = exp(lgamma(n+1) - lgamma(k+1) - lgamma(n-k+1))
449        (libm::lgamma(n + 1.0) - libm::lgamma(k + 1.0) - libm::lgamma(n - k + 1.0))
450            .exp()
451            .round()
452    })
453}
454
455/// All combinations of n elements taken k at a time.
456///
457/// Returns a matrix with k rows and choose(n, k) columns, where each
458/// column is one combination.
459///
460/// @param x if numeric scalar, treated as 1:x; if vector, elements to combine
461/// @param m number of elements to choose
462/// @return matrix of combinations (k rows, choose(n,k) columns)
463#[builtin(min_args = 2)]
464fn builtin_combn(args: &[RValue], _: &[(String, RValue)]) -> Result<RValue, RError> {
465    // Resolve the pool of elements
466    let pool: Vec<f64> = match args.first() {
467        Some(RValue::Vector(v)) => {
468            let doubles = v.to_doubles();
469            if doubles.len() == 1 {
470                // Scalar n => pool is 1:n
471                let n = doubles[0].ok_or_else(|| {
472                    RError::new(
473                        RErrorKind::Argument,
474                        "NA in combn first argument".to_string(),
475                    )
476                })?;
477                let n_int = n as i64;
478                (1..=n_int).map(|i| i as f64).collect()
479            } else {
480                doubles
481                    .into_iter()
482                    .map(|x| {
483                        x.ok_or_else(|| {
484                            RError::new(RErrorKind::Argument, "NA in combn input".to_string())
485                        })
486                    })
487                    .collect::<Result<Vec<f64>, RError>>()?
488            }
489        }
490        _ => {
491            return Err(RError::new(
492                RErrorKind::Argument,
493                "first argument must be numeric".to_string(),
494            ))
495        }
496    };
497
498    let m = match args.get(1) {
499        Some(RValue::Vector(v)) => v.as_integer_scalar().ok_or_else(|| {
500            RError::new(
501                RErrorKind::Argument,
502                "second argument (m) must be a single integer".to_string(),
503            )
504        })?,
505        _ => {
506            return Err(RError::new(
507                RErrorKind::Argument,
508                "second argument (m) must be a single integer".to_string(),
509            ))
510        }
511    };
512
513    let n = pool.len();
514    let m_usize = usize::try_from(m).map_err(|_| {
515        RError::new(
516            RErrorKind::Argument,
517            format!("m must be non-negative, got {m}"),
518        )
519    })?;
520
521    if m_usize > n {
522        return Err(RError::new(
523            RErrorKind::Argument,
524            format!("m ({m_usize}) must be <= n ({n}) in combn"),
525        ));
526    }
527
528    // Generate all combinations using itertools
529    let combos: Vec<Vec<f64>> = (0..n)
530        .combinations(m_usize)
531        .map(|indices| indices.iter().map(|&i| pool[i]).collect())
532        .collect();
533
534    let ncol = combos.len();
535    let nrow = m_usize;
536
537    // Fill column-major (R convention): column by column
538    let mut data: Vec<Option<f64>> = Vec::with_capacity(nrow * ncol);
539    for combo in &combos {
540        for &val in combo {
541            data.push(Some(val));
542        }
543    }
544
545    let mut rv = RVector::from(Vector::Double(data.into()));
546    set_matrix_attrs(&mut rv, nrow, ncol, None, None)?;
547    Ok(RValue::Vector(rv))
548}
549
550// endregion
551
552// region: Digamma and trigamma
553
554/// Digamma function (psi function): the logarithmic derivative of the gamma function.
555///
556/// Computes d/dx log(gamma(x)) = gamma'(x) / gamma(x).
557/// Uses an asymptotic expansion for large x and the recurrence relation
558/// psi(x) = psi(x+1) - 1/x for small x.
559///
560/// @param x numeric vector
561/// @return numeric vector of digamma values
562#[builtin(min_args = 1)]
563fn builtin_digamma(args: &[RValue], _: &[(String, RValue)]) -> Result<RValue, RError> {
564    math_unary(args, digamma_f64)
565}
566
567/// Trigamma function: the derivative of the digamma function.
568///
569/// Computes d^2/dx^2 log(gamma(x)).
570/// Uses an asymptotic expansion for large x and the recurrence relation
571/// trigamma(x) = trigamma(x+1) + 1/x^2 for small x.
572///
573/// @param x numeric vector
574/// @return numeric vector of trigamma values
575#[builtin(min_args = 1)]
576fn builtin_trigamma(args: &[RValue], _: &[(String, RValue)]) -> Result<RValue, RError> {
577    math_unary(args, trigamma_f64)
578}
579
580/// Compute digamma(x) using asymptotic expansion and recurrence.
581///
582/// For large x, use the asymptotic series:
583///   psi(x) ~ ln(x) - 1/(2x) - B_2/(2*x^2) - B_4/(4*x^4) - ...
584/// where B_{2k} are Bernoulli numbers.
585/// For small x, use psi(x) = psi(x+1) - 1/x repeatedly.
586fn digamma_f64(mut x: f64) -> f64 {
587    if x.is_nan() {
588        return f64::NAN;
589    }
590    if x == f64::NEG_INFINITY {
591        return f64::NAN;
592    }
593    if x == f64::INFINITY {
594        return f64::INFINITY;
595    }
596
597    // Reflection formula for x < 0: psi(x) = psi(1-x) - pi / tan(pi*x)
598    let mut result = 0.0;
599    if x < 0.0 {
600        let pi_x = std::f64::consts::PI * x;
601        let tan_val = pi_x.tan();
602        if tan_val == 0.0 {
603            return f64::NAN; // poles at non-positive integers
604        }
605        result -= std::f64::consts::PI / tan_val;
606        x = 1.0 - x;
607    }
608
609    // Pole at x = 0
610    if x == 0.0 {
611        return f64::NAN;
612    }
613
614    // Recurrence: psi(x) = psi(x+1) - 1/x, until x is large enough
615    while x < 12.0 {
616        result -= 1.0 / x;
617        x += 1.0;
618    }
619
620    // Asymptotic expansion for large x:
621    // psi(x) ~ ln(x) - 1/(2x) - sum_{k=1..} B_{2k}/(2k * x^{2k})
622    // Coefficients -B_{2k}/(2k):
623    //   k=1: -1/12,  k=2: 1/120,  k=3: -1/252,
624    //   k=4: 1/240,  k=5: -1/132, k=6: 691/32760
625    let inv_x = 1.0 / x;
626    let inv_x2 = inv_x * inv_x;
627
628    // Horner evaluation: S = (1/x^2) * (c1 + (1/x^2) * (c2 + ...))
629    // where c_k = -B_{2k}/(2k), starting from innermost (highest k)
630    let mut series = 691.0 / 32760.0; // k=6
631    series = series * inv_x2 + (-1.0 / 132.0); // k=5
632    series = series * inv_x2 + (1.0 / 240.0); // k=4
633    series = series * inv_x2 + (-1.0 / 252.0); // k=3
634    series = series * inv_x2 + (1.0 / 120.0); // k=2
635    series = series * inv_x2 + (-1.0 / 12.0); // k=1
636    series *= inv_x2;
637
638    result += x.ln() - 0.5 * inv_x + series;
639
640    result
641}
642
643/// Compute trigamma(x) using asymptotic expansion and recurrence.
644///
645/// For large x, use the asymptotic series:
646///   trigamma(x) ~ 1/x + 1/(2x^2) + sum_{k=1..} B_{2k}/x^{2k+1}
647/// where B_{2k} are Bernoulli numbers.
648/// For small x, use trigamma(x) = trigamma(x+1) + 1/x^2 repeatedly.
649fn trigamma_f64(mut x: f64) -> f64 {
650    if x.is_nan() {
651        return f64::NAN;
652    }
653    if x == f64::INFINITY {
654        return 0.0;
655    }
656    if x == f64::NEG_INFINITY {
657        return f64::NAN;
658    }
659
660    // Reflection formula: trigamma(1-x) + trigamma(x) = pi^2 / sin^2(pi*x)
661    let mut result = 0.0;
662    if x < 0.0 {
663        let pi_x = std::f64::consts::PI * x;
664        let sin_val = pi_x.sin();
665        if sin_val == 0.0 {
666            return f64::NAN; // poles at non-positive integers
667        }
668        let pi2 = std::f64::consts::PI * std::f64::consts::PI;
669        result += pi2 / (sin_val * sin_val);
670        x = 1.0 - x;
671        return result - trigamma_f64(x);
672    }
673
674    // Pole at x = 0
675    if x == 0.0 {
676        return f64::NAN;
677    }
678
679    // Recurrence: trigamma(x) = trigamma(x+1) + 1/x^2
680    while x < 12.0 {
681        result += 1.0 / (x * x);
682        x += 1.0;
683    }
684
685    // Asymptotic expansion for large x:
686    // trigamma(x) ~ 1/x + 1/(2x^2) + B_2/x^3 + B_4/x^5 + B_6/x^7 + ...
687    // B_2=1/6, B_4=-1/30, B_6=1/42, B_8=-1/30, B_10=5/66, B_12=-691/2730
688    let inv_x = 1.0 / x;
689    let inv_x2 = inv_x * inv_x;
690
691    // Horner-like evaluation: multiply by inv_x2 each step, then by inv_x at end
692    // Start from highest-order term and work backward
693    let mut series = -691.0 / 2730.0;
694    series = series * inv_x2 + 5.0 / 66.0;
695    series = series * inv_x2 + -1.0 / 30.0;
696    series = series * inv_x2 + 1.0 / 42.0;
697    series = series * inv_x2 + -1.0 / 30.0;
698    series = series * inv_x2 + 1.0 / 6.0;
699    series *= inv_x2 * inv_x; // multiply by 1/x^3
700
701    result += inv_x + 0.5 * inv_x2 + series;
702
703    result
704}
705
706// endregion
707
708// region: Bessel functions
709
710/// Bessel function of the first kind, J_nu(x).
711///
712/// Computes J_nu(x) for integer order nu using libm's jn function.
713/// R's besselJ also supports non-integer nu, but this implementation
714/// currently handles integer orders only (non-integer orders return NaN
715/// with a warning).
716///
717/// @param x numeric vector
718/// @param nu numeric scalar (order, rounded to nearest integer)
719/// @return numeric vector of Bessel J values
720#[builtin(name = "besselJ", min_args = 2)]
721fn builtin_bessel_j(args: &[RValue], _: &[(String, RValue)]) -> Result<RValue, RError> {
722    let nu = args
723        .get(1)
724        .and_then(|v| v.as_vector())
725        .and_then(|v| v.as_double_scalar())
726        .ok_or_else(|| {
727            RError::new(
728                RErrorKind::Argument,
729                "besselJ requires a numeric 'nu' (order) argument".to_string(),
730            )
731        })?;
732    let n = f64_to_i32(nu.round())?;
733    match args.first() {
734        Some(RValue::Vector(v)) => {
735            let result: Vec<Option<f64>> = v
736                .to_doubles()
737                .iter()
738                .map(|x| x.map(|x| libm::jn(n, x)))
739                .collect();
740            Ok(RValue::vec(Vector::Double(result.into())))
741        }
742        _ => Err(RError::new(
743            RErrorKind::Argument,
744            "non-numeric argument to mathematical function".to_string(),
745        )),
746    }
747}
748
749/// Bessel function of the second kind, Y_nu(x).
750///
751/// Computes Y_nu(x) for integer order nu using libm's yn function.
752/// R's besselY also supports non-integer nu, but this implementation
753/// currently handles integer orders only.
754///
755/// @param x numeric vector (must be positive for finite results)
756/// @param nu numeric scalar (order, rounded to nearest integer)
757/// @return numeric vector of Bessel Y values
758#[builtin(name = "besselY", min_args = 2)]
759fn builtin_bessel_y(args: &[RValue], _: &[(String, RValue)]) -> Result<RValue, RError> {
760    let nu = args
761        .get(1)
762        .and_then(|v| v.as_vector())
763        .and_then(|v| v.as_double_scalar())
764        .ok_or_else(|| {
765            RError::new(
766                RErrorKind::Argument,
767                "besselY requires a numeric 'nu' (order) argument".to_string(),
768            )
769        })?;
770    let n = f64_to_i32(nu.round())?;
771    match args.first() {
772        Some(RValue::Vector(v)) => {
773            let result: Vec<Option<f64>> = v
774                .to_doubles()
775                .iter()
776                .map(|x| x.map(|x| libm::yn(n, x)))
777                .collect();
778            Ok(RValue::vec(Vector::Double(result.into())))
779        }
780        _ => Err(RError::new(
781            RErrorKind::Argument,
782            "non-numeric argument to mathematical function".to_string(),
783        )),
784    }
785}
786
787// endregion
788
789// region: libm special functions
790
791/// Cube root.
792///
793/// Computes the real cube root of x. Unlike x^(1/3), this correctly
794/// handles negative values: cbrt(-8) = -2.
795///
796/// @param x numeric vector
797/// @return numeric vector of cube roots
798#[builtin(min_args = 1)]
799fn builtin_cbrt(args: &[RValue], _: &[(String, RValue)]) -> Result<RValue, RError> {
800    math_unary(args, libm::cbrt)
801}
802
803/// Euclidean distance: sqrt(x^2 + y^2) without overflow.
804///
805/// Computes the hypotenuse length avoiding intermediate overflow
806/// that would occur with naive sqrt(x*x + y*y).
807///
808/// @param x numeric vector
809/// @param y numeric vector
810/// @return numeric vector of hypotenuse lengths
811#[builtin(min_args = 2)]
812fn builtin_hypot(args: &[RValue], _: &[(String, RValue)]) -> Result<RValue, RError> {
813    math_binary(args, libm::hypot)
814}
815
816// endregion
817
818// region: Binary math helper
819
820/// Helper for binary math builtins: applies `(f64, f64) -> f64` element-wise
821/// with recycling.
822fn math_binary(args: &[RValue], f: fn(f64, f64) -> f64) -> Result<RValue, RError> {
823    let (a_vec, b_vec) = match (args.first(), args.get(1)) {
824        (Some(RValue::Vector(a)), Some(RValue::Vector(b))) => (a.to_doubles(), b.to_doubles()),
825        _ => {
826            return Err(RError::new(
827                RErrorKind::Argument,
828                "non-numeric argument to mathematical function".to_string(),
829            ))
830        }
831    };
832    let len = a_vec.len().max(b_vec.len());
833    let result: Vec<Option<f64>> = (0..len)
834        .map(|i| {
835            let a = a_vec[i % a_vec.len()];
836            let b = b_vec[i % b_vec.len()];
837            match (a, b) {
838                (Some(x), Some(y)) => Some(f(x, y)),
839                _ => None,
840            }
841        })
842        .collect();
843    Ok(RValue::vec(Vector::Double(result.into())))
844}
845
846// endregion
847
848/// Round to the specified number of decimal places using IEEE 754
849/// round-half-to-even (banker's rounding): when the fractional part is
850/// exactly 0.5, round to the nearest even number.
851///
852/// @param x numeric vector
853/// @param digits number of decimal places (default 0)
854/// @return numeric vector of rounded values
855#[builtin(min_args = 1)]
856fn builtin_round(args: &[RValue], named: &[(String, RValue)]) -> Result<RValue, RError> {
857    let digits = named
858        .iter()
859        .find(|(n, _)| n == "digits")
860        .and_then(|(_, v)| v.as_vector()?.as_integer_scalar())
861        .or_else(|| args.get(1)?.as_vector()?.as_integer_scalar())
862        .unwrap_or(0);
863    let factor = 10f64.powi(i32::try_from(digits)?);
864    match args.first() {
865        Some(RValue::Vector(v)) => {
866            let result: Vec<Option<f64>> = v
867                .to_doubles()
868                .iter()
869                .map(|x| x.map(|f| round_half_to_even(f * factor) / factor))
870                .collect();
871            Ok(RValue::vec(Vector::Double(result.into())))
872        }
873        _ => Err(RError::new(
874            RErrorKind::Argument,
875            "non-numeric argument".to_string(),
876        )),
877    }
878}
879
880/// IEEE 754 round-half-to-even: if the fractional part is exactly 0.5,
881/// round to the nearest even integer. Otherwise round normally.
882fn round_half_to_even(x: f64) -> f64 {
883    let rounded = x.round();
884    // Check if we're exactly at a .5 boundary
885    let diff = x - x.floor();
886    if (diff - 0.5).abs() < f64::EPSILON {
887        // Exactly half: round to even
888        let floor = x.floor();
889        let ceil = x.ceil();
890        if floor % 2.0 == 0.0 {
891            floor
892        } else {
893            ceil
894        }
895    } else {
896        rounded
897    }
898}
899
900/// Round to the specified number of significant digits.
901///
902/// @param x numeric vector
903/// @param digits number of significant digits (default 6)
904/// @return numeric vector rounded to significant digits
905#[builtin(min_args = 1)]
906fn builtin_signif(args: &[RValue], named: &[(String, RValue)]) -> Result<RValue, RError> {
907    let digits = named
908        .iter()
909        .find(|(n, _)| n == "digits")
910        .and_then(|(_, v)| v.as_vector()?.as_integer_scalar())
911        .or_else(|| args.get(1)?.as_vector()?.as_integer_scalar())
912        .unwrap_or(6);
913    let digits = i32::try_from(digits)?;
914    match args.first() {
915        Some(RValue::Vector(v)) => {
916            let result: Vec<Option<f64>> = v
917                .to_doubles()
918                .iter()
919                .map(|x| {
920                    x.map(|f| {
921                        if f == 0.0 || !f.is_finite() {
922                            return f;
923                        }
924                        let d = f64_to_i32(f.abs().log10().ceil()).unwrap_or(0);
925                        let factor = 10f64.powi(digits - d);
926                        (f * factor).round() / factor
927                    })
928                })
929                .collect();
930            Ok(RValue::vec(Vector::Double(result.into())))
931        }
932        _ => Err(RError::new(
933            RErrorKind::Argument,
934            "non-numeric argument to signif".to_string(),
935        )),
936    }
937}
938
939// === Parallel min/max ===
940
941/// Parallel (element-wise) minimum across vectors.
942///
943/// @param ... numeric vectors (recycled to common length)
944/// @param na.rm logical; if TRUE, remove NAs before comparison
945/// @return numeric vector of element-wise minima
946#[builtin(min_args = 1)]
947fn builtin_pmin(args: &[RValue], named: &[(String, RValue)]) -> Result<RValue, RError> {
948    let na_rm = named.iter().any(|(n, v)| {
949        n == "na.rm" && v.as_vector().and_then(|v| v.as_logical_scalar()) == Some(true)
950    });
951    parallel_minmax(args, na_rm, false)
952}
953
954/// Parallel (element-wise) maximum across vectors.
955///
956/// @param ... numeric vectors (recycled to common length)
957/// @param na.rm logical; if TRUE, remove NAs before comparison
958/// @return numeric vector of element-wise maxima
959#[builtin(min_args = 1)]
960fn builtin_pmax(args: &[RValue], named: &[(String, RValue)]) -> Result<RValue, RError> {
961    let na_rm = named.iter().any(|(n, v)| {
962        n == "na.rm" && v.as_vector().and_then(|v| v.as_logical_scalar()) == Some(true)
963    });
964    parallel_minmax(args, na_rm, true)
965}
966
967fn parallel_minmax(args: &[RValue], na_rm: bool, is_max: bool) -> Result<RValue, RError> {
968    // Collect all argument vectors as doubles
969    let vecs: Vec<Vec<Option<f64>>> = args
970        .iter()
971        .filter_map(|a| {
972            if let RValue::Vector(v) = a {
973                Some(v.to_doubles())
974            } else {
975                None
976            }
977        })
978        .collect();
979    if vecs.is_empty() {
980        return Err(RError::new(
981            RErrorKind::Argument,
982            "no arguments to pmin/pmax".to_string(),
983        ));
984    }
985    // Find max length for recycling
986    let max_len = vecs.iter().map(|v| v.len()).max().unwrap_or(0);
987    let mut result = Vec::with_capacity(max_len);
988    for i in 0..max_len {
989        let mut current: Option<f64> = None;
990        let mut has_na = false;
991        for vec in &vecs {
992            let val = vec[i % vec.len()];
993            match val {
994                Some(f) => {
995                    current = Some(match current {
996                        Some(c) => {
997                            if is_max {
998                                c.max(f)
999                            } else {
1000                                c.min(f)
1001                            }
1002                        }
1003                        None => f,
1004                    });
1005                }
1006                None => {
1007                    has_na = true;
1008                }
1009            }
1010        }
1011        if has_na && !na_rm {
1012            result.push(None);
1013        } else {
1014            result.push(current);
1015        }
1016    }
1017    Ok(RValue::vec(Vector::Double(result.into())))
1018}
1019
1020// === Cumulative logical ===
1021
1022/// Cumulative all: TRUE while all preceding elements are TRUE.
1023///
1024/// @param x logical vector
1025/// @return logical vector of cumulative conjunction
1026#[builtin(min_args = 1)]
1027fn builtin_cumall(args: &[RValue], _: &[(String, RValue)]) -> Result<RValue, RError> {
1028    match args.first() {
1029        Some(RValue::Vector(v)) => {
1030            let logicals = v.to_logicals();
1031            let mut acc = true;
1032            let result: Vec<Option<bool>> = logicals
1033                .iter()
1034                .map(|x| match x {
1035                    Some(b) => {
1036                        acc = acc && *b;
1037                        Some(acc)
1038                    }
1039                    None => None,
1040                })
1041                .collect();
1042            Ok(RValue::vec(Vector::Logical(result.into())))
1043        }
1044        _ => Err(RError::new(
1045            RErrorKind::Argument,
1046            "invalid argument to cumall".to_string(),
1047        )),
1048    }
1049}
1050
1051/// Cumulative any: TRUE once any preceding element is TRUE.
1052///
1053/// @param x logical vector
1054/// @return logical vector of cumulative disjunction
1055#[builtin(min_args = 1)]
1056fn builtin_cumany(args: &[RValue], _: &[(String, RValue)]) -> Result<RValue, RError> {
1057    match args.first() {
1058        Some(RValue::Vector(v)) => {
1059            let logicals = v.to_logicals();
1060            let mut acc = false;
1061            let result: Vec<Option<bool>> = logicals
1062                .iter()
1063                .map(|x| match x {
1064                    Some(b) => {
1065                        acc = acc || *b;
1066                        Some(acc)
1067                    }
1068                    None => None,
1069                })
1070                .collect();
1071            Ok(RValue::vec(Vector::Logical(result.into())))
1072        }
1073        _ => Err(RError::new(
1074            RErrorKind::Argument,
1075            "invalid argument to cumany".to_string(),
1076        )),
1077    }
1078}
1079
1080// === Bitwise operations ===
1081
1082fn bitwise_binary_op_fallible(
1083    args: &[RValue],
1084    op: impl Fn(i64, i64) -> Result<i64, RError>,
1085) -> Result<RValue, RError> {
1086    let a_ints = args
1087        .first()
1088        .and_then(|v| v.as_vector())
1089        .map(|v| v.to_integers())
1090        .ok_or_else(|| {
1091            RError::new(
1092                RErrorKind::Argument,
1093                "non-integer argument to bitwise function".to_string(),
1094            )
1095        })?;
1096    let b_ints = args
1097        .get(1)
1098        .and_then(|v| v.as_vector())
1099        .map(|v| v.to_integers())
1100        .ok_or_else(|| {
1101            RError::new(
1102                RErrorKind::Argument,
1103                "non-integer argument to bitwise function".to_string(),
1104            )
1105        })?;
1106    let max_len = a_ints.len().max(b_ints.len());
1107    let result: Result<Vec<Option<i64>>, RError> = (0..max_len)
1108        .map(|i| {
1109            let a = a_ints[i % a_ints.len()];
1110            let b = b_ints[i % b_ints.len()];
1111            match (a, b) {
1112                (Some(x), Some(y)) => Ok(Some(op(x, y)?)),
1113                _ => Ok(None),
1114            }
1115        })
1116        .collect();
1117    Ok(RValue::vec(Vector::Integer(result?.into())))
1118}
1119
1120fn bitwise_binary_op(args: &[RValue], op: fn(i64, i64) -> i64) -> Result<RValue, RError> {
1121    let a_ints = args
1122        .first()
1123        .and_then(|v| v.as_vector())
1124        .map(|v| v.to_integers())
1125        .ok_or_else(|| {
1126            RError::new(
1127                RErrorKind::Argument,
1128                "non-integer argument to bitwise function".to_string(),
1129            )
1130        })?;
1131    let b_ints = args
1132        .get(1)
1133        .and_then(|v| v.as_vector())
1134        .map(|v| v.to_integers())
1135        .ok_or_else(|| {
1136            RError::new(
1137                RErrorKind::Argument,
1138                "non-integer argument to bitwise function".to_string(),
1139            )
1140        })?;
1141    let max_len = a_ints.len().max(b_ints.len());
1142    let result: Vec<Option<i64>> = (0..max_len)
1143        .map(|i| {
1144            let a = a_ints[i % a_ints.len()];
1145            let b = b_ints[i % b_ints.len()];
1146            match (a, b) {
1147                (Some(x), Some(y)) => Some(op(x, y)),
1148                _ => None,
1149            }
1150        })
1151        .collect();
1152    Ok(RValue::vec(Vector::Integer(result.into())))
1153}
1154
1155/// Bitwise AND.
1156///
1157/// @param a integer vector
1158/// @param b integer vector
1159/// @return integer vector of bitwise AND results
1160#[builtin(name = "bitwAnd", min_args = 2)]
1161fn builtin_bitw_and(args: &[RValue], _: &[(String, RValue)]) -> Result<RValue, RError> {
1162    bitwise_binary_op(args, |a, b| a & b)
1163}
1164
1165/// Bitwise OR.
1166///
1167/// @param a integer vector
1168/// @param b integer vector
1169/// @return integer vector of bitwise OR results
1170#[builtin(name = "bitwOr", min_args = 2)]
1171fn builtin_bitw_or(args: &[RValue], _: &[(String, RValue)]) -> Result<RValue, RError> {
1172    bitwise_binary_op(args, |a, b| a | b)
1173}
1174
1175/// Bitwise XOR.
1176///
1177/// @param a integer vector
1178/// @param b integer vector
1179/// @return integer vector of bitwise XOR results
1180#[builtin(name = "bitwXor", min_args = 2)]
1181fn builtin_bitw_xor(args: &[RValue], _: &[(String, RValue)]) -> Result<RValue, RError> {
1182    bitwise_binary_op(args, |a, b| a ^ b)
1183}
1184
1185/// Bitwise NOT (ones' complement).
1186///
1187/// @param a integer vector
1188/// @return integer vector of bitwise-negated values
1189#[builtin(name = "bitwNot", min_args = 1)]
1190fn builtin_bitw_not(args: &[RValue], _: &[(String, RValue)]) -> Result<RValue, RError> {
1191    let ints = args
1192        .first()
1193        .and_then(|v| v.as_vector())
1194        .map(|v| v.to_integers())
1195        .ok_or_else(|| {
1196            RError::new(
1197                RErrorKind::Argument,
1198                "non-integer argument to bitwNot".to_string(),
1199            )
1200        })?;
1201    let result: Vec<Option<i64>> = ints.iter().map(|x| x.map(|i| !i)).collect();
1202    Ok(RValue::vec(Vector::Integer(result.into())))
1203}
1204
1205/// Bitwise left shift.
1206///
1207/// @param a integer vector
1208/// @param n number of positions to shift
1209/// @return integer vector of left-shifted values
1210#[builtin(name = "bitwShiftL", min_args = 2)]
1211fn builtin_bitw_shift_l(args: &[RValue], _: &[(String, RValue)]) -> Result<RValue, RError> {
1212    bitwise_binary_op_fallible(args, |a, n| Ok(a << u32::try_from(n)?))
1213}
1214
1215/// Bitwise right shift.
1216///
1217/// @param a integer vector
1218/// @param n number of positions to shift
1219/// @return integer vector of right-shifted values
1220#[builtin(name = "bitwShiftR", min_args = 2)]
1221fn builtin_bitw_shift_r(args: &[RValue], _: &[(String, RValue)]) -> Result<RValue, RError> {
1222    bitwise_binary_op_fallible(args, |a, n| Ok(a >> u32::try_from(n)?))
1223}
1224
1225// === Triangular matrix extraction ===
1226
1227/// Lower triangle of a matrix.
1228///
1229/// @param x a matrix
1230/// @param diag logical; include the diagonal? (default FALSE)
1231/// @return logical matrix with TRUE in the lower triangle
1232#[builtin(name = "lower.tri", min_args = 1)]
1233fn builtin_lower_tri(args: &[RValue], named: &[(String, RValue)]) -> Result<RValue, RError> {
1234    let diag_incl = named
1235        .iter()
1236        .find(|(n, _)| n == "diag")
1237        .and_then(|(_, v)| v.as_vector()?.as_logical_scalar())
1238        .or_else(|| args.get(1)?.as_vector()?.as_logical_scalar())
1239        .unwrap_or(false);
1240    tri_matrix(args, diag_incl, true)
1241}
1242
1243/// Upper triangle of a matrix.
1244///
1245/// @param x a matrix
1246/// @param diag logical; include the diagonal? (default FALSE)
1247/// @return logical matrix with TRUE in the upper triangle
1248#[builtin(name = "upper.tri", min_args = 1)]
1249fn builtin_upper_tri(args: &[RValue], named: &[(String, RValue)]) -> Result<RValue, RError> {
1250    let diag_incl = named
1251        .iter()
1252        .find(|(n, _)| n == "diag")
1253        .and_then(|(_, v)| v.as_vector()?.as_logical_scalar())
1254        .or_else(|| args.get(1)?.as_vector()?.as_logical_scalar())
1255        .unwrap_or(false);
1256    tri_matrix(args, diag_incl, false)
1257}
1258
1259fn tri_matrix(args: &[RValue], diag_incl: bool, lower: bool) -> Result<RValue, RError> {
1260    let (nrow, ncol) = match args.first() {
1261        Some(RValue::Vector(rv)) => match rv.get_attr("dim") {
1262            Some(RValue::Vector(dim_rv)) => match &dim_rv.inner {
1263                Vector::Integer(d) if d.len() >= 2 => (
1264                    usize::try_from(d.get_opt(0).unwrap_or(0)).unwrap_or(0),
1265                    usize::try_from(d.get_opt(1).unwrap_or(0)).unwrap_or(0),
1266                ),
1267                _ => {
1268                    return Err(RError::new(
1269                        RErrorKind::Argument,
1270                        "argument is not a matrix".to_string(),
1271                    ))
1272                }
1273            },
1274            _ => {
1275                return Err(RError::new(
1276                    RErrorKind::Argument,
1277                    "argument is not a matrix".to_string(),
1278                ))
1279            }
1280        },
1281        _ => {
1282            return Err(RError::new(
1283                RErrorKind::Argument,
1284                "argument is not a matrix".to_string(),
1285            ))
1286        }
1287    };
1288
1289    // R stores matrices column-major
1290    let mut result = Vec::with_capacity(nrow * ncol);
1291    for j in 0..ncol {
1292        for i in 0..nrow {
1293            let val = if lower {
1294                if diag_incl {
1295                    i >= j
1296                } else {
1297                    i > j
1298                }
1299            } else if diag_incl {
1300                i <= j
1301            } else {
1302                i < j
1303            };
1304            result.push(Some(val));
1305        }
1306    }
1307    let mut rv = RVector::from(Vector::Logical(result.into()));
1308    set_matrix_attrs(&mut rv, nrow, ncol, None, None)?;
1309    Ok(RValue::Vector(rv))
1310}
1311
1312// === Diagonal matrix ===
1313
1314/// Diagonal of a matrix, or construct a diagonal matrix.
1315///
1316/// If x is a matrix, extracts the diagonal. If x is a scalar n, creates an
1317/// n-by-n identity matrix. If x is a vector, creates a diagonal matrix with
1318/// x on the diagonal.
1319///
1320/// @param x a matrix, scalar, or vector
1321/// @return diagonal vector or diagonal matrix
1322#[builtin(min_args = 1)]
1323fn builtin_diag(args: &[RValue], _: &[(String, RValue)]) -> Result<RValue, RError> {
1324    match args.first() {
1325        Some(RValue::Vector(rv)) => {
1326            // If x is a matrix, extract the diagonal
1327            if let Some(RValue::Vector(dim_rv)) = rv.get_attr("dim") {
1328                if let Vector::Integer(d) = &dim_rv.inner {
1329                    if d.len() >= 2 {
1330                        let nrow = usize::try_from(d.get_opt(0).unwrap_or(0)).unwrap_or(0);
1331                        let ncol = usize::try_from(d.get_opt(1).unwrap_or(0)).unwrap_or(0);
1332                        let data = rv.to_doubles();
1333                        let n = nrow.min(ncol);
1334                        let result: Vec<Option<f64>> = (0..n)
1335                            .map(|i| {
1336                                let idx = i * nrow + i; // column-major: col * nrow + row
1337                                if idx < data.len() {
1338                                    data[idx]
1339                                } else {
1340                                    None
1341                                }
1342                            })
1343                            .collect();
1344                        return Ok(RValue::vec(Vector::Double(result.into())));
1345                    }
1346                }
1347            }
1348
1349            let len = rv.len();
1350            if len == 1 {
1351                // Scalar integer n: create n x n identity matrix
1352                let n = usize::try_from(rv.as_integer_scalar().unwrap_or(1)).unwrap_or(0);
1353                let mut result = vec![Some(0.0); n * n];
1354                for i in 0..n {
1355                    result[i * n + i] = Some(1.0);
1356                }
1357                let mut out = RVector::from(Vector::Double(result.into()));
1358                set_matrix_attrs(&mut out, n, n, None, None)?;
1359                Ok(RValue::Vector(out))
1360            } else {
1361                // Vector: create diagonal matrix from vector
1362                let vals = rv.to_doubles();
1363                let n = vals.len();
1364                let mut result = vec![Some(0.0); n * n];
1365                for (i, v) in vals.iter().enumerate() {
1366                    result[i * n + i] = *v;
1367                }
1368                let mut out = RVector::from(Vector::Double(result.into()));
1369                set_matrix_attrs(&mut out, n, n, None, None)?;
1370                Ok(RValue::Vector(out))
1371            }
1372        }
1373        _ => Err(RError::new(
1374            RErrorKind::Argument,
1375            "'x' must be numeric".to_string(),
1376        )),
1377    }
1378}
1379
1380/// Sum of all elements.
1381///
1382/// @param ... numeric vectors
1383/// @param na.rm logical; if TRUE, remove NAs before summing
1384/// @return scalar double
1385#[builtin(min_args = 0)]
1386fn builtin_sum(args: &[RValue], named: &[(String, RValue)]) -> Result<RValue, RError> {
1387    let na_rm = named.iter().any(|(n, v)| {
1388        n == "na.rm" && v.as_vector().and_then(|v| v.as_logical_scalar()) == Some(true)
1389    });
1390    let mut total = 0.0;
1391    for arg in args {
1392        if let RValue::Vector(v) = arg {
1393            for x in v.to_doubles() {
1394                match x {
1395                    Some(f) => total += f,
1396                    None if !na_rm => return Ok(RValue::vec(Vector::Double(vec![None].into()))),
1397                    None => {}
1398                }
1399            }
1400        }
1401    }
1402    Ok(RValue::vec(Vector::Double(vec![Some(total)].into())))
1403}
1404
1405/// Product of all elements.
1406///
1407/// @param ... numeric vectors
1408/// @param na.rm logical; if TRUE, remove NAs before multiplying
1409/// @return scalar double
1410#[builtin(min_args = 0)]
1411fn builtin_prod(args: &[RValue], named: &[(String, RValue)]) -> Result<RValue, RError> {
1412    let na_rm = named.iter().any(|(n, v)| {
1413        n == "na.rm" && v.as_vector().and_then(|v| v.as_logical_scalar()) == Some(true)
1414    });
1415    let mut total = 1.0;
1416    for arg in args {
1417        if let RValue::Vector(v) = arg {
1418            for x in v.to_doubles() {
1419                match x {
1420                    Some(f) => total *= f,
1421                    None if !na_rm => return Ok(RValue::vec(Vector::Double(vec![None].into()))),
1422                    None => {}
1423                }
1424            }
1425        }
1426    }
1427    Ok(RValue::vec(Vector::Double(vec![Some(total)].into())))
1428}
1429
1430/// Maximum value across all arguments.
1431///
1432/// @param ... numeric vectors
1433/// @param na.rm logical; if TRUE, remove NAs
1434/// @return scalar double (or -Inf if no values)
1435#[builtin(min_args = 0)]
1436fn builtin_max(args: &[RValue], named: &[(String, RValue)]) -> Result<RValue, RError> {
1437    let na_rm = named.iter().any(|(n, v)| {
1438        n == "na.rm" && v.as_vector().and_then(|v| v.as_logical_scalar()) == Some(true)
1439    });
1440    let mut result: Option<f64> = None;
1441    for arg in args {
1442        if let RValue::Vector(v) = arg {
1443            for x in v.to_doubles() {
1444                match x {
1445                    Some(f) => {
1446                        result = Some(match result {
1447                            Some(r) => r.max(f),
1448                            None => f,
1449                        })
1450                    }
1451                    None if !na_rm => return Ok(RValue::vec(Vector::Double(vec![None].into()))),
1452                    None => {}
1453                }
1454            }
1455        }
1456    }
1457    Ok(RValue::vec(Vector::Double(
1458        vec![result.or(Some(f64::NEG_INFINITY))].into(),
1459    )))
1460}
1461
1462/// Minimum value across all arguments.
1463///
1464/// @param ... numeric vectors
1465/// @param na.rm logical; if TRUE, remove NAs
1466/// @return scalar double (or Inf if no values)
1467#[builtin(min_args = 0)]
1468fn builtin_min(args: &[RValue], named: &[(String, RValue)]) -> Result<RValue, RError> {
1469    let na_rm = named.iter().any(|(n, v)| {
1470        n == "na.rm" && v.as_vector().and_then(|v| v.as_logical_scalar()) == Some(true)
1471    });
1472    let mut result: Option<f64> = None;
1473    for arg in args {
1474        if let RValue::Vector(v) = arg {
1475            for x in v.to_doubles() {
1476                match x {
1477                    Some(f) => {
1478                        result = Some(match result {
1479                            Some(r) => r.min(f),
1480                            None => f,
1481                        })
1482                    }
1483                    None if !na_rm => return Ok(RValue::vec(Vector::Double(vec![None].into()))),
1484                    None => {}
1485                }
1486            }
1487        }
1488    }
1489    Ok(RValue::vec(Vector::Double(
1490        vec![result.or(Some(f64::INFINITY))].into(),
1491    )))
1492}
1493
1494/// Arithmetic mean.
1495///
1496/// @param x numeric vector
1497/// @param na.rm logical; if TRUE, remove NAs before averaging
1498/// @return scalar double
1499#[builtin(min_args = 1)]
1500fn builtin_mean(args: &[RValue], named: &[(String, RValue)]) -> Result<RValue, RError> {
1501    let na_rm = named.iter().any(|(n, v)| {
1502        n == "na.rm" && v.as_vector().and_then(|v| v.as_logical_scalar()) == Some(true)
1503    });
1504    match args.first() {
1505        Some(RValue::Vector(v)) => {
1506            let vals = v.to_doubles();
1507            let mut sum = 0.0;
1508            let mut count = 0;
1509            for x in &vals {
1510                match x {
1511                    Some(f) => {
1512                        sum += f;
1513                        count += 1;
1514                    }
1515                    None if !na_rm => return Ok(RValue::vec(Vector::Double(vec![None].into()))),
1516                    None => {}
1517                }
1518            }
1519            if count == 0 {
1520                return Ok(RValue::vec(Vector::Double(vec![Some(f64::NAN)].into())));
1521            }
1522            Ok(RValue::vec(Vector::Double(
1523                vec![Some(sum / usize_to_f64(count))].into(),
1524            )))
1525        }
1526        _ => Ok(RValue::vec(Vector::Double(vec![Some(f64::NAN)].into()))),
1527    }
1528}
1529
1530/// Median value.
1531///
1532/// @param x numeric vector
1533/// @param na.rm logical; if TRUE, remove NAs before computing
1534/// @return scalar double (NA if input contains NAs and na.rm is FALSE)
1535#[builtin(min_args = 1, namespace = "stats")]
1536fn builtin_median(args: &[RValue], named: &[(String, RValue)]) -> Result<RValue, RError> {
1537    let na_rm = named.iter().any(|(n, v)| {
1538        n == "na.rm" && v.as_vector().and_then(|v| v.as_logical_scalar()) == Some(true)
1539    });
1540    match args.first() {
1541        Some(RValue::Vector(v)) => {
1542            let doubles = v.to_doubles();
1543            // Check for NAs before filtering
1544            if !na_rm && doubles.iter().any(|x| x.is_none()) {
1545                return Ok(RValue::vec(Vector::Double(vec![None].into())));
1546            }
1547            let mut vals: Vec<f64> = doubles.into_iter().flatten().collect();
1548            if vals.is_empty() {
1549                return Ok(RValue::vec(Vector::Double(vec![Some(f64::NAN)].into())));
1550            }
1551            vals.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
1552            let mid = vals.len() / 2;
1553            let median = if vals.len().is_multiple_of(2) {
1554                (vals[mid - 1] + vals[mid]) / 2.0
1555            } else {
1556                vals[mid]
1557            };
1558            Ok(RValue::vec(Vector::Double(vec![Some(median)].into())))
1559        }
1560        _ => Ok(RValue::vec(Vector::Double(vec![Some(f64::NAN)].into()))),
1561    }
1562}
1563
1564/// Sample variance (Bessel-corrected, divides by n-1).
1565///
1566/// @param x numeric vector
1567/// @param na.rm logical; if TRUE, remove NAs before computing
1568/// @return scalar double (NA if input contains NAs and na.rm is FALSE)
1569#[builtin(min_args = 1, namespace = "stats")]
1570fn builtin_var(args: &[RValue], named: &[(String, RValue)]) -> Result<RValue, RError> {
1571    let na_rm = named.iter().any(|(n, v)| {
1572        n == "na.rm" && v.as_vector().and_then(|v| v.as_logical_scalar()) == Some(true)
1573    });
1574    match args.first() {
1575        Some(RValue::Vector(v)) => {
1576            let doubles = v.to_doubles();
1577            // Check for NAs before filtering
1578            if !na_rm && doubles.iter().any(|x| x.is_none()) {
1579                return Ok(RValue::vec(Vector::Double(vec![None].into())));
1580            }
1581            let vals: Vec<f64> = doubles.into_iter().flatten().collect();
1582            let n = usize_to_f64(vals.len());
1583            if n < 2.0 {
1584                return Ok(RValue::vec(Vector::Double(vec![Some(f64::NAN)].into())));
1585            }
1586            let mean = vals.iter().sum::<f64>() / n;
1587            let var = vals.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / (n - 1.0);
1588            Ok(RValue::vec(Vector::Double(vec![Some(var)].into())))
1589        }
1590        _ => Ok(RValue::vec(Vector::Double(vec![Some(f64::NAN)].into()))),
1591    }
1592}
1593
1594/// Sample standard deviation (square root of var).
1595///
1596/// @param x numeric vector
1597/// @return scalar double
1598#[builtin(min_args = 1, namespace = "stats")]
1599fn builtin_sd(args: &[RValue], named: &[(String, RValue)]) -> Result<RValue, RError> {
1600    match builtin_var(args, named)? {
1601        RValue::Vector(rv) => match rv.inner {
1602            Vector::Double(v) => Ok(RValue::vec(Vector::Double(
1603                v.iter_opt()
1604                    .map(|x| x.map(f64::sqrt))
1605                    .collect::<Vec<_>>()
1606                    .into(),
1607            ))),
1608            other => Ok(RValue::vec(other)),
1609        },
1610        other => Ok(other),
1611    }
1612}
1613
1614/// Cumulative sum.
1615///
1616/// Once an NA is encountered, all subsequent values become NA.
1617///
1618/// @param x numeric vector
1619/// @return numeric vector of running sums
1620#[builtin(min_args = 1)]
1621fn builtin_cumsum(args: &[RValue], _: &[(String, RValue)]) -> Result<RValue, RError> {
1622    match args.first() {
1623        Some(RValue::Vector(v)) => {
1624            let mut acc = 0.0;
1625            let mut seen_na = false;
1626            let result: Vec<Option<f64>> = v
1627                .to_doubles()
1628                .iter()
1629                .map(|x| {
1630                    if seen_na {
1631                        return None;
1632                    }
1633                    match *x {
1634                        Some(f) => {
1635                            acc += f;
1636                            Some(acc)
1637                        }
1638                        None => {
1639                            seen_na = true;
1640                            None
1641                        }
1642                    }
1643                })
1644                .collect();
1645            Ok(RValue::vec(Vector::Double(result.into())))
1646        }
1647        _ => Err(RError::new(
1648            RErrorKind::Argument,
1649            "invalid argument".to_string(),
1650        )),
1651    }
1652}
1653
1654/// Cumulative product.
1655///
1656/// Once an NA is encountered, all subsequent values become NA.
1657///
1658/// @param x numeric vector
1659/// @return numeric vector of running products
1660#[builtin(min_args = 1)]
1661fn builtin_cumprod(args: &[RValue], _: &[(String, RValue)]) -> Result<RValue, RError> {
1662    match args.first() {
1663        Some(RValue::Vector(v)) => {
1664            let mut acc = 1.0;
1665            let mut seen_na = false;
1666            let result: Vec<Option<f64>> = v
1667                .to_doubles()
1668                .iter()
1669                .map(|x| {
1670                    if seen_na {
1671                        return None;
1672                    }
1673                    match *x {
1674                        Some(f) => {
1675                            acc *= f;
1676                            Some(acc)
1677                        }
1678                        None => {
1679                            seen_na = true;
1680                            None
1681                        }
1682                    }
1683                })
1684                .collect();
1685            Ok(RValue::vec(Vector::Double(result.into())))
1686        }
1687        _ => Err(RError::new(
1688            RErrorKind::Argument,
1689            "invalid argument".to_string(),
1690        )),
1691    }
1692}
1693
1694/// Cumulative maximum.
1695///
1696/// Once an NA is encountered, all subsequent values become NA.
1697///
1698/// @param x numeric vector
1699/// @return numeric vector of running maxima
1700#[builtin(min_args = 1)]
1701fn builtin_cummax(args: &[RValue], _: &[(String, RValue)]) -> Result<RValue, RError> {
1702    match args.first() {
1703        Some(RValue::Vector(v)) => {
1704            let mut acc = f64::NEG_INFINITY;
1705            let mut seen_na = false;
1706            let result: Vec<Option<f64>> = v
1707                .to_doubles()
1708                .iter()
1709                .map(|x| {
1710                    if seen_na {
1711                        return None;
1712                    }
1713                    match *x {
1714                        Some(f) => {
1715                            acc = acc.max(f);
1716                            Some(acc)
1717                        }
1718                        None => {
1719                            seen_na = true;
1720                            None
1721                        }
1722                    }
1723                })
1724                .collect();
1725            Ok(RValue::vec(Vector::Double(result.into())))
1726        }
1727        _ => Err(RError::new(
1728            RErrorKind::Argument,
1729            "invalid argument".to_string(),
1730        )),
1731    }
1732}
1733
1734/// Cumulative minimum.
1735///
1736/// Once an NA is encountered, all subsequent values become NA.
1737///
1738/// @param x numeric vector
1739/// @return numeric vector of running minima
1740#[builtin(min_args = 1)]
1741fn builtin_cummin(args: &[RValue], _: &[(String, RValue)]) -> Result<RValue, RError> {
1742    match args.first() {
1743        Some(RValue::Vector(v)) => {
1744            let mut acc = f64::INFINITY;
1745            let mut seen_na = false;
1746            let result: Vec<Option<f64>> = v
1747                .to_doubles()
1748                .iter()
1749                .map(|x| {
1750                    if seen_na {
1751                        return None;
1752                    }
1753                    match *x {
1754                        Some(f) => {
1755                            acc = acc.min(f);
1756                            Some(acc)
1757                        }
1758                        None => {
1759                            seen_na = true;
1760                            None
1761                        }
1762                    }
1763                })
1764                .collect();
1765            Ok(RValue::vec(Vector::Double(result.into())))
1766        }
1767        _ => Err(RError::new(
1768            RErrorKind::Argument,
1769            "invalid argument".to_string(),
1770        )),
1771    }
1772}
1773
1774/// Generate a regular sequence.
1775///
1776/// @param from starting value (default 1)
1777/// @param to ending value (default 1)
1778/// @param by increment (default 1 or -1)
1779/// @param length.out desired length of the sequence
1780/// @return numeric vector
1781#[builtin(min_args = 0, names = ["seq.int"])]
1782fn builtin_seq(args: &[RValue], named: &[(String, RValue)]) -> Result<RValue, RError> {
1783    let from = named
1784        .iter()
1785        .find(|(n, _)| n == "from")
1786        .map(|(_, v)| v)
1787        .or(args.first())
1788        .and_then(|v| v.as_vector()?.as_double_scalar())
1789        .unwrap_or(1.0);
1790    let to = named
1791        .iter()
1792        .find(|(n, _)| n == "to")
1793        .map(|(_, v)| v)
1794        .or(args.get(1))
1795        .and_then(|v| v.as_vector()?.as_double_scalar())
1796        .unwrap_or(1.0);
1797    let by = named
1798        .iter()
1799        .find(|(n, _)| n == "by")
1800        .map(|(_, v)| v)
1801        .or(args.get(2))
1802        .and_then(|v| v.as_vector()?.as_double_scalar());
1803    let length_out = named
1804        .iter()
1805        .find(|(n, _)| n == "length.out")
1806        .map(|(_, v)| v)
1807        .and_then(|v| v.as_vector()?.as_integer_scalar());
1808
1809    if let Some(len) = length_out {
1810        let len = usize::try_from(len)?;
1811        if len == 0 {
1812            return Ok(RValue::vec(Vector::Double(vec![].into())));
1813        }
1814        if len == 1 {
1815            return Ok(RValue::vec(Vector::Double(vec![Some(from)].into())));
1816        }
1817        let step = (to - from) / usize_to_f64(len - 1);
1818        let result: Vec<Option<f64>> = (0..len)
1819            .map(|i| Some(from + step * usize_to_f64(i)))
1820            .collect();
1821        return Ok(RValue::vec(Vector::Double(result.into())));
1822    }
1823
1824    let by = by.unwrap_or(if to >= from { 1.0 } else { -1.0 });
1825    if by == 0.0 {
1826        return Err(RError::new(
1827            RErrorKind::Argument,
1828            "'by' argument must not be zero".to_string(),
1829        ));
1830    }
1831
1832    let mut result = Vec::new();
1833    let mut val = from;
1834    if by > 0.0 {
1835        while val <= to + f64::EPSILON * 100.0 {
1836            result.push(Some(val));
1837            val += by;
1838        }
1839    } else {
1840        while val >= to - f64::EPSILON * 100.0 {
1841            result.push(Some(val));
1842            val += by;
1843        }
1844    }
1845    Ok(RValue::vec(Vector::Double(result.into())))
1846}
1847
1848/// Generate 1:n as an integer vector.
1849///
1850/// @param length.out the desired length
1851/// @return integer vector 1, 2, ..., n
1852#[builtin(name = "seq_len", min_args = 1)]
1853fn builtin_seq_len(args: &[RValue], _: &[(String, RValue)]) -> Result<RValue, RError> {
1854    let n = args
1855        .first()
1856        .and_then(|v| v.as_vector()?.as_integer_scalar())
1857        .unwrap_or(0);
1858    let result: Vec<Option<i64>> = (1..=n).map(Some).collect();
1859    Ok(RValue::vec(Vector::Integer(result.into())))
1860}
1861
1862/// Generate 1:length(along.with) as an integer vector.
1863///
1864/// @param along.with the object whose length determines the sequence
1865/// @return integer vector 1, 2, ..., length(along.with)
1866#[builtin(name = "seq_along", min_args = 1)]
1867fn builtin_seq_along(args: &[RValue], _: &[(String, RValue)]) -> Result<RValue, RError> {
1868    let n = args.first().map(|v| v.length()).unwrap_or(0);
1869    let n_i64 = i64::try_from(n)?;
1870    let result: Vec<Option<i64>> = (1..=n_i64).map(Some).collect();
1871    Ok(RValue::vec(Vector::Integer(result.into())))
1872}
1873
1874/// Replicate elements of a vector.
1875///
1876/// @param x a vector
1877/// @param times number of times to repeat the whole vector (default 1)
1878/// @param each number of times to repeat each element before moving to the next
1879/// @param length.out desired output length (truncates or extends the result)
1880/// @return vector with elements repeated
1881#[builtin(min_args = 1)]
1882fn builtin_rep(args: &[RValue], named: &[(String, RValue)]) -> Result<RValue, RError> {
1883    let each = named
1884        .iter()
1885        .find(|(n, _)| n == "each")
1886        .and_then(|(_, v)| v.as_vector()?.as_integer_scalar());
1887
1888    let length_out = named
1889        .iter()
1890        .find(|(n, _)| n == "length.out")
1891        .and_then(|(_, v)| v.as_vector()?.as_integer_scalar());
1892
1893    let times = named
1894        .iter()
1895        .find(|(n, _)| n == "times")
1896        .and_then(|(_, v)| v.as_vector()?.as_integer_scalar())
1897        .or_else(|| {
1898            // Only use positional arg 1 for times if `each` is not set
1899            if each.is_none() {
1900                args.get(1)?.as_vector()?.as_integer_scalar()
1901            } else {
1902                None
1903            }
1904        })
1905        .unwrap_or(1);
1906    let times = usize::try_from(times)?;
1907
1908    let each_n = each.map(usize::try_from).transpose()?.unwrap_or(1);
1909
1910    match args.first() {
1911        Some(RValue::Vector(v)) => {
1912            let result = rep_vector(&v.inner, each_n, times, length_out)?;
1913            Ok(RValue::vec(result))
1914        }
1915        _ => Err(RError::new(
1916            RErrorKind::Argument,
1917            "invalid argument".to_string(),
1918        )),
1919    }
1920}
1921
1922/// Helper: replicate a vector with `each`, `times`, and optional `length.out`.
1923///
1924/// Algorithm: first apply `each` (repeat each element), then apply `times`
1925/// (repeat the whole result), then truncate/extend to `length.out` if given.
1926fn rep_vector(
1927    v: &Vector,
1928    each: usize,
1929    times: usize,
1930    length_out: Option<i64>,
1931) -> Result<Vector, RError> {
1932    // Apply `each` first, then `times`, using the doubles representation as a
1933    // type-agnostic strategy won't work — we need to preserve the original type.
1934    // Instead, convert to doubles, do the replication, then figure out the type.
1935    // Actually, let's handle it per-type to preserve type fidelity.
1936    let result = match v {
1937        Vector::Double(vals) => {
1938            let opt_vec = vals.to_option_vec();
1939            let expanded = rep_each_then_times(opt_vec.as_slice(), each, times);
1940            Vector::Double(apply_length_out_cloneable(expanded, length_out)?.into())
1941        }
1942        Vector::Integer(vals) => {
1943            let opt_vec = vals.to_option_vec();
1944            let expanded = rep_each_then_times(opt_vec.as_slice(), each, times);
1945            Vector::Integer(apply_length_out_cloneable(expanded, length_out)?.into())
1946        }
1947        Vector::Logical(vals) => {
1948            let expanded = rep_each_then_times(vals.as_slice(), each, times);
1949            Vector::Logical(apply_length_out_cloneable(expanded, length_out)?.into())
1950        }
1951        Vector::Character(vals) => {
1952            let expanded = rep_each_then_times(vals.as_slice(), each, times);
1953            Vector::Character(apply_length_out_cloneable(expanded, length_out)?.into())
1954        }
1955        Vector::Complex(vals) => {
1956            let expanded = rep_each_then_times(vals.as_slice(), each, times);
1957            Vector::Complex(apply_length_out_cloneable(expanded, length_out)?.into())
1958        }
1959        Vector::Raw(vals) => {
1960            let expanded = rep_each_then_times_copy(vals, each, times);
1961            let final_vec = if let Some(lo) = length_out {
1962                let lo = usize::try_from(lo)?;
1963                if expanded.is_empty() {
1964                    vec![]
1965                } else {
1966                    expanded.iter().cycle().take(lo).copied().collect()
1967                }
1968            } else {
1969                expanded
1970            };
1971            Vector::Raw(final_vec)
1972        }
1973    };
1974    Ok(result)
1975}
1976
1977/// Repeat each element `each` times, then repeat the whole result `times` times.
1978fn rep_each_then_times<T: Clone>(vals: &[T], each: usize, times: usize) -> Vec<T> {
1979    let with_each: Vec<T> = if each <= 1 {
1980        vals.to_vec()
1981    } else {
1982        vals.iter()
1983            .flat_map(|v| std::iter::repeat_n(v.clone(), each))
1984            .collect()
1985    };
1986    if times <= 1 {
1987        with_each
1988    } else {
1989        let len = with_each.len();
1990        with_each.into_iter().cycle().take(len * times).collect()
1991    }
1992}
1993
1994/// Same as `rep_each_then_times` but for Copy types (Raw/u8).
1995fn rep_each_then_times_copy<T: Copy>(vals: &[T], each: usize, times: usize) -> Vec<T> {
1996    let with_each: Vec<T> = if each <= 1 {
1997        vals.to_vec()
1998    } else {
1999        vals.iter()
2000            .flat_map(|&v| std::iter::repeat_n(v, each))
2001            .collect()
2002    };
2003    if times <= 1 {
2004        with_each
2005    } else {
2006        let len = with_each.len();
2007        with_each.into_iter().cycle().take(len * times).collect()
2008    }
2009}
2010
2011/// Truncate or cycle-extend a Vec<T: Clone> to the requested `length.out`.
2012fn apply_length_out_cloneable<T: Clone>(
2013    v: Vec<T>,
2014    length_out: Option<i64>,
2015) -> Result<Vec<T>, RError> {
2016    match length_out {
2017        None => Ok(v),
2018        Some(lo) => {
2019            let lo = usize::try_from(lo)?;
2020            if v.is_empty() {
2021                return Ok(vec![]);
2022            }
2023            Ok(v.iter().cycle().take(lo).cloned().collect())
2024        }
2025    }
2026}
2027
2028/// Reverse a vector.
2029///
2030/// @param x a vector
2031/// @return vector with elements in reverse order
2032#[builtin(min_args = 1)]
2033fn builtin_rev(args: &[RValue], _: &[(String, RValue)]) -> Result<RValue, RError> {
2034    match args.first() {
2035        Some(RValue::Vector(v)) => {
2036            let result = match &v.inner {
2037                Vector::Raw(vals) => {
2038                    let mut v = vals.clone();
2039                    v.reverse();
2040                    Vector::Raw(v)
2041                }
2042                Vector::Double(vals) => {
2043                    let mut v = vals.clone();
2044                    v.reverse();
2045                    Vector::Double(v)
2046                }
2047                Vector::Integer(vals) => {
2048                    let mut v = vals.clone();
2049                    v.reverse();
2050                    Vector::Integer(v)
2051                }
2052                Vector::Logical(vals) => {
2053                    let mut v = vals.clone();
2054                    v.reverse();
2055                    Vector::Logical(v)
2056                }
2057                Vector::Complex(vals) => {
2058                    let mut v = vals.clone();
2059                    v.reverse();
2060                    Vector::Complex(v)
2061                }
2062                Vector::Character(vals) => {
2063                    let mut v = vals.clone();
2064                    v.reverse();
2065                    Vector::Character(v)
2066                }
2067            };
2068            Ok(RValue::vec(result))
2069        }
2070        _ => Ok(RValue::Null),
2071    }
2072}
2073
2074/// Sort a vector.
2075///
2076/// @param x a vector
2077/// @param decreasing logical; sort in descending order? (default FALSE)
2078/// @param na.last logical; TRUE puts NAs at end, FALSE at beginning, NA removes them (default TRUE)
2079/// @return sorted vector
2080#[builtin(min_args = 1)]
2081fn builtin_sort(args: &[RValue], named: &[(String, RValue)]) -> Result<RValue, RError> {
2082    let decreasing = named
2083        .iter()
2084        .find(|(n, _)| n == "decreasing")
2085        .and_then(|(_, v)| v.as_vector()?.as_logical_scalar())
2086        .unwrap_or(false);
2087
2088    // na.last: TRUE (default) = NAs at end, FALSE = NAs at beginning, NA = remove NAs
2089    // R's sort() defaults to na.last = NA (remove NAs), unlike order() which defaults to TRUE
2090    let na_last_val = named.iter().find(|(n, _)| n == "na.last").map(|(_, v)| v);
2091    let na_last: Option<bool> = match na_last_val {
2092        Some(RValue::Vector(rv)) => rv.inner.as_logical_scalar(),
2093        Some(RValue::Null) => None,
2094        None => None, // R default for sort is na.last=NA (remove NAs)
2095        _ => None,
2096    };
2097
2098    match args.first() {
2099        Some(RValue::Vector(v)) => {
2100            let result = match &v.inner {
2101                Vector::Double(vals) => {
2102                    let opt_vec = vals.to_option_vec();
2103                    let (mut non_na, na_count) = partition_na_doubles(&opt_vec);
2104                    non_na.sort_by(|a, b| {
2105                        if decreasing {
2106                            b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
2107                        } else {
2108                            a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)
2109                        }
2110                    });
2111                    let result = reassemble_with_na(
2112                        non_na.into_iter().map(Some).collect(),
2113                        na_count,
2114                        na_last,
2115                    );
2116                    Vector::Double(result.into())
2117                }
2118                Vector::Integer(vals) => {
2119                    let opt_vec = vals.to_option_vec();
2120                    let (mut non_na, na_count) = partition_na_options(&opt_vec);
2121                    non_na.sort_by(|a, b| if decreasing { b.cmp(a) } else { a.cmp(b) });
2122                    let result = reassemble_with_na(
2123                        non_na.into_iter().map(Some).collect(),
2124                        na_count,
2125                        na_last,
2126                    );
2127                    Vector::Integer(result.into())
2128                }
2129                Vector::Character(vals) => {
2130                    let (mut non_na, na_count) = partition_na_options(vals.as_slice());
2131                    non_na.sort_by(|a, b| if decreasing { b.cmp(a) } else { a.cmp(b) });
2132                    let result = reassemble_with_na(
2133                        non_na.into_iter().map(Some).collect(),
2134                        na_count,
2135                        na_last,
2136                    );
2137                    Vector::Character(result.into())
2138                }
2139                other => other.clone(),
2140            };
2141            Ok(RValue::vec(result))
2142        }
2143        _ => Ok(RValue::Null),
2144    }
2145}
2146
2147/// Separate non-NA f64 values from NA count.
2148fn partition_na_doubles(vals: &[Option<f64>]) -> (Vec<f64>, usize) {
2149    let mut non_na = Vec::with_capacity(vals.len());
2150    let mut na_count = 0;
2151    for v in vals {
2152        match v {
2153            Some(f) if !f.is_nan() => non_na.push(*f),
2154            _ => na_count += 1,
2155        }
2156    }
2157    (non_na, na_count)
2158}
2159
2160/// Separate non-NA Option values from NA count for Clone types.
2161fn partition_na_options<T: Clone>(vals: &[Option<T>]) -> (Vec<T>, usize) {
2162    let mut non_na = Vec::with_capacity(vals.len());
2163    let mut na_count = 0;
2164    for v in vals {
2165        match v {
2166            Some(x) => non_na.push(x.clone()),
2167            None => na_count += 1,
2168        }
2169    }
2170    (non_na, na_count)
2171}
2172
2173/// Reassemble sorted non-NA values with NAs placed according to `na_last`.
2174///
2175/// - `Some(true)` — NAs go at end
2176/// - `Some(false)` — NAs go at beginning
2177/// - `None` — NAs are removed
2178fn reassemble_with_na<T: Clone>(
2179    sorted: Vec<Option<T>>,
2180    na_count: usize,
2181    na_last: Option<bool>,
2182) -> Vec<Option<T>> {
2183    match na_last {
2184        None => sorted, // remove NAs
2185        Some(true) => {
2186            let mut result = sorted;
2187            result.extend(std::iter::repeat_n(None, na_count));
2188            result
2189        }
2190        Some(false) => {
2191            let mut result: Vec<Option<T>> = std::iter::repeat_n(None, na_count).collect();
2192            result.extend(sorted);
2193            result
2194        }
2195    }
2196}
2197
2198/// Permutation which rearranges a vector into ascending order.
2199///
2200/// @param x a vector
2201/// @return integer vector of 1-based indices
2202#[builtin(min_args = 1)]
2203fn builtin_order(args: &[RValue], _: &[(String, RValue)]) -> Result<RValue, RError> {
2204    match args.first() {
2205        Some(RValue::Vector(v)) => {
2206            let doubles = v.to_doubles();
2207            let mut indices: Vec<usize> = (0..doubles.len()).collect();
2208            indices.sort_by(|&a, &b| {
2209                let va = doubles[a].unwrap_or(f64::NAN);
2210                let vb = doubles[b].unwrap_or(f64::NAN);
2211                va.partial_cmp(&vb).unwrap_or(std::cmp::Ordering::Equal)
2212            });
2213            let result: Vec<Option<i64>> = indices
2214                .iter()
2215                .map(|&i| Some(i64::try_from(i).unwrap_or(0) + 1))
2216                .collect();
2217            Ok(RValue::vec(Vector::Integer(result.into())))
2218        }
2219        _ => Ok(RValue::Null),
2220    }
2221}
2222
2223/// Return the ranks of values in a numeric vector.
2224///
2225/// Computes the sample ranks of the values. Ties are handled according to
2226/// the `ties.method` argument: "average" (default) assigns the mean rank,
2227/// "first" preserves the original order, "min" uses the minimum rank, and
2228/// "max" uses the maximum rank.
2229///
2230/// @param x numeric vector to rank
2231/// @param ties.method character string: "average", "first", "min", or "max"
2232/// @return double vector of ranks (1-based)
2233#[builtin(min_args = 1)]
2234fn builtin_rank(args: &[RValue], named: &[(String, RValue)]) -> Result<RValue, RError> {
2235    let ties_method = named
2236        .iter()
2237        .find(|(n, _)| n == "ties.method")
2238        .and_then(|(_, v)| v.as_vector()?.as_character_scalar())
2239        .unwrap_or_else(|| "average".to_string());
2240
2241    let vals = match args.first() {
2242        Some(RValue::Vector(v)) => v.to_doubles(),
2243        _ => return Ok(RValue::Null),
2244    };
2245
2246    let n = vals.len();
2247    if n == 0 {
2248        return Ok(RValue::vec(Vector::Double(Vec::new().into())));
2249    }
2250
2251    // Build (value, original_index) pairs and sort by value.
2252    // NA values sort last (matching R's na.last = TRUE default).
2253    let mut indexed: Vec<(Option<f64>, usize)> = vals.iter().copied().zip(0..n).collect();
2254    indexed.sort_by(|a, b| match (a.0, b.0) {
2255        (None, None) => std::cmp::Ordering::Equal,
2256        (None, _) => std::cmp::Ordering::Greater,
2257        (_, None) => std::cmp::Ordering::Less,
2258        (Some(va), Some(vb)) => va.partial_cmp(&vb).unwrap_or(std::cmp::Ordering::Equal),
2259    });
2260
2261    // Assign ranks based on ties method
2262    let mut ranks = vec![0.0f64; n];
2263    match ties_method.as_str() {
2264        "first" => {
2265            for (rank_minus_1, &(_, orig_idx)) in indexed.iter().enumerate() {
2266                ranks[orig_idx] = (rank_minus_1 + 1) as f64;
2267            }
2268        }
2269        "min" | "max" | "average" => {
2270            let mut i = 0;
2271            while i < n {
2272                // Find the run of tied values
2273                let mut j = i + 1;
2274                while j < n {
2275                    let same = match (indexed[i].0, indexed[j].0) {
2276                        (None, None) => true,
2277                        (Some(a), Some(b)) => a == b,
2278                        _ => false,
2279                    };
2280                    if !same {
2281                        break;
2282                    }
2283                    j += 1;
2284                }
2285                // Ranks for this tied group are i+1 .. j (1-based)
2286                let rank = match ties_method.as_str() {
2287                    "min" => (i + 1) as f64,
2288                    "max" => j as f64,
2289                    _ => {
2290                        // "average": mean of ranks i+1 .. j
2291                        let sum: f64 = ((i + 1)..=j).map(|r| r as f64).sum();
2292                        sum / (j - i) as f64
2293                    }
2294                };
2295                for item in indexed.iter().take(j).skip(i) {
2296                    ranks[item.1] = rank;
2297                }
2298                i = j;
2299            }
2300        }
2301        other => {
2302            return Err(RError::new(
2303                RErrorKind::Argument,
2304                format!(
2305                "ties.method must be one of \"average\", \"first\", \"min\", or \"max\", not {:?}",
2306                other
2307            ),
2308            ))
2309        }
2310    }
2311
2312    let result: Vec<Option<f64>> = ranks.into_iter().map(Some).collect();
2313    Ok(RValue::vec(Vector::Double(result.into())))
2314}
2315
2316/// Remove duplicate elements, preserving first occurrence order.
2317///
2318/// @param x a vector
2319/// @return vector of unique elements
2320#[builtin(min_args = 1)]
2321fn builtin_unique(args: &[RValue], _: &[(String, RValue)]) -> Result<RValue, RError> {
2322    match args.first() {
2323        Some(RValue::Vector(v)) => {
2324            let result = match &v.inner {
2325                Vector::Raw(vals) => {
2326                    let mut seen = Vec::new();
2327                    let mut result = Vec::new();
2328                    for &x in vals.iter() {
2329                        if !seen.contains(&x) {
2330                            seen.push(x);
2331                            result.push(x);
2332                        }
2333                    }
2334                    Vector::Raw(result)
2335                }
2336                Vector::Double(vals) => {
2337                    let mut seen = Vec::new();
2338                    let mut result = Vec::new();
2339                    for x in vals.iter_opt() {
2340                        let key = format!("{:?}", x);
2341                        if !seen.contains(&key) {
2342                            seen.push(key);
2343                            result.push(x);
2344                        }
2345                    }
2346                    Vector::Double(result.into())
2347                }
2348                Vector::Integer(vals) => {
2349                    let mut seen: Vec<Option<i64>> = Vec::new();
2350                    let mut result = Vec::new();
2351                    for x in vals.iter_opt() {
2352                        if !seen.contains(&x) {
2353                            seen.push(x);
2354                            result.push(x);
2355                        }
2356                    }
2357                    Vector::Integer(result.into())
2358                }
2359                Vector::Character(vals) => {
2360                    let mut seen: Vec<Option<String>> = Vec::new();
2361                    let mut result = Vec::new();
2362                    for x in vals.iter() {
2363                        if !seen.contains(x) {
2364                            seen.push(x.clone());
2365                            result.push(x.clone());
2366                        }
2367                    }
2368                    Vector::Character(result.into())
2369                }
2370                Vector::Complex(vals) => {
2371                    let mut seen = Vec::new();
2372                    let mut result = Vec::new();
2373                    for x in vals.iter() {
2374                        let key = format!("{:?}", x);
2375                        if !seen.contains(&key) {
2376                            seen.push(key);
2377                            result.push(*x);
2378                        }
2379                    }
2380                    Vector::Complex(result.into())
2381                }
2382                Vector::Logical(vals) => {
2383                    let mut seen = Vec::new();
2384                    let mut result = Vec::new();
2385                    for x in vals.iter() {
2386                        if !seen.contains(x) {
2387                            seen.push(*x);
2388                            result.push(*x);
2389                        }
2390                    }
2391                    Vector::Logical(result.into())
2392                }
2393            };
2394            Ok(RValue::vec(result))
2395        }
2396        _ => Ok(RValue::Null),
2397    }
2398}
2399
2400/// miniR extension: single-pass sorted unique using BTreeSet.
2401/// Faster than `sort(unique(x))` for large vectors.
2402#[builtin(name = "sort_unique", min_args = 1)]
2403fn builtin_sort_unique(args: &[RValue], named: &[(String, RValue)]) -> Result<RValue, RError> {
2404    let decreasing = named
2405        .iter()
2406        .find(|(n, _)| n == "decreasing")
2407        .and_then(|(_, v)| v.as_vector()?.as_logical_scalar())
2408        .unwrap_or(false);
2409
2410    match args.first() {
2411        Some(RValue::Vector(v)) => {
2412            let result = match &v.inner {
2413                Vector::Double(vals) => {
2414                    // Total ordering key: flip bits so BTreeSet gives numeric order.
2415                    // Positive floats: bits as-is. Negative floats: flip all bits.
2416                    // This maps f64 ordering to u64 ordering. NAs (None) sort last.
2417                    fn sort_key(v: Option<f64>) -> (bool, u64) {
2418                        match v {
2419                            None => (true, 0), // NAs last
2420                            Some(f) => {
2421                                let bits = f.to_bits();
2422                                let key = if bits >> 63 == 1 {
2423                                    !bits
2424                                } else {
2425                                    bits ^ (1 << 63)
2426                                };
2427                                (false, key)
2428                            }
2429                        }
2430                    }
2431                    let set: BTreeSet<(bool, u64)> = vals.iter_opt().map(sort_key).collect();
2432                    // Reverse-map keys back to f64 values
2433                    let mut result: Vec<Option<f64>> = set
2434                        .into_iter()
2435                        .map(|(is_na, key)| {
2436                            if is_na {
2437                                None
2438                            } else {
2439                                let bits = if key >> 63 == 0 {
2440                                    !key
2441                                } else {
2442                                    key ^ (1 << 63)
2443                                };
2444                                Some(f64::from_bits(bits))
2445                            }
2446                        })
2447                        .collect();
2448                    if decreasing {
2449                        result.reverse();
2450                    }
2451                    Vector::Double(result.into())
2452                }
2453                Vector::Integer(vals) => {
2454                    let has_na = vals.iter_opt().any(|x| x.is_none());
2455                    let set: BTreeSet<i64> = vals.iter_opt().flatten().collect();
2456                    let mut result: Vec<Option<i64>> = set.into_iter().map(Some).collect();
2457                    if decreasing {
2458                        result.reverse();
2459                    }
2460                    if has_na {
2461                        result.push(None); // NAs sort last
2462                    }
2463                    Vector::Integer(result.into())
2464                }
2465                Vector::Character(vals) => {
2466                    let has_na = vals.iter().any(|x| x.is_none());
2467                    let set: BTreeSet<&str> = vals.iter().filter_map(|x| x.as_deref()).collect();
2468                    let mut result: Vec<Option<String>> =
2469                        set.into_iter().map(|s| Some(s.to_string())).collect();
2470                    if decreasing {
2471                        result.reverse();
2472                    }
2473                    if has_na {
2474                        result.push(None); // NAs sort last
2475                    }
2476                    Vector::Character(result.into())
2477                }
2478                other => other.clone(),
2479            };
2480            Ok(RValue::vec(result))
2481        }
2482        _ => Ok(RValue::Null),
2483    }
2484}
2485
2486/// Indices of TRUE elements.
2487///
2488/// When `arr.ind = TRUE` and the input has a `dim` attribute (matrix),
2489/// returns a matrix of row/column subscripts instead of linear indices.
2490///
2491/// @param x logical vector or matrix
2492/// @param arr.ind if TRUE, return matrix subscripts for array input
2493/// @return integer vector of 1-based indices, or integer matrix of subscripts
2494#[builtin(min_args = 1)]
2495fn builtin_which(args: &[RValue], named: &[(String, RValue)]) -> Result<RValue, RError> {
2496    let arr_ind = named
2497        .iter()
2498        .find(|(n, _)| n == "arr.ind")
2499        .and_then(|(_, v)| v.as_vector().and_then(|v| v.as_logical_scalar()))
2500        .unwrap_or(false);
2501
2502    match args.first() {
2503        Some(RValue::Vector(rv)) => {
2504            let logicals = rv.to_logicals();
2505            let true_indices: Vec<i64> = logicals
2506                .iter()
2507                .enumerate()
2508                .filter_map(|(i, v)| {
2509                    if *v == Some(true) {
2510                        Some(i64::try_from(i).unwrap_or(0) + 1)
2511                    } else {
2512                        None
2513                    }
2514                })
2515                .collect();
2516
2517            // Check for arr.ind with matrix dim attribute
2518            if arr_ind {
2519                if let Some(RValue::Vector(dim_rv)) = rv.get_attr("dim") {
2520                    let dims = dim_rv.to_integers();
2521                    if dims.len() >= 2 {
2522                        let n = true_indices.len();
2523                        let ndim = dims.len();
2524                        let dim_sizes: Vec<i64> = dims.iter().map(|d| d.unwrap_or(0)).collect();
2525                        let mut result: Vec<Option<i64>> = vec![None; n * ndim];
2526
2527                        for (idx_pos, &linear_1based) in true_indices.iter().enumerate() {
2528                            let mut linear = linear_1based - 1;
2529                            for d in 0..ndim {
2530                                let subscript = linear % dim_sizes[d];
2531                                result[d * n + idx_pos] = Some(subscript + 1);
2532                                linear /= dim_sizes[d];
2533                            }
2534                        }
2535
2536                        let mut result_rv = RVector::from(Vector::Integer(result.into()));
2537                        result_rv.set_attr(
2538                            "dim".to_string(),
2539                            RValue::vec(Vector::Integer(
2540                                vec![Some(i64::try_from(n)?), Some(i64::try_from(ndim)?)].into(),
2541                            )),
2542                        );
2543                        // Add dimnames with "row" and "col" column names (R compat)
2544                        let col_names: Vec<Option<String>> = (0..ndim)
2545                            .map(|d| {
2546                                Some(if d == 0 {
2547                                    "row".to_string()
2548                                } else if d == 1 {
2549                                    "col".to_string()
2550                                } else {
2551                                    format!("dim{}", d + 1)
2552                                })
2553                            })
2554                            .collect();
2555                        result_rv.set_attr(
2556                            "dimnames".to_string(),
2557                            RValue::List(RList::new(vec![
2558                                (None, RValue::Null),
2559                                (None, RValue::vec(Vector::Character(col_names.into()))),
2560                            ])),
2561                        );
2562                        return Ok(RValue::Vector(result_rv));
2563                    }
2564                }
2565            }
2566
2567            let result: Vec<Option<i64>> = true_indices.into_iter().map(Some).collect();
2568            Ok(RValue::vec(Vector::Integer(result.into())))
2569        }
2570        _ => Ok(RValue::vec(Vector::Integer(vec![].into()))),
2571    }
2572}
2573
2574/// Index of the minimum element.
2575///
2576/// @param x numeric vector
2577/// @return scalar integer (1-based index)
2578#[builtin(min_args = 1)]
2579fn builtin_which_min(args: &[RValue], _: &[(String, RValue)]) -> Result<RValue, RError> {
2580    match args.first() {
2581        Some(RValue::Vector(v)) => {
2582            let doubles = v.to_doubles();
2583            let mut min_idx = None;
2584            let mut min_val = f64::INFINITY;
2585            for (i, x) in doubles.iter().enumerate() {
2586                if let Some(f) = x {
2587                    if *f < min_val {
2588                        min_val = *f;
2589                        min_idx = Some(i);
2590                    }
2591                }
2592            }
2593            Ok(RValue::vec(Vector::Integer(
2594                vec![min_idx.map(|i| i64::try_from(i).unwrap_or(0) + 1)].into(),
2595            )))
2596        }
2597        _ => Ok(RValue::vec(Vector::Integer(vec![].into()))),
2598    }
2599}
2600
2601/// Index of the maximum element.
2602///
2603/// @param x numeric vector
2604/// @return scalar integer (1-based index)
2605#[builtin(min_args = 1)]
2606fn builtin_which_max(args: &[RValue], _: &[(String, RValue)]) -> Result<RValue, RError> {
2607    match args.first() {
2608        Some(RValue::Vector(v)) => {
2609            let doubles = v.to_doubles();
2610            let mut max_idx = None;
2611            let mut max_val = f64::NEG_INFINITY;
2612            for (i, x) in doubles.iter().enumerate() {
2613                if let Some(f) = x {
2614                    if *f > max_val {
2615                        max_val = *f;
2616                        max_idx = Some(i);
2617                    }
2618                }
2619            }
2620            Ok(RValue::vec(Vector::Integer(
2621                vec![max_idx.map(|i| i64::try_from(i).unwrap_or(0) + 1)].into(),
2622            )))
2623        }
2624        _ => Ok(RValue::vec(Vector::Integer(vec![].into()))),
2625    }
2626}
2627
2628/// Append elements to a vector.
2629///
2630/// Concatenates `values` into `x` at position `after`. Type coercion follows
2631/// R's hierarchy: raw < logical < integer < double < complex < character.
2632///
2633/// @param x a vector
2634/// @param values values to append
2635/// @param after index after which to insert (default: end of x)
2636/// @return concatenated vector preserving the highest type
2637#[builtin(min_args = 2)]
2638fn builtin_append(args: &[RValue], named: &[(String, RValue)]) -> Result<RValue, RError> {
2639    match (args.first(), args.get(1)) {
2640        (Some(RValue::Vector(v1)), Some(RValue::Vector(v2))) => {
2641            // Determine `after` position (1-based, default = length of x)
2642            let x_len = v1.inner.len();
2643            let after = named
2644                .iter()
2645                .find(|(k, _)| k == "after")
2646                .map(|(_, v)| v)
2647                .or(args.get(2))
2648                .and_then(|v| v.as_vector()?.as_integer_scalar())
2649                .map(|a| usize::try_from(a.max(0)).unwrap_or(0).min(x_len))
2650                .unwrap_or(x_len);
2651
2652            // Determine the highest type between both vectors
2653            let has_char = matches!(v1.inner, Vector::Character(_))
2654                || matches!(v2.inner, Vector::Character(_));
2655            let has_complex =
2656                matches!(v1.inner, Vector::Complex(_)) || matches!(v2.inner, Vector::Complex(_));
2657            let has_double =
2658                matches!(v1.inner, Vector::Double(_)) || matches!(v2.inner, Vector::Double(_));
2659            let has_int =
2660                matches!(v1.inner, Vector::Integer(_)) || matches!(v2.inner, Vector::Integer(_));
2661
2662            let result = if has_char {
2663                let x = v1.to_characters();
2664                let vals = v2.to_characters();
2665                let mut out = x[..after].to_vec();
2666                out.extend(vals);
2667                out.extend_from_slice(&x[after..]);
2668                Vector::Character(out.into())
2669            } else if has_complex {
2670                let x = v1.inner.to_complex();
2671                let vals = v2.inner.to_complex();
2672                let mut out = x[..after].to_vec();
2673                out.extend(vals);
2674                out.extend_from_slice(&x[after..]);
2675                Vector::Complex(out.into())
2676            } else if has_double {
2677                let x = v1.to_doubles();
2678                let vals = v2.to_doubles();
2679                let mut out = x[..after].to_vec();
2680                out.extend(vals);
2681                out.extend_from_slice(&x[after..]);
2682                Vector::Double(out.into())
2683            } else if has_int {
2684                let x = v1.to_integers();
2685                let vals = v2.to_integers();
2686                let mut out = x[..after].to_vec();
2687                out.extend(vals);
2688                out.extend_from_slice(&x[after..]);
2689                Vector::Integer(out.into())
2690            } else {
2691                // Both logical (or raw)
2692                let x = v1.to_logicals();
2693                let vals = v2.to_logicals();
2694                let mut out = x[..after].to_vec();
2695                out.extend(vals);
2696                out.extend_from_slice(&x[after..]);
2697                Vector::Logical(out.into())
2698            };
2699            Ok(RValue::vec(result))
2700        }
2701        (Some(RValue::List(l1)), Some(RValue::List(l2))) => {
2702            let x_len = l1.values.len();
2703            let after = named
2704                .iter()
2705                .find(|(k, _)| k == "after")
2706                .map(|(_, v)| v)
2707                .or(args.get(2))
2708                .and_then(|v| v.as_vector()?.as_integer_scalar())
2709                .map(|a| usize::try_from(a.max(0)).unwrap_or(0).min(x_len))
2710                .unwrap_or(x_len);
2711            let mut out = l1.values[..after].to_vec();
2712            out.extend(l2.values.clone());
2713            out.extend_from_slice(&l1.values[after..]);
2714            Ok(RValue::List(RList::new(out)))
2715        }
2716        _ => Ok(args.first().cloned().unwrap_or(RValue::Null)),
2717    }
2718}
2719
2720/// Return the first n elements of a vector.
2721///
2722/// @param x a vector
2723/// @param n number of elements to return (default 6)
2724/// @return vector of the first n elements
2725#[builtin(min_args = 1, namespace = "utils")]
2726fn builtin_head(args: &[RValue], named: &[(String, RValue)]) -> Result<RValue, RError> {
2727    let n = named
2728        .iter()
2729        .find(|(k, _)| k == "n")
2730        .map(|(_, v)| v)
2731        .or(args.get(1))
2732        .and_then(|v| v.as_vector()?.as_integer_scalar())
2733        .unwrap_or(6);
2734    let n = usize::try_from(n).unwrap_or(0);
2735    match args.first() {
2736        Some(RValue::Vector(v)) => {
2737            let result = match &v.inner {
2738                Vector::Raw(vals) => Vector::Raw(vals[..n.min(vals.len())].to_vec()),
2739                Vector::Double(vals) => Vector::Double(vals.slice(0, n.min(vals.len()))),
2740                Vector::Integer(vals) => Vector::Integer(vals.slice(0, n.min(vals.len()))),
2741                Vector::Logical(vals) => Vector::Logical(vals[..n.min(vals.len())].to_vec().into()),
2742                Vector::Complex(vals) => Vector::Complex(vals[..n.min(vals.len())].to_vec().into()),
2743                Vector::Character(vals) => {
2744                    Vector::Character(vals[..n.min(vals.len())].to_vec().into())
2745                }
2746            };
2747            Ok(RValue::vec(result))
2748        }
2749        _ => Ok(RValue::Null),
2750    }
2751}
2752
2753/// Return the last n elements of a vector.
2754///
2755/// @param x a vector
2756/// @param n number of elements to return (default 6)
2757/// @return vector of the last n elements
2758#[builtin(min_args = 1, namespace = "utils")]
2759fn builtin_tail(args: &[RValue], named: &[(String, RValue)]) -> Result<RValue, RError> {
2760    let n = named
2761        .iter()
2762        .find(|(k, _)| k == "n")
2763        .map(|(_, v)| v)
2764        .or(args.get(1))
2765        .and_then(|v| v.as_vector()?.as_integer_scalar())
2766        .unwrap_or(6);
2767    let n = usize::try_from(n).unwrap_or(0);
2768    match args.first() {
2769        Some(RValue::Vector(v)) => {
2770            let len = v.len();
2771            let start = len.saturating_sub(n);
2772            let result = match &v.inner {
2773                Vector::Raw(vals) => Vector::Raw(vals[start..].to_vec()),
2774                Vector::Double(vals) => Vector::Double(vals.slice(start, vals.len() - start)),
2775                Vector::Integer(vals) => Vector::Integer(vals.slice(start, vals.len() - start)),
2776                Vector::Logical(vals) => Vector::Logical(vals[start..].to_vec().into()),
2777                Vector::Complex(vals) => Vector::Complex(vals[start..].to_vec().into()),
2778                Vector::Character(vals) => Vector::Character(vals[start..].to_vec().into()),
2779            };
2780            Ok(RValue::vec(result))
2781        }
2782        _ => Ok(RValue::Null),
2783    }
2784}
2785
2786/// Range (minimum and maximum) of all values.
2787///
2788/// @param ... numeric vectors
2789/// @param na.rm logical; if TRUE, remove NAs before computing
2790/// @return numeric vector of length 2: c(min, max) (c(NA, NA) if NAs present and na.rm is FALSE)
2791#[builtin(min_args = 0)]
2792fn builtin_range(args: &[RValue], named: &[(String, RValue)]) -> Result<RValue, RError> {
2793    let na_rm = named.iter().any(|(n, v)| {
2794        n == "na.rm" && v.as_vector().and_then(|v| v.as_logical_scalar()) == Some(true)
2795    });
2796    let mut min = f64::INFINITY;
2797    let mut max = f64::NEG_INFINITY;
2798    for arg in args {
2799        if let RValue::Vector(v) = arg {
2800            for x in v.to_doubles() {
2801                match x {
2802                    Some(f) => {
2803                        if f < min {
2804                            min = f;
2805                        }
2806                        if f > max {
2807                            max = f;
2808                        }
2809                    }
2810                    None if !na_rm => {
2811                        return Ok(RValue::vec(Vector::Double(vec![None, None].into())));
2812                    }
2813                    None => {}
2814                }
2815            }
2816        }
2817    }
2818    Ok(RValue::vec(Vector::Double(
2819        vec![Some(min), Some(max)].into(),
2820    )))
2821}
2822
2823/// Lagged differences.
2824///
2825/// @param x numeric vector
2826/// @param lag the lag to use (default 1)
2827/// @param differences number of times to apply differencing (default 1)
2828/// @return numeric vector of length(x) - lag * differences
2829#[builtin(min_args = 1)]
2830fn builtin_diff(args: &[RValue], named: &[(String, RValue)]) -> Result<RValue, RError> {
2831    let lag = named
2832        .iter()
2833        .find(|(n, _)| n == "lag")
2834        .map(|(_, v)| v)
2835        .or(args.get(1))
2836        .and_then(|v| v.as_vector()?.as_integer_scalar())
2837        .unwrap_or(1);
2838    let lag = usize::try_from(lag).map_err(|_| {
2839        RError::new(
2840            RErrorKind::Argument,
2841            format!("'lag' must be a positive integer, got {lag}"),
2842        )
2843    })?;
2844    if lag == 0 {
2845        return Err(RError::new(
2846            RErrorKind::Argument,
2847            "'lag' must be a positive integer, got 0".to_string(),
2848        ));
2849    }
2850
2851    let differences = named
2852        .iter()
2853        .find(|(n, _)| n == "differences")
2854        .and_then(|(_, v)| v.as_vector()?.as_integer_scalar())
2855        .unwrap_or(1);
2856    let differences = usize::try_from(differences).map_err(|_| {
2857        RError::new(
2858            RErrorKind::Argument,
2859            format!("'differences' must be a positive integer, got {differences}"),
2860        )
2861    })?;
2862    if differences == 0 {
2863        return Err(RError::new(
2864            RErrorKind::Argument,
2865            "'differences' must be a positive integer, got 0".to_string(),
2866        ));
2867    }
2868
2869    match args.first() {
2870        Some(RValue::Vector(v)) => {
2871            let mut vals = v.to_doubles();
2872            for _ in 0..differences {
2873                if vals.len() <= lag {
2874                    return Ok(RValue::vec(Vector::Double(vec![].into())));
2875                }
2876                vals = (lag..vals.len())
2877                    .map(|i| match (vals[i - lag], vals[i]) {
2878                        (Some(a), Some(b)) => Some(b - a),
2879                        _ => None,
2880                    })
2881                    .collect();
2882            }
2883            Ok(RValue::vec(Vector::Double(vals.into())))
2884        }
2885        _ => Err(RError::new(
2886            RErrorKind::Argument,
2887            "invalid argument".to_string(),
2888        )),
2889    }
2890}
2891
2892/// Sample quantiles.
2893///
2894/// Computes quantiles using type 7 (R default): for probability p and sorted
2895/// data of length n, h = (n-1)*p, result = x[floor(h)] + (h - floor(h)) *
2896/// (x[ceil(h)] - x[floor(h)]).
2897///
2898/// @param x numeric vector
2899/// @param probs numeric vector of probabilities (default: c(0, 0.25, 0.5, 0.75, 1))
2900/// @param na.rm logical; remove NAs before computing? (default FALSE)
2901/// @param type integer; quantile algorithm type (only type 7 supported)
2902/// @return named numeric vector of quantiles
2903#[builtin(min_args = 1)]
2904fn builtin_quantile(args: &[RValue], named: &[(String, RValue)]) -> Result<RValue, RError> {
2905    let na_rm = named
2906        .iter()
2907        .find(|(n, _)| n == "na.rm")
2908        .and_then(|(_, v)| v.as_vector()?.as_logical_scalar())
2909        .unwrap_or(false);
2910
2911    let qtype = named
2912        .iter()
2913        .find(|(n, _)| n == "type")
2914        .and_then(|(_, v)| v.as_vector()?.as_integer_scalar())
2915        .unwrap_or(7);
2916    if qtype != 7 {
2917        return Err(RError::new(
2918            RErrorKind::Argument,
2919            format!(
2920                "only quantile type 7 is currently supported, got type {qtype}. \
2921                 Type 7 is R's default and the most commonly used algorithm."
2922            ),
2923        ));
2924    }
2925
2926    // Parse probs: named arg, positional arg 1, or default c(0, 0.25, 0.5, 0.75, 1)
2927    let default_probs = vec![Some(0.0), Some(0.25), Some(0.5), Some(0.75), Some(1.0)];
2928    let probs: Vec<Option<f64>> = named
2929        .iter()
2930        .find(|(n, _)| n == "probs")
2931        .map(|(_, v)| v)
2932        .or(args.get(1))
2933        .map(|v| match v {
2934            RValue::Vector(rv) => rv.to_doubles(),
2935            RValue::Null => vec![],
2936            _ => default_probs.clone(),
2937        })
2938        .unwrap_or(default_probs);
2939
2940    // Get and sort the data
2941    let vals = match args.first() {
2942        Some(RValue::Vector(v)) => v.to_doubles(),
2943        _ => {
2944            return Err(RError::new(
2945                RErrorKind::Argument,
2946                "non-numeric argument to quantile".to_string(),
2947            ))
2948        }
2949    };
2950
2951    // Filter NAs
2952    let mut data: Vec<f64> = if na_rm {
2953        vals.iter()
2954            .filter_map(|v| *v)
2955            .filter(|f| !f.is_nan())
2956            .collect()
2957    } else {
2958        // If any NA/NaN present without na.rm, check and error
2959        let mut d = Vec::with_capacity(vals.len());
2960        for v in &vals {
2961            match v {
2962                Some(f) if !f.is_nan() => d.push(*f),
2963                _ => {
2964                    return Err(RError::new(
2965                        RErrorKind::Argument,
2966                        "missing values and NaN's not allowed if 'na.rm' is FALSE".to_string(),
2967                    ));
2968                }
2969            }
2970        }
2971        d
2972    };
2973
2974    data.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
2975    let n = data.len();
2976
2977    // Compute quantiles using type 7
2978    let mut result_vals: Vec<Option<f64>> = Vec::with_capacity(probs.len());
2979    let mut names: Vec<Option<String>> = Vec::with_capacity(probs.len());
2980
2981    for prob in &probs {
2982        match prob {
2983            Some(p) => {
2984                if !(0.0..=1.0).contains(p) {
2985                    return Err(RError::new(
2986                        RErrorKind::Argument,
2987                        format!("'probs' outside [0, 1], got {p}"),
2988                    ));
2989                }
2990                if n == 0 {
2991                    result_vals.push(None);
2992                } else if n == 1 {
2993                    result_vals.push(Some(data[0]));
2994                } else {
2995                    let h = (n - 1) as f64 * p;
2996                    let lo = h.floor() as usize;
2997                    let hi = h.ceil() as usize;
2998                    let frac = h - h.floor();
2999                    let val = data[lo] + frac * (data[hi] - data[lo]);
3000                    result_vals.push(Some(val));
3001                }
3002                // Format name: e.g. "0%", "25%", "50%", "75%", "100%"
3003                let pct = p * 100.0;
3004                let name = if pct == pct.floor() {
3005                    format!("{}%", pct as i64)
3006                } else {
3007                    format!("{pct}%")
3008                };
3009                names.push(Some(name));
3010            }
3011            None => {
3012                result_vals.push(None);
3013                names.push(None);
3014            }
3015        }
3016    }
3017
3018    let mut rv = RVector::from(Vector::Double(result_vals.into()));
3019    rv.set_attr(
3020        "names".to_string(),
3021        RValue::vec(Vector::Character(names.into())),
3022    );
3023    Ok(RValue::Vector(rv))
3024}
3025
3026/// Replicate elements to a specified length.
3027///
3028/// @param x a vector
3029/// @param length.out desired output length
3030/// @return vector of the specified length, recycling x as needed
3031#[builtin(name = "rep_len", min_args = 2)]
3032fn builtin_rep_len(args: &[RValue], _: &[(String, RValue)]) -> Result<RValue, RError> {
3033    if args.len() < 2 {
3034        return Err(RError::new(
3035            RErrorKind::Argument,
3036            "need 2 arguments".to_string(),
3037        ));
3038    }
3039    let length_out = args[1]
3040        .as_vector()
3041        .and_then(|v| v.as_integer_scalar())
3042        .unwrap_or(0);
3043    let length_out = usize::try_from(length_out).unwrap_or(0);
3044    match &args[0] {
3045        RValue::Vector(v) => {
3046            if v.is_empty() {
3047                return Ok(RValue::Vector(v.clone()));
3048            }
3049            match &v.inner {
3050                Vector::Raw(vals) => Ok(RValue::vec(Vector::Raw(
3051                    vals.iter().cycle().take(length_out).copied().collect(),
3052                ))),
3053                Vector::Double(vals) => Ok(RValue::vec(Vector::Double(
3054                    vals.iter_opt()
3055                        .cycle()
3056                        .take(length_out)
3057                        .collect::<Vec<_>>()
3058                        .into(),
3059                ))),
3060                Vector::Integer(vals) => Ok(RValue::vec(Vector::Integer(
3061                    vals.iter_opt()
3062                        .cycle()
3063                        .take(length_out)
3064                        .collect::<Vec<_>>()
3065                        .into(),
3066                ))),
3067                Vector::Logical(vals) => Ok(RValue::vec(Vector::Logical(
3068                    vals.iter()
3069                        .cycle()
3070                        .take(length_out)
3071                        .cloned()
3072                        .collect::<Vec<_>>()
3073                        .into(),
3074                ))),
3075                Vector::Complex(vals) => Ok(RValue::vec(Vector::Complex(
3076                    vals.iter()
3077                        .cycle()
3078                        .take(length_out)
3079                        .cloned()
3080                        .collect::<Vec<_>>()
3081                        .into(),
3082                ))),
3083                Vector::Character(vals) => Ok(RValue::vec(Vector::Character(
3084                    vals.iter()
3085                        .cycle()
3086                        .take(length_out)
3087                        .cloned()
3088                        .collect::<Vec<_>>()
3089                        .into(),
3090                ))),
3091            }
3092        }
3093        _ => Err(RError::new(
3094            RErrorKind::Argument,
3095            "invalid argument".to_string(),
3096        )),
3097    }
3098}
3099
3100/// Replicate elements a specified number of times.
3101///
3102/// @param x a vector
3103/// @param times number of times to repeat
3104/// @return vector with elements repeated
3105#[builtin(min_args = 2)]
3106fn builtin_rep_int(args: &[RValue], _: &[(String, RValue)]) -> Result<RValue, RError> {
3107    if args.len() < 2 {
3108        return Err(RError::new(
3109            RErrorKind::Argument,
3110            "need 2 arguments".to_string(),
3111        ));
3112    }
3113    let times = args[1]
3114        .as_vector()
3115        .and_then(|v| v.as_integer_scalar())
3116        .unwrap_or(1);
3117    let times = usize::try_from(times)?;
3118    match &args[0] {
3119        RValue::Vector(v) => match &v.inner {
3120            Vector::Raw(vals) => Ok(RValue::vec(Vector::Raw(
3121                vals.iter()
3122                    .cycle()
3123                    .take(vals.len() * times)
3124                    .copied()
3125                    .collect(),
3126            ))),
3127            Vector::Double(vals) => Ok(RValue::vec(Vector::Double(
3128                vals.iter_opt()
3129                    .cycle()
3130                    .take(vals.len() * times)
3131                    .collect::<Vec<_>>()
3132                    .into(),
3133            ))),
3134            Vector::Integer(vals) => Ok(RValue::vec(Vector::Integer(
3135                vals.iter_opt()
3136                    .cycle()
3137                    .take(vals.len() * times)
3138                    .collect::<Vec<_>>()
3139                    .into(),
3140            ))),
3141            Vector::Logical(vals) => Ok(RValue::vec(Vector::Logical(
3142                vals.iter()
3143                    .cycle()
3144                    .take(vals.len() * times)
3145                    .cloned()
3146                    .collect::<Vec<_>>()
3147                    .into(),
3148            ))),
3149            Vector::Complex(vals) => Ok(RValue::vec(Vector::Complex(
3150                vals.iter()
3151                    .cycle()
3152                    .take(vals.len() * times)
3153                    .cloned()
3154                    .collect::<Vec<_>>()
3155                    .into(),
3156            ))),
3157            Vector::Character(vals) => Ok(RValue::vec(Vector::Character(
3158                vals.iter()
3159                    .cycle()
3160                    .take(vals.len() * times)
3161                    .cloned()
3162                    .collect::<Vec<_>>()
3163                    .into(),
3164            ))),
3165        },
3166        _ => Err(RError::new(
3167            RErrorKind::Argument,
3168            "invalid argument".to_string(),
3169        )),
3170    }
3171}
3172
3173/// Convert an RValue to an ndarray Array2 (column-major)
3174#[cfg(feature = "linalg")]
3175fn rvalue_to_array2(val: &RValue) -> Result<Array2<f64>, RError> {
3176    let (data, dim_attr) = match val {
3177        RValue::Vector(rv) => (rv.to_doubles(), rv.get_attr("dim")),
3178        _ => {
3179            return Err(RError::new(
3180                RErrorKind::Type,
3181                "requires numeric matrix/vector arguments".to_string(),
3182            ))
3183        }
3184    };
3185    let (nrow, ncol) = match dim_attr {
3186        Some(RValue::Vector(rv)) => match &rv.inner {
3187            Vector::Integer(d) if d.len() >= 2 => (
3188                usize::try_from(d.get_opt(0).unwrap_or(0)).unwrap_or(0),
3189                usize::try_from(d.get_opt(1).unwrap_or(0)).unwrap_or(0),
3190            ),
3191            _ => (data.len(), 1),
3192        },
3193        _ => (data.len(), 1),
3194    };
3195    let flat: Vec<f64> = data.iter().map(|x| x.unwrap_or(f64::NAN)).collect();
3196    Array2::from_shape_vec((nrow, ncol).f(), flat)
3197        .map_err(|source| -> RError { MathError::Shape { source }.into() })
3198}
3199
3200/// Convert an ndarray Array2 back to an RValue matrix
3201#[cfg(feature = "linalg")]
3202fn array2_to_rvalue(arr: &Array2<f64>) -> RValue {
3203    let (nrow, ncol) = (arr.nrows(), arr.ncols());
3204    let mut result = Vec::with_capacity(nrow * ncol);
3205    for j in 0..ncol {
3206        for i in 0..nrow {
3207            result.push(Some(arr[[i, j]]));
3208        }
3209    }
3210    let mut rv = RVector::from(Vector::Double(result.into()));
3211    rv.set_attr(
3212        "dim".to_string(),
3213        RValue::vec(Vector::Integer(
3214            vec![
3215                Some(i64::try_from(nrow).unwrap_or(0)),
3216                Some(i64::try_from(ncol).unwrap_or(0)),
3217            ]
3218            .into(),
3219        )),
3220    );
3221    RValue::Vector(rv)
3222}
3223
3224/// Convert an RValue matrix to a nalgebra DMatrix (column-major — zero-copy reorder).
3225#[cfg(feature = "linalg")]
3226fn rvalue_to_dmatrix(val: &RValue) -> Result<DMatrix<f64>, RError> {
3227    let (data, dim_attr) = match val {
3228        RValue::Vector(rv) => (rv.to_doubles(), rv.get_attr("dim")),
3229        _ => {
3230            return Err(RError::new(
3231                RErrorKind::Type,
3232                "requires numeric matrix/vector arguments".to_string(),
3233            ))
3234        }
3235    };
3236    let (nrow, ncol) = match dim_attr {
3237        Some(RValue::Vector(rv)) => match &rv.inner {
3238            Vector::Integer(d) if d.len() >= 2 => (
3239                usize::try_from(d.get_opt(0).unwrap_or(0)).unwrap_or(0),
3240                usize::try_from(d.get_opt(1).unwrap_or(0)).unwrap_or(0),
3241            ),
3242            _ => (data.len(), 1),
3243        },
3244        _ => (data.len(), 1),
3245    };
3246    let flat: Vec<f64> = data.iter().map(|x| x.unwrap_or(f64::NAN)).collect();
3247    // nalgebra DMatrix stores data in column-major order, same as R
3248    Ok(DMatrix::from_vec(nrow, ncol, flat))
3249}
3250
3251/// Convert a nalgebra DMatrix back to an RValue matrix.
3252#[cfg(feature = "linalg")]
3253fn dmatrix_to_rvalue(mat: &DMatrix<f64>) -> RValue {
3254    let nrow = mat.nrows();
3255    let ncol = mat.ncols();
3256    // nalgebra stores column-major, so as_slice() gives us the right order
3257    let data: Vec<Option<f64>> = mat.as_slice().iter().copied().map(Some).collect();
3258    let mut rv = RVector::from(Vector::Double(data.into()));
3259    rv.set_attr(
3260        "dim".to_string(),
3261        RValue::vec(Vector::Integer(
3262            vec![
3263                Some(i64::try_from(nrow).unwrap_or(0)),
3264                Some(i64::try_from(ncol).unwrap_or(0)),
3265            ]
3266            .into(),
3267        )),
3268    );
3269    RValue::Vector(rv)
3270}
3271
3272fn matrix_dimnames(value: &RValue) -> MatrixDimNames {
3273    let dimnames = match value {
3274        RValue::Vector(rv) => rv.get_attr("dimnames"),
3275        RValue::List(list) => list.get_attr("dimnames"),
3276        _ => None,
3277    };
3278
3279    let row_names = super::dimnames_component(dimnames, 0);
3280    let col_names = super::dimnames_component(dimnames, 1);
3281    (row_names, col_names)
3282}
3283
3284fn set_matrix_attrs(
3285    rv: &mut RVector,
3286    nrow: usize,
3287    ncol: usize,
3288    row_names: Option<DimNameVec>,
3289    col_names: Option<DimNameVec>,
3290) -> Result<(), RError> {
3291    rv.set_attr(
3292        "class".to_string(),
3293        RValue::vec(Vector::Character(
3294            vec![Some("matrix".to_string()), Some("array".to_string())].into(),
3295        )),
3296    );
3297    rv.set_attr(
3298        "dim".to_string(),
3299        RValue::vec(Vector::Integer(
3300            vec![Some(i64::try_from(nrow)?), Some(i64::try_from(ncol)?)].into(),
3301        )),
3302    );
3303    if let Some(dimnames) =
3304        super::bind_dimnames_value(row_names.unwrap_or_default(), col_names.unwrap_or_default())
3305    {
3306        rv.set_attr("dimnames".to_string(), dimnames);
3307    }
3308    Ok(())
3309}
3310
3311/// crossprod(x, y) = t(x) %*% y
3312#[cfg(feature = "linalg")]
3313#[builtin(min_args = 1)]
3314fn builtin_crossprod(args: &[RValue], _: &[(String, RValue)]) -> Result<RValue, RError> {
3315    let x = rvalue_to_array2(args.first().unwrap_or(&RValue::Null))?;
3316    let y = if let Some(b) = args.get(1) {
3317        rvalue_to_array2(b)?
3318    } else {
3319        x.clone()
3320    };
3321    let xt = x.t();
3322    let result = xt.dot(&y);
3323    let mut out = match array2_to_rvalue(&result) {
3324        RValue::Vector(rv) => rv,
3325        _ => unreachable!(),
3326    };
3327    let (_, x_col_names) = matrix_dimnames(args.first().unwrap_or(&RValue::Null));
3328    let (_, y_col_names) = args.get(1).map_or((None, None), matrix_dimnames);
3329    set_matrix_attrs(
3330        &mut out,
3331        result.nrows(),
3332        result.ncols(),
3333        x_col_names,
3334        if args.get(1).is_some() {
3335            y_col_names
3336        } else {
3337            matrix_dimnames(args.first().unwrap_or(&RValue::Null)).1
3338        },
3339    )?;
3340    Ok(RValue::Vector(out))
3341}
3342
3343/// tcrossprod(x, y) = x %*% t(y)
3344#[cfg(feature = "linalg")]
3345#[builtin(min_args = 1)]
3346fn builtin_tcrossprod(args: &[RValue], _: &[(String, RValue)]) -> Result<RValue, RError> {
3347    let x = rvalue_to_array2(args.first().unwrap_or(&RValue::Null))?;
3348    let y = if let Some(b) = args.get(1) {
3349        rvalue_to_array2(b)?
3350    } else {
3351        x.clone()
3352    };
3353    let yt = y.t();
3354    let result = x.dot(&yt);
3355    let mut out = match array2_to_rvalue(&result) {
3356        RValue::Vector(rv) => rv,
3357        _ => unreachable!(),
3358    };
3359    let (x_row_names, _) = matrix_dimnames(args.first().unwrap_or(&RValue::Null));
3360    let (y_row_names, _) = args.get(1).map_or((None, None), matrix_dimnames);
3361    set_matrix_attrs(
3362        &mut out,
3363        result.nrows(),
3364        result.ncols(),
3365        x_row_names,
3366        if args.get(1).is_some() {
3367            y_row_names
3368        } else {
3369            matrix_dimnames(args.first().unwrap_or(&RValue::Null)).0
3370        },
3371    )?;
3372    Ok(RValue::Vector(out))
3373}
3374
3375// region: norm, solve, outer
3376
3377/// `norm(x, type = "O")` — matrix/vector norm.
3378///
3379/// Supported types: "O"/"1" (one-norm), "I" (infinity-norm), "F" (Frobenius), "M" (max modulus).
3380#[cfg(feature = "linalg")]
3381#[builtin(min_args = 1)]
3382fn builtin_norm(args: &[RValue], named: &[(String, RValue)]) -> Result<RValue, RError> {
3383    let norm_type = named
3384        .iter()
3385        .find(|(n, _)| n == "type")
3386        .map(|(_, v)| v)
3387        .or(args.get(1))
3388        .and_then(|v| v.as_vector()?.as_character_scalar())
3389        .unwrap_or_else(|| "O".to_string());
3390
3391    let mat = rvalue_to_array2(args.first().unwrap_or(&RValue::Null))?;
3392    let nrow = mat.nrows();
3393    let ncol = mat.ncols();
3394
3395    let result = match norm_type.as_str() {
3396        "O" | "o" | "1" => {
3397            // One-norm: max column sum of absolute values
3398            (0..ncol)
3399                .map(|j| (0..nrow).map(|i| mat[[i, j]].abs()).sum::<f64>())
3400                .fold(f64::NEG_INFINITY, f64::max)
3401        }
3402        "I" | "i" => {
3403            // Infinity-norm: max row sum of absolute values
3404            (0..nrow)
3405                .map(|i| (0..ncol).map(|j| mat[[i, j]].abs()).sum::<f64>())
3406                .fold(f64::NEG_INFINITY, f64::max)
3407        }
3408        "F" | "f" => {
3409            // Frobenius norm: sqrt of sum of squares
3410            mat.iter().map(|x| x * x).sum::<f64>().sqrt()
3411        }
3412        "M" | "m" => {
3413            // Max modulus: max absolute value
3414            mat.iter()
3415                .map(|x| x.abs())
3416                .fold(f64::NEG_INFINITY, f64::max)
3417        }
3418        other => {
3419            return Err(RError::new(
3420                RErrorKind::Argument,
3421                format!(
3422                    "invalid norm type '{}'. Use \"O\" (one-norm), \"I\" (infinity-norm), \
3423                 \"F\" (Frobenius), or \"M\" (max modulus)",
3424                    other
3425                ),
3426            ));
3427        }
3428    };
3429
3430    Ok(RValue::vec(Vector::Double(vec![Some(result)].into())))
3431}
3432
3433/// `solve(a, b)` — solve linear system or compute matrix inverse via LU decomposition.
3434///
3435/// - `solve(a)`: returns the inverse of matrix a
3436/// - `solve(a, b)`: solves the linear system Ax = b
3437///
3438/// Uses nalgebra's LU decomposition with partial pivoting for numerical stability.
3439#[cfg(feature = "linalg")]
3440#[builtin(min_args = 1)]
3441fn builtin_solve(args: &[RValue], named: &[(String, RValue)]) -> Result<RValue, RError> {
3442    let a = rvalue_to_dmatrix(args.first().unwrap_or(&RValue::Null))?;
3443    let nrow = a.nrows();
3444    let ncol = a.ncols();
3445
3446    if nrow != ncol {
3447        return Err(RError::new(
3448            RErrorKind::Argument,
3449            format!(
3450                "solve() requires a square matrix, but got {}x{}. \
3451             Non-square systems need qr.solve() or a least-squares method",
3452                nrow, ncol
3453            ),
3454        ));
3455    }
3456    let n = nrow;
3457
3458    if n == 0 {
3459        return Err(RError::new(
3460            RErrorKind::Argument,
3461            "solve() requires a non-empty matrix".to_string(),
3462        ));
3463    }
3464
3465    let b_arg = named
3466        .iter()
3467        .find(|(name, _)| name == "b")
3468        .map(|(_, v)| v)
3469        .or(args.get(1));
3470
3471    let b = match b_arg {
3472        Some(val) => rvalue_to_dmatrix(val)?,
3473        None => DMatrix::identity(n, n),
3474    };
3475
3476    if b.nrows() != n {
3477        return Err(RError::new(
3478            RErrorKind::Argument,
3479            format!(
3480                "solve(a, b): nrow(a) = {} but nrow(b) = {} — they must match",
3481                n,
3482                b.nrows()
3483            ),
3484        ));
3485    }
3486
3487    let lu = a.lu();
3488    let result = lu.solve(&b).ok_or_else(|| {
3489        RError::other(
3490            "solve(): matrix is singular (or very close to singular). \
3491             Check that your matrix has full rank — its determinant is effectively zero",
3492        )
3493    })?;
3494
3495    Ok(dmatrix_to_rvalue(&result))
3496}
3497
3498/// `det(x)` — matrix determinant via LU decomposition.
3499#[cfg(feature = "linalg")]
3500#[builtin(min_args = 1)]
3501fn builtin_det(args: &[RValue], _: &[(String, RValue)]) -> Result<RValue, RError> {
3502    let a = rvalue_to_dmatrix(args.first().unwrap_or(&RValue::Null))?;
3503    let n = a.nrows();
3504    if n != a.ncols() {
3505        return Err(RError::new(
3506            RErrorKind::Argument,
3507            format!("'x' must be a square matrix, got {}x{}", n, a.ncols()),
3508        ));
3509    }
3510    if n == 0 {
3511        return Ok(RValue::vec(Vector::Double(vec![Some(1.0)].into())));
3512    }
3513
3514    let det = a.lu().determinant();
3515    Ok(RValue::vec(Vector::Double(vec![Some(det)].into())))
3516}
3517
3518/// `chol(x)` — Cholesky decomposition (upper triangular R such that x = R'R).
3519///
3520/// Uses nalgebra's Cholesky decomposition and returns the upper triangular factor.
3521#[cfg(feature = "linalg")]
3522#[builtin(min_args = 1)]
3523fn builtin_chol(args: &[RValue], _: &[(String, RValue)]) -> Result<RValue, RError> {
3524    let a = rvalue_to_dmatrix(args.first().unwrap_or(&RValue::Null))?;
3525    let n = a.nrows();
3526    if n != a.ncols() {
3527        return Err(RError::new(
3528            RErrorKind::Argument,
3529            format!("'x' must be a square matrix, got {}x{}", n, a.ncols()),
3530        ));
3531    }
3532
3533    // nalgebra's cholesky() computes L such that A = L L^T (lower triangular).
3534    // R's chol() returns the upper triangular R such that A = R^T R, so R = L^T.
3535    let chol = a.cholesky().ok_or_else(|| {
3536        RError::other(
3537            "matrix is not positive definite — Cholesky decomposition failed. \
3538             Ensure the matrix is symmetric and all eigenvalues are positive",
3539        )
3540    })?;
3541    let l = chol.l();
3542    let r = l.transpose();
3543
3544    Ok(dmatrix_to_rvalue(&r))
3545}
3546
3547// region: QR, SVD, Eigen decompositions
3548
3549/// `qr(x)` — QR decomposition via nalgebra's column-pivoted QR.
3550///
3551/// Returns a list with class "qr" containing:
3552/// - `$qr`: the compact QR matrix (R in upper triangle)
3553/// - `$rank`: integer rank estimate
3554/// - `$pivot`: integer permutation vector (1-based)
3555/// - `$Q`: the orthogonal Q matrix (for qr.Q() access)
3556#[cfg(feature = "linalg")]
3557#[builtin(min_args = 1)]
3558fn builtin_qr(args: &[RValue], _: &[(String, RValue)]) -> Result<RValue, RError> {
3559    let a = rvalue_to_dmatrix(args.first().unwrap_or(&RValue::Null))?;
3560    let m = a.nrows();
3561    let n = a.ncols();
3562
3563    let qr = a.col_piv_qr();
3564    let q = qr.q();
3565    let r = qr.r();
3566
3567    // Build compact QR representation: upper triangle = R, zeros below
3568    let k = m.min(n);
3569    let mut compact = DMatrix::<f64>::zeros(m, n);
3570    for i in 0..m {
3571        for j in 0..n {
3572            if i <= j {
3573                compact[(i, j)] = r[(i, j)];
3574            }
3575        }
3576    }
3577    let qr_val = dmatrix_to_rvalue(&compact);
3578
3579    // Estimate rank from diagonal of R
3580    let tol = f64::EPSILON * (m.max(n) as f64) * {
3581        let mut max_diag = 0.0f64;
3582        for i in 0..k {
3583            max_diag = max_diag.max(r[(i, i)].abs());
3584        }
3585        max_diag
3586    };
3587    let mut rank = 0i64;
3588    for i in 0..k {
3589        if r[(i, i)].abs() > tol {
3590            rank += 1;
3591        }
3592    }
3593
3594    // Pivot vector (1-based): apply the column permutation to get column ordering.
3595    // We apply the permutation to a row matrix of column indices.
3596    let p = qr.p();
3597    let mut pivot_mat = DMatrix::<f64>::zeros(1, n);
3598    for j in 0..n {
3599        pivot_mat[(0, j)] = j as f64;
3600    }
3601    p.permute_columns(&mut pivot_mat);
3602    let pivot: Vec<Option<i64>> = (0..n).map(|j| Some(pivot_mat[(0, j)] as i64 + 1)).collect();
3603
3604    let q_val = dmatrix_to_rvalue(&q);
3605
3606    let mut list = RList::new(vec![
3607        (Some("qr".to_string()), qr_val),
3608        (
3609            Some("rank".to_string()),
3610            RValue::vec(Vector::Integer(vec![Some(rank)].into())),
3611        ),
3612        (
3613            Some("pivot".to_string()),
3614            RValue::vec(Vector::Integer(pivot.into())),
3615        ),
3616        (Some("Q".to_string()), q_val),
3617    ]);
3618    list.set_attr(
3619        "class".to_string(),
3620        RValue::vec(Vector::Character(vec![Some("qr".to_string())].into())),
3621    );
3622    Ok(RValue::List(list))
3623}
3624
3625/// `svd(x)` — Singular Value Decomposition via nalgebra's bidiagonal SVD.
3626///
3627/// Returns a list with:
3628/// - `$d`: numeric vector of singular values (descending)
3629/// - `$u`: left singular vectors (m x min(m,n) matrix)
3630/// - `$v`: right singular vectors (n x min(m,n) matrix)
3631#[cfg(feature = "linalg")]
3632#[builtin(min_args = 1)]
3633fn builtin_svd(args: &[RValue], _: &[(String, RValue)]) -> Result<RValue, RError> {
3634    let a = rvalue_to_dmatrix(args.first().unwrap_or(&RValue::Null))?;
3635    let m = a.nrows();
3636    let n = a.ncols();
3637
3638    if m == 0 || n == 0 {
3639        return Err(RError::new(
3640            RErrorKind::Argument,
3641            "0 extent dimensions are not allowed".to_string(),
3642        ));
3643    }
3644
3645    let svd = a.svd(true, true);
3646    let k = m.min(n);
3647
3648    // Singular values (already in descending order from nalgebra)
3649    let d_vals: Vec<Option<f64>> = svd.singular_values.iter().copied().map(Some).collect();
3650
3651    // Left singular vectors: m x k
3652    let u_full = svd
3653        .u
3654        .ok_or_else(|| RError::other("svd(): failed to compute left singular vectors"))?;
3655    let u = u_full.columns(0, k).clone_owned();
3656
3657    // Right singular vectors: n x k
3658    let v_t_full = svd
3659        .v_t
3660        .ok_or_else(|| RError::other("svd(): failed to compute right singular vectors"))?;
3661    let v = v_t_full.rows(0, k).transpose();
3662
3663    Ok(RValue::List(RList::new(vec![
3664        (
3665            Some("d".to_string()),
3666            RValue::vec(Vector::Double(d_vals.into())),
3667        ),
3668        (Some("u".to_string()), dmatrix_to_rvalue(&u)),
3669        (Some("v".to_string()), dmatrix_to_rvalue(&v)),
3670    ])))
3671}
3672
3673/// `eigen(x)` — Eigenvalue decomposition via nalgebra.
3674///
3675/// Returns a list with:
3676/// - `$values`: numeric vector of eigenvalues (descending by absolute value)
3677/// - `$vectors`: matrix of eigenvectors (columns)
3678///
3679/// Supports both symmetric and non-symmetric real matrices.
3680/// For symmetric matrices, uses nalgebra's `symmetric_eigen()` (faster, all-real).
3681/// For non-symmetric matrices, uses Schur decomposition to extract real eigenvalues,
3682/// or reports complex eigenvalues as an error (R returns complex values, which we
3683/// don't yet support in this context).
3684#[cfg(feature = "linalg")]
3685#[builtin(min_args = 1)]
3686fn builtin_eigen(args: &[RValue], _: &[(String, RValue)]) -> Result<RValue, RError> {
3687    let a = rvalue_to_dmatrix(args.first().unwrap_or(&RValue::Null))?;
3688    let n = a.nrows();
3689    if n != a.ncols() {
3690        return Err(RError::new(
3691            RErrorKind::Argument,
3692            format!(
3693                "non-square matrix in 'eigen': {}x{} — eigen() requires a square matrix",
3694                n,
3695                a.ncols()
3696            ),
3697        ));
3698    }
3699
3700    if n == 0 {
3701        return Ok(RValue::List(RList::new(vec![
3702            (
3703                Some("values".to_string()),
3704                RValue::vec(Vector::Double(vec![].into())),
3705            ),
3706            (
3707                Some("vectors".to_string()),
3708                dmatrix_to_rvalue(&DMatrix::<f64>::zeros(0, 0)),
3709            ),
3710        ])));
3711    }
3712
3713    // Check symmetry
3714    let sym_tol = 1e-10;
3715    let mut is_symmetric = true;
3716    'sym_check: for i in 0..n {
3717        for j in (i + 1)..n {
3718            if (a[(i, j)] - a[(j, i)]).abs() > sym_tol * (a[(i, j)].abs() + a[(j, i)].abs() + 1.0) {
3719                is_symmetric = false;
3720                break 'sym_check;
3721            }
3722        }
3723    }
3724
3725    if is_symmetric {
3726        // Use optimized symmetric eigendecomposition
3727        let eig = a.symmetric_eigen();
3728
3729        // nalgebra returns eigenvalues in arbitrary order — sort descending
3730        let mut eigen_pairs: Vec<(f64, usize)> = eig
3731            .eigenvalues
3732            .iter()
3733            .copied()
3734            .enumerate()
3735            .map(|(i, v)| (v, i))
3736            .collect();
3737        eigen_pairs.sort_by(|a_pair, b_pair| {
3738            b_pair
3739                .0
3740                .partial_cmp(&a_pair.0)
3741                .unwrap_or(std::cmp::Ordering::Equal)
3742        });
3743
3744        let values: Vec<Option<f64>> = eigen_pairs.iter().map(|&(val, _)| Some(val)).collect();
3745
3746        let mut vectors = DMatrix::<f64>::zeros(n, n);
3747        for (new_j, &(_, old_j)) in eigen_pairs.iter().enumerate() {
3748            for i in 0..n {
3749                vectors[(i, new_j)] = eig.eigenvectors[(i, old_j)];
3750            }
3751        }
3752
3753        Ok(RValue::List(RList::new(vec![
3754            (
3755                Some("values".to_string()),
3756                RValue::vec(Vector::Double(values.into())),
3757            ),
3758            (Some("vectors".to_string()), dmatrix_to_rvalue(&vectors)),
3759        ])))
3760    } else {
3761        // Non-symmetric: use Schur decomposition to extract eigenvalues.
3762        // The real Schur form has eigenvalues on the diagonal (for real eigenvalues)
3763        // or in 2x2 blocks (for complex conjugate pairs).
3764        let schur = a.schur();
3765        let (u_mat, t_mat) = schur.unpack();
3766
3767        // Extract eigenvalues from diagonal/2x2 blocks of the quasi-triangular T
3768        let mut values = Vec::new();
3769        let mut has_complex = false;
3770        let mut i = 0;
3771        while i < n {
3772            if i + 1 < n && t_mat[(i + 1, i)].abs() > 1e-10 {
3773                // 2x2 block: complex conjugate pair
3774                has_complex = true;
3775                i += 2;
3776            } else {
3777                values.push(t_mat[(i, i)]);
3778                i += 1;
3779            }
3780        }
3781
3782        if has_complex {
3783            return Err(RError::other(
3784                "non-symmetric matrix has complex eigenvalues — \
3785                 complex eigenvalue support is not yet implemented. \
3786                 If the matrix should be symmetric, consider using (x + t(x))/2 \
3787                 to symmetrize it.",
3788            ));
3789        }
3790
3791        // Sort descending by value
3792        values.sort_by(|a_val, b_val| {
3793            b_val
3794                .partial_cmp(a_val)
3795                .unwrap_or(std::cmp::Ordering::Equal)
3796        });
3797
3798        let vals: Vec<Option<f64>> = values.iter().copied().map(Some).collect();
3799
3800        // Use the Schur vectors as approximate eigenvectors
3801        let vectors = u_mat;
3802
3803        Ok(RValue::List(RList::new(vec![
3804            (
3805                Some("values".to_string()),
3806                RValue::vec(Vector::Double(vals.into())),
3807            ),
3808            (Some("vectors".to_string()), dmatrix_to_rvalue(&vectors)),
3809        ])))
3810    }
3811}
3812
3813// endregion
3814
3815/// `t(x)` — matrix transpose.
3816#[builtin(min_args = 1)]
3817fn builtin_t(args: &[RValue], _: &[(String, RValue)]) -> Result<RValue, RError> {
3818    let x = args.first().unwrap_or(&RValue::Null);
3819    let rv = match x {
3820        RValue::Vector(rv) => rv,
3821        _ => {
3822            return Err(RError::new(
3823                RErrorKind::Type,
3824                "argument is not a matrix".to_string(),
3825            ))
3826        }
3827    };
3828
3829    let (nrow, ncol) = match rv.get_attr("dim") {
3830        Some(RValue::Vector(dim_rv)) => {
3831            let dims = dim_rv.to_integers();
3832            if dims.len() >= 2 {
3833                (
3834                    usize::try_from(dims[0].unwrap_or(0))?,
3835                    usize::try_from(dims[1].unwrap_or(0))?,
3836                )
3837            } else {
3838                // Treat as column vector
3839                (rv.inner.len(), 1)
3840            }
3841        }
3842        _ => (rv.inner.len(), 1), // Treat as column vector
3843    };
3844
3845    // Build transposed flat indices: original[i,j] = flat[j*nrow + i] → transposed[j,i] = flat[i*ncol + j]
3846    let indices: Vec<usize> = (0..nrow)
3847        .flat_map(|i| (0..ncol).map(move |j| j * nrow + i))
3848        .collect();
3849    let transposed = rv.inner.select_indices(&indices);
3850
3851    let mut out = RVector::from(transposed);
3852
3853    // Swap dimnames
3854    let (row_names, col_names) = matrix_dimnames(x);
3855    set_matrix_attrs(&mut out, ncol, nrow, col_names, row_names)?;
3856    Ok(RValue::Vector(out))
3857}
3858
3859// endregion
3860
3861// region: Complex number builtins
3862
3863/// Construct complex numbers from real and imaginary parts.
3864///
3865/// @param real numeric vector of real parts
3866/// @param imaginary numeric vector of imaginary parts
3867/// @param length.out desired output length
3868/// @return complex vector
3869#[builtin(name = "complex")]
3870fn builtin_complex(_args: &[RValue], named: &[(String, RValue)]) -> Result<RValue, RError> {
3871    let real = named
3872        .iter()
3873        .find(|(n, _)| n == "real")
3874        .and_then(|(_, v)| v.as_vector())
3875        .map(|v| v.to_doubles())
3876        .unwrap_or_default();
3877
3878    let imaginary = named
3879        .iter()
3880        .find(|(n, _)| n == "imaginary")
3881        .and_then(|(_, v)| v.as_vector())
3882        .map(|v| v.to_doubles())
3883        .unwrap_or_default();
3884
3885    let length_out = match named
3886        .iter()
3887        .find(|(n, _)| n == "length.out")
3888        .and_then(|(_, v)| v.as_vector()?.as_integer_scalar())
3889    {
3890        Some(n) => usize::try_from(n)?,
3891        None => real.len().max(imaginary.len()),
3892    };
3893
3894    let result: Vec<Option<num_complex::Complex64>> = (0..length_out)
3895        .map(|i| {
3896            let re = real
3897                .get(i % real.len().max(1))
3898                .copied()
3899                .flatten()
3900                .unwrap_or(0.0);
3901            let im = imaginary
3902                .get(i % imaginary.len().max(1))
3903                .copied()
3904                .flatten()
3905                .unwrap_or(0.0);
3906            Some(num_complex::Complex64::new(re, im))
3907        })
3908        .collect();
3909
3910    Ok(RValue::vec(Vector::Complex(result.into())))
3911}
3912
3913/// Extract the real part of complex numbers.
3914///
3915/// @param z complex or numeric vector
3916/// @return numeric vector of real parts
3917#[builtin(name = "Re", min_args = 1)]
3918fn builtin_re(args: &[RValue], _: &[(String, RValue)]) -> Result<RValue, RError> {
3919    let v = args
3920        .first()
3921        .and_then(|v| v.as_vector())
3922        .ok_or_else(|| RError::new(RErrorKind::Type, "non-numeric argument to Re".to_string()))?;
3923    match v {
3924        Vector::Complex(vals) => {
3925            let result: Vec<Option<f64>> = vals.iter().map(|x| x.map(|c| c.re)).collect();
3926            Ok(RValue::vec(Vector::Double(result.into())))
3927        }
3928        _ => {
3929            // For non-complex, Re is identity (the real part of a real number is itself)
3930            Ok(RValue::vec(Vector::Double(v.to_doubles().into())))
3931        }
3932    }
3933}
3934
3935/// Extract the imaginary part of complex numbers.
3936///
3937/// @param z complex or numeric vector
3938/// @return numeric vector of imaginary parts (0 for reals)
3939#[builtin(name = "Im", min_args = 1)]
3940fn builtin_im(args: &[RValue], _: &[(String, RValue)]) -> Result<RValue, RError> {
3941    let v = args
3942        .first()
3943        .and_then(|v| v.as_vector())
3944        .ok_or_else(|| RError::new(RErrorKind::Type, "non-numeric argument to Im".to_string()))?;
3945    match v {
3946        Vector::Complex(vals) => {
3947            let result: Vec<Option<f64>> = vals.iter().map(|x| x.map(|c| c.im)).collect();
3948            Ok(RValue::vec(Vector::Double(result.into())))
3949        }
3950        _ => {
3951            // For non-complex, Im is 0
3952            let result: Vec<Option<f64>> = vec![Some(0.0); v.len()];
3953            Ok(RValue::vec(Vector::Double(result.into())))
3954        }
3955    }
3956}
3957
3958/// Modulus (absolute value) of complex numbers.
3959///
3960/// @param z complex or numeric vector
3961/// @return numeric vector of moduli
3962#[builtin(name = "Mod", min_args = 1)]
3963fn builtin_mod_complex(args: &[RValue], _: &[(String, RValue)]) -> Result<RValue, RError> {
3964    let v = args
3965        .first()
3966        .and_then(|v| v.as_vector())
3967        .ok_or_else(|| RError::new(RErrorKind::Type, "non-numeric argument to Mod".to_string()))?;
3968    match v {
3969        Vector::Complex(vals) => {
3970            let result: Vec<Option<f64>> = vals.iter().map(|x| x.map(|c| c.norm())).collect();
3971            Ok(RValue::vec(Vector::Double(result.into())))
3972        }
3973        _ => {
3974            // For non-complex, Mod is abs
3975            let result: Vec<Option<f64>> = v.to_doubles().iter().map(|x| x.map(f64::abs)).collect();
3976            Ok(RValue::vec(Vector::Double(result.into())))
3977        }
3978    }
3979}
3980
3981/// Argument (phase angle) of complex numbers, in radians.
3982///
3983/// @param z complex or numeric vector
3984/// @return numeric vector of arguments (0 for non-negative reals, pi for negative)
3985#[builtin(name = "Arg", min_args = 1)]
3986fn builtin_arg(args: &[RValue], _: &[(String, RValue)]) -> Result<RValue, RError> {
3987    let v = args
3988        .first()
3989        .and_then(|v| v.as_vector())
3990        .ok_or_else(|| RError::new(RErrorKind::Type, "non-numeric argument to Arg".to_string()))?;
3991    match v {
3992        Vector::Complex(vals) => {
3993            let result: Vec<Option<f64>> = vals.iter().map(|x| x.map(|c| c.arg())).collect();
3994            Ok(RValue::vec(Vector::Double(result.into())))
3995        }
3996        _ => {
3997            // For non-negative reals Arg is 0, for negative reals it's pi
3998            let result: Vec<Option<f64>> = v
3999                .to_doubles()
4000                .iter()
4001                .map(|x| x.map(|f| if f >= 0.0 { 0.0 } else { std::f64::consts::PI }))
4002                .collect();
4003            Ok(RValue::vec(Vector::Double(result.into())))
4004        }
4005    }
4006}
4007
4008/// Complex conjugate.
4009///
4010/// @param z complex or numeric vector
4011/// @return complex vector of conjugates (identity for reals)
4012#[builtin(name = "Conj", min_args = 1)]
4013fn builtin_conj(args: &[RValue], _: &[(String, RValue)]) -> Result<RValue, RError> {
4014    let v = args
4015        .first()
4016        .and_then(|v| v.as_vector())
4017        .ok_or_else(|| RError::new(RErrorKind::Type, "non-numeric argument to Conj".to_string()))?;
4018    match v {
4019        Vector::Complex(vals) => {
4020            let result: Vec<Option<num_complex::Complex64>> =
4021                vals.iter().map(|x| x.map(|c| c.conj())).collect();
4022            Ok(RValue::vec(Vector::Complex(result.into())))
4023        }
4024        _ => {
4025            // For non-complex, Conj is identity
4026            Ok(args[0].clone())
4027        }
4028    }
4029}
4030
4031/// Test if an object is of complex type.
4032///
4033/// @param x any R value
4034/// @return scalar logical
4035#[builtin(name = "is.complex", min_args = 1)]
4036fn builtin_is_complex(args: &[RValue], _: &[(String, RValue)]) -> Result<RValue, RError> {
4037    let result = matches!(
4038        args.first(),
4039        Some(RValue::Vector(rv)) if matches!(rv.inner, Vector::Complex(_))
4040    );
4041    Ok(RValue::vec(Vector::Logical(vec![Some(result)].into())))
4042}
4043
4044/// Coerce to complex type.
4045///
4046/// @param x numeric or complex vector
4047/// @return complex vector
4048#[builtin(name = "as.complex", min_args = 1)]
4049fn builtin_as_complex(args: &[RValue], _: &[(String, RValue)]) -> Result<RValue, RError> {
4050    let v = args
4051        .first()
4052        .and_then(|v| v.as_vector())
4053        .ok_or_else(|| RError::new(RErrorKind::Type, "cannot coerce to complex".to_string()))?;
4054    match v {
4055        Vector::Complex(vals) => Ok(RValue::vec(Vector::Complex(vals.clone()))),
4056        _ => {
4057            let result: Vec<Option<num_complex::Complex64>> = v
4058                .to_doubles()
4059                .iter()
4060                .map(|x| x.map(|f| num_complex::Complex64::new(f, 0.0)))
4061                .collect();
4062            Ok(RValue::vec(Vector::Complex(result.into())))
4063        }
4064    }
4065}
4066// endregion
4067
4068// region: Linear models (lm, summary.lm, coef)
4069
4070#[cfg(feature = "linalg")]
4071/// Extract a named column from a data frame (RList) as a Vec<Option<f64>>.
4072fn df_column_doubles(list: &RList, name: &str) -> Result<Vec<Option<f64>>, RError> {
4073    for (col_name, col_val) in &list.values {
4074        if col_name.as_deref() == Some(name) {
4075            return match col_val {
4076                RValue::Vector(rv) => Ok(rv.to_doubles()),
4077                _ => Err(RError::new(
4078                    RErrorKind::Type,
4079                    format!(
4080                        "column '{}' is not a numeric vector — lm() requires numeric columns",
4081                        name
4082                    ),
4083                )),
4084            };
4085        }
4086    }
4087    Err(RError::new(
4088        RErrorKind::Name,
4089        format!(
4090            "column '{}' not found in data frame. Available columns: {}",
4091            name,
4092            list.values
4093                .iter()
4094                .filter_map(|(n, _)| n.as_deref())
4095                .join(", ")
4096        ),
4097    ))
4098}
4099
4100#[cfg(feature = "linalg")]
4101/// Extract the response and predictor names from a formula expression.
4102///
4103/// For `y ~ x`, returns `("y", vec!["x"])`.
4104/// For `y ~ x1 + x2`, returns `("y", vec!["x1", "x2"])`.
4105fn parse_formula_terms(expr: &Expr) -> Result<(String, Vec<String>), RError> {
4106    match expr {
4107        Expr::Formula { lhs, rhs } => {
4108            let response = lhs
4109                .as_ref()
4110                .and_then(|e| match e.as_ref() {
4111                    Expr::Symbol(s) => Some(s.clone()),
4112                    _ => None,
4113                })
4114                .ok_or_else(|| {
4115                    RError::new(
4116                        RErrorKind::Argument,
4117                        "lm() formula must have a response variable on the left side of ~"
4118                            .to_string(),
4119                    )
4120                })?;
4121            let rhs_expr = rhs.as_ref().ok_or_else(|| {
4122                RError::new(
4123                    RErrorKind::Argument,
4124                    "lm() formula must have predictor(s) on the right side of ~".to_string(),
4125                )
4126            })?;
4127            let mut predictors = Vec::new();
4128            collect_additive_terms(rhs_expr, &mut predictors)?;
4129            if predictors.is_empty() {
4130                return Err(RError::new(
4131                    RErrorKind::Argument,
4132                    "lm() formula must have at least one predictor on the right side of ~"
4133                        .to_string(),
4134                ));
4135            }
4136            Ok((response, predictors))
4137        }
4138        _ => Err(RError::new(
4139            RErrorKind::Argument,
4140            "lm() requires a formula as its first argument (e.g. y ~ x)".to_string(),
4141        )),
4142    }
4143}
4144
4145#[cfg(feature = "linalg")]
4146/// Recursively collect symbol names from additive terms (x1 + x2 + x3).
4147fn collect_additive_terms(expr: &Expr, out: &mut Vec<String>) -> Result<(), RError> {
4148    match expr {
4149        Expr::Symbol(s) => {
4150            out.push(s.clone());
4151            Ok(())
4152        }
4153        Expr::BinaryOp {
4154            op: crate::parser::ast::BinaryOp::Add,
4155            lhs,
4156            rhs,
4157        } => {
4158            collect_additive_terms(lhs, out)?;
4159            collect_additive_terms(rhs, out)?;
4160            Ok(())
4161        }
4162        _ => Err(RError::new(
4163            RErrorKind::Argument,
4164            format!(
4165                "lm() formula terms must be symbols or sums of symbols, got: {:?}",
4166                expr
4167            ),
4168        )),
4169    }
4170}
4171
4172/// Fit a linear model using ordinary least squares.
4173///
4174/// Supports simple and multiple linear regression with formula syntax.
4175/// The formula `y ~ x` fits a simple regression; `y ~ x1 + x2` fits
4176/// multiple regression. An intercept is always included.
4177///
4178/// @param formula a formula specifying the model (e.g. y ~ x)
4179/// @param data a data frame containing the variables in the formula
4180/// @return a list of class "lm" with components: coefficients, residuals,
4181///         fitted.values, and call
4182#[cfg(feature = "linalg")]
4183#[interpreter_builtin(min_args = 1, namespace = "stats")]
4184fn interp_lm(
4185    args: &[RValue],
4186    named: &[(String, RValue)],
4187    _context: &BuiltinContext,
4188) -> Result<RValue, RError> {
4189    // Extract formula (first arg or named "formula")
4190    let formula_val = super::find_arg(args, named, "formula", 0).ok_or_else(|| {
4191        RError::new(
4192            RErrorKind::Argument,
4193            "lm() requires a formula argument".to_string(),
4194        )
4195    })?;
4196
4197    let formula_expr = match formula_val {
4198        RValue::Language(lang) => &*lang.inner,
4199        _ => {
4200            return Err(RError::new(
4201                RErrorKind::Type,
4202                "lm() first argument must be a formula (e.g. y ~ x), got a non-formula value"
4203                    .to_string(),
4204            ))
4205        }
4206    };
4207
4208    let (response_name, predictor_names) = parse_formula_terms(formula_expr)?;
4209
4210    // Extract data frame (second arg or named "data")
4211    let data_val = super::find_arg(args, named, "data", 1).ok_or_else(|| {
4212        RError::new(
4213            RErrorKind::Argument,
4214            "lm() requires a 'data' argument — pass a data frame containing the model variables"
4215                .to_string(),
4216        )
4217    })?;
4218
4219    let data_list = match data_val {
4220        RValue::List(list) => list,
4221        _ => {
4222            return Err(RError::new(
4223                RErrorKind::Type,
4224                "lm() 'data' argument must be a data frame".to_string(),
4225            ))
4226        }
4227    };
4228
4229    // Extract response vector
4230    let y_raw = df_column_doubles(data_list, &response_name)?;
4231    let n = y_raw.len();
4232    if n == 0 {
4233        return Err(RError::new(
4234            RErrorKind::Argument,
4235            "lm() requires at least one observation".to_string(),
4236        ));
4237    }
4238
4239    let p = predictor_names.len(); // number of predictors (not counting intercept)
4240
4241    // Check for NA values in response
4242    let y: Vec<f64> = y_raw
4243        .into_iter()
4244        .map(|v| {
4245            v.ok_or_else(|| {
4246                RError::new(
4247                    RErrorKind::Argument,
4248                    format!(
4249                        "NA values in response variable '{}' — lm() does not yet support na.action",
4250                        response_name
4251                    ),
4252                )
4253            })
4254        })
4255        .collect::<Result<Vec<_>, _>>()?;
4256
4257    // Build design matrix X: n x (p+1), first column is intercept (all 1s)
4258    let ncol = p + 1;
4259    let mut x_data = vec![0.0; n * ncol];
4260    // Column 0: intercept
4261    for item in x_data.iter_mut().take(n) {
4262        *item = 1.0; // column-major: element (i, 0)
4263    }
4264    // Columns 1..=p: predictors
4265    for (j, pred_name) in predictor_names.iter().enumerate() {
4266        let col_raw = df_column_doubles(data_list, pred_name)?;
4267        if col_raw.len() != n {
4268            return Err(RError::new(
4269                RErrorKind::Argument,
4270                format!(
4271                    "predictor '{}' has {} observations but response '{}' has {} — they must match",
4272                    pred_name,
4273                    col_raw.len(),
4274                    response_name,
4275                    n
4276                ),
4277            ));
4278        }
4279        for (i, val) in col_raw.into_iter().enumerate() {
4280            let v = val.ok_or_else(|| {
4281                RError::new(
4282                    RErrorKind::Argument,
4283                    format!(
4284                        "NA values in predictor '{}' — lm() does not yet support na.action",
4285                        pred_name
4286                    ),
4287                )
4288            })?;
4289            x_data[i + (j + 1) * n] = v; // column-major: element (i, j+1)
4290        }
4291    }
4292
4293    let x = Array2::from_shape_vec((n, ncol).f(), x_data)
4294        .map_err(|source| -> RError { MathError::Shape { source }.into() })?;
4295    let y_arr = Array1::from_vec(y);
4296
4297    // Compute beta = (X'X)^{-1} X'y via normal equations
4298    let xt = x.t();
4299    let xtx = xt.dot(&x);
4300    let xty = xt.dot(&y_arr);
4301
4302    // Solve X'X * beta = X'y using Gaussian elimination
4303    let beta = solve_linear_system(&xtx, &xty)?;
4304
4305    // Compute fitted values and residuals
4306    let fitted: Vec<f64> = (0..n)
4307        .map(|i| {
4308            let mut val = 0.0;
4309            for j in 0..ncol {
4310                val += x[[i, j]] * beta[j];
4311            }
4312            val
4313        })
4314        .collect();
4315
4316    let residuals: Vec<f64> = (0..n).map(|i| y_arr[i] - fitted[i]).collect();
4317
4318    // Build coefficient names: (Intercept), pred1, pred2, ...
4319    let mut coef_names: Vec<Option<String>> = Vec::with_capacity(ncol);
4320    coef_names.push(Some("(Intercept)".to_string()));
4321    for name in &predictor_names {
4322        coef_names.push(Some(name.clone()));
4323    }
4324
4325    // Build named coefficient vector
4326    let coef_doubles: Vec<Option<f64>> = beta.iter().copied().map(Some).collect();
4327    let mut coef_rv = RVector::from(Vector::Double(coef_doubles.into()));
4328    coef_rv.set_attr(
4329        "names".to_string(),
4330        RValue::vec(Vector::Character(coef_names.into())),
4331    );
4332
4333    // Build fitted.values vector
4334    let fitted_doubles: Vec<Option<f64>> = fitted.into_iter().map(Some).collect();
4335
4336    // Build residuals vector
4337    let residual_doubles: Vec<Option<f64>> = residuals.into_iter().map(Some).collect();
4338
4339    // Build result list with class "lm"
4340    let mut result = RList::new(vec![
4341        (Some("coefficients".to_string()), RValue::Vector(coef_rv)),
4342        (
4343            Some("residuals".to_string()),
4344            RValue::vec(Vector::Double(residual_doubles.into())),
4345        ),
4346        (
4347            Some("fitted.values".to_string()),
4348            RValue::vec(Vector::Double(fitted_doubles.into())),
4349        ),
4350        (Some("call".to_string()), RValue::Null),
4351    ]);
4352    result.set_attr(
4353        "class".to_string(),
4354        RValue::vec(Vector::Character(vec![Some("lm".to_string())].into())),
4355    );
4356
4357    Ok(RValue::List(result))
4358}
4359
4360#[cfg(feature = "linalg")]
4361/// Solve a linear system A * x = b using Gaussian elimination with partial pivoting.
4362/// A must be square (ncol x ncol), b must have length ncol.
4363fn solve_linear_system(a: &Array2<f64>, b: &Array1<f64>) -> Result<Vec<f64>, RError> {
4364    let n = a.nrows();
4365    if n != a.ncols() || n != b.len() {
4366        return Err(RError::new(
4367            RErrorKind::Other,
4368            "internal error: solve_linear_system dimension mismatch".to_string(),
4369        ));
4370    }
4371    if n == 0 {
4372        return Ok(vec![]);
4373    }
4374
4375    // Build augmented matrix [A | b]
4376    let mut aug = Array2::<f64>::zeros((n, n + 1));
4377    for i in 0..n {
4378        for j in 0..n {
4379            aug[[i, j]] = a[[i, j]];
4380        }
4381        aug[[i, n]] = b[i];
4382    }
4383
4384    // Forward elimination with partial pivoting
4385    for col in 0..n {
4386        // Find pivot
4387        let mut max_row = col;
4388        let mut max_val = aug[[col, col]].abs();
4389        for row in (col + 1)..n {
4390            let val = aug[[row, col]].abs();
4391            if val > max_val {
4392                max_val = val;
4393                max_row = row;
4394            }
4395        }
4396
4397        if max_val < 1e-12 {
4398            return Err(RError::new(
4399                RErrorKind::Other,
4400                "lm() design matrix is singular or nearly singular — \
4401                 check for collinear predictors or constant columns"
4402                    .to_string(),
4403            ));
4404        }
4405
4406        // Swap rows
4407        if max_row != col {
4408            for k in 0..=n {
4409                let tmp = aug[[col, k]];
4410                aug[[col, k]] = aug[[max_row, k]];
4411                aug[[max_row, k]] = tmp;
4412            }
4413        }
4414
4415        // Eliminate below
4416        for row in (col + 1)..n {
4417            let factor = aug[[row, col]] / aug[[col, col]];
4418            for k in col..=n {
4419                aug[[row, k]] -= factor * aug[[col, k]];
4420            }
4421        }
4422    }
4423
4424    // Back substitution
4425    let mut x = vec![0.0; n];
4426    for i in (0..n).rev() {
4427        let mut sum = aug[[i, n]];
4428        for j in (i + 1)..n {
4429            sum -= aug[[i, j]] * x[j];
4430        }
4431        x[i] = sum / aug[[i, i]];
4432    }
4433
4434    Ok(x)
4435}
4436
4437/// Print a summary of a linear model.
4438///
4439/// Displays the coefficients table for an lm object.
4440///
4441/// @param object an lm object (result of lm())
4442/// @return the object, invisibly
4443#[cfg(feature = "linalg")]
4444#[interpreter_builtin(name = "summary.lm", min_args = 1, namespace = "stats")]
4445fn interp_summary_lm(
4446    args: &[RValue],
4447    _named: &[(String, RValue)],
4448    context: &BuiltinContext,
4449) -> Result<RValue, RError> {
4450    let obj = args.first().ok_or_else(|| {
4451        RError::new(
4452            RErrorKind::Argument,
4453            "summary.lm() requires an lm object".to_string(),
4454        )
4455    })?;
4456
4457    let list = match obj {
4458        RValue::List(l) => l,
4459        _ => {
4460            return Err(RError::new(
4461                RErrorKind::Type,
4462                "summary.lm() requires an lm object (a list with class 'lm')".to_string(),
4463            ))
4464        }
4465    };
4466
4467    // Extract coefficients
4468    let coefs = list
4469        .values
4470        .iter()
4471        .find(|(n, _)| n.as_deref() == Some("coefficients"))
4472        .map(|(_, v)| v);
4473
4474    context.write("Call:\nlm(formula = ...)\n\n");
4475
4476    if let Some(RValue::Vector(rv)) = coefs {
4477        let values = rv.to_doubles();
4478        let names: Vec<String> = rv
4479            .get_attr("names")
4480            .and_then(|n| match n {
4481                RValue::Vector(nv) => match &nv.inner {
4482                    Vector::Character(c) => {
4483                        Some(c.iter().map(|s| s.clone().unwrap_or_default()).collect())
4484                    }
4485                    _ => None,
4486                },
4487                _ => None,
4488            })
4489            .unwrap_or_default();
4490
4491        context.write("Coefficients:\n");
4492        let max_name_len = names.iter().map(|n| n.len()).max().unwrap_or(0).max(8);
4493        context.write(&format!("{:>width$}  Estimate\n", "", width = max_name_len));
4494        for (i, val) in values.iter().enumerate() {
4495            let name = names.get(i).map(|s| s.as_str()).unwrap_or("???");
4496            let estimate = val.map_or("NA".to_string(), |v| format!("{:.6}", v));
4497            context.write(&format!(
4498                "{:>width$}  {}\n",
4499                name,
4500                estimate,
4501                width = max_name_len
4502            ));
4503        }
4504    }
4505
4506    // Extract residuals for a brief summary
4507    let residuals = list
4508        .values
4509        .iter()
4510        .find(|(n, _)| n.as_deref() == Some("residuals"))
4511        .map(|(_, v)| v);
4512
4513    if let Some(RValue::Vector(rv)) = residuals {
4514        let vals: Vec<f64> = rv.to_doubles().into_iter().flatten().collect();
4515        if !vals.is_empty() {
4516            let mut sorted = vals.clone();
4517            sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
4518            let min = sorted[0];
4519            let max = sorted[sorted.len() - 1];
4520            let median = if sorted.len().is_multiple_of(2) {
4521                (sorted[sorted.len() / 2 - 1] + sorted[sorted.len() / 2]) / 2.0
4522            } else {
4523                sorted[sorted.len() / 2]
4524            };
4525            context.write(&format!(
4526                "\nResiduals: Min = {:.4}, Median = {:.4}, Max = {:.4}\n",
4527                min, median, max
4528            ));
4529        }
4530    }
4531
4532    Ok(obj.clone())
4533}
4534
4535/// Extract coefficients from a model object.
4536///
4537/// Extracts the `$coefficients` component from a fitted model (e.g. lm).
4538///
4539/// @param object a fitted model object with a coefficients component
4540/// @return a named numeric vector of coefficients
4541#[cfg(feature = "linalg")]
4542#[builtin(min_args = 1, namespace = "stats")]
4543fn builtin_coef(args: &[RValue], _: &[(String, RValue)]) -> Result<RValue, RError> {
4544    let obj = args.first().ok_or_else(|| {
4545        RError::new(
4546            RErrorKind::Argument,
4547            "coef() requires a model object".to_string(),
4548        )
4549    })?;
4550
4551    match obj {
4552        RValue::List(list) => {
4553            for (name, val) in &list.values {
4554                if name.as_deref() == Some("coefficients") {
4555                    return Ok(val.clone());
4556                }
4557            }
4558            Err(RError::new(
4559                RErrorKind::Name,
4560                "object does not have a 'coefficients' component".to_string(),
4561            ))
4562        }
4563        _ => Err(RError::new(
4564            RErrorKind::Type,
4565            "coef() requires a list-like model object (e.g. result of lm())".to_string(),
4566        )),
4567    }
4568}
4569
4570// endregion
4571
4572// region: arrayInd
4573
4574/// Convert linear indices to array (row, col) subscripts.
4575///
4576/// Given a vector of 1-based linear indices and a dimension vector,
4577/// returns a matrix of subscripts (one row per index, one column per dim).
4578///
4579/// @param ind integer vector of linear indices (1-based)
4580/// @param .dim integer vector of dimensions (e.g. c(nrow, ncol))
4581/// @return integer matrix of subscripts
4582#[builtin(name = "arrayInd", min_args = 2)]
4583fn builtin_array_ind(args: &[RValue], _: &[(String, RValue)]) -> Result<RValue, RError> {
4584    let indices = match args.first() {
4585        Some(RValue::Vector(rv)) => rv.to_doubles(),
4586        _ => {
4587            return Err(RError::new(
4588                RErrorKind::Argument,
4589                "'ind' must be a numeric vector".to_string(),
4590            ))
4591        }
4592    };
4593    let dims = match args.get(1) {
4594        Some(RValue::Vector(rv)) => rv.to_doubles(),
4595        _ => {
4596            return Err(RError::new(
4597                RErrorKind::Argument,
4598                "'.dim' must be a numeric vector".to_string(),
4599            ))
4600        }
4601    };
4602
4603    if dims.len() < 2 {
4604        return Err(RError::new(
4605            RErrorKind::Argument,
4606            "'.dim' must have at least 2 elements for matrix subscripts".to_string(),
4607        ));
4608    }
4609
4610    let dim_sizes: Vec<i64> = dims
4611        .iter()
4612        .map(|d| match d {
4613            Some(v) => Ok(*v as i64),
4614            None => Err(RError::new(
4615                RErrorKind::Argument,
4616                "NA in '.dim'".to_string(),
4617            )),
4618        })
4619        .collect::<Result<_, _>>()?;
4620
4621    let ndim = dims.len();
4622    let n = indices.len();
4623    // Result stored column-major: all rows for dim1, then all rows for dim2, ...
4624    let mut result: Vec<Option<i64>> = vec![None; n * ndim];
4625
4626    for (idx_pos, ind_val) in indices.iter().enumerate() {
4627        match ind_val {
4628            Some(v) => {
4629                let mut linear = *v as i64 - 1; // convert to 0-based
4630                for d in 0..ndim {
4631                    let subscript = linear % dim_sizes[d];
4632                    result[d * n + idx_pos] = Some(subscript + 1); // back to 1-based
4633                    linear /= dim_sizes[d];
4634                }
4635            }
4636            None => {
4637                for d in 0..ndim {
4638                    result[d * n + idx_pos] = None;
4639                }
4640            }
4641        }
4642    }
4643
4644    let mut rv = RVector::from(Vector::Integer(result.into()));
4645    rv.set_attr(
4646        "dim".to_string(),
4647        RValue::vec(Vector::Integer(
4648            vec![
4649                Some(i64::try_from(n).unwrap_or(0)),
4650                Some(i64::try_from(ndim).unwrap_or(0)),
4651            ]
4652            .into(),
4653        )),
4654    );
4655
4656    Ok(RValue::Vector(rv))
4657}
4658
4659// endregion
4660
4661// region: Row/column aggregation (rowSums, colSums, rowMeans, colMeans)
4662
4663/// Helper: extract matrix dimensions and data as doubles.
4664fn matrix_dims_and_data(args: &[RValue]) -> Option<(usize, usize, Vec<Option<f64>>)> {
4665    let v = match args.first()? {
4666        RValue::Vector(v) => v,
4667        RValue::List(list) => {
4668            // Data frame: ncol = number of list elements, nrow = length of first
4669            let ncol = list.values.len();
4670            if ncol == 0 {
4671                return Some((0, 0, vec![]));
4672            }
4673            let nrow = list
4674                .values
4675                .first()
4676                .map(|(_, v)| match v {
4677                    RValue::Vector(rv) => rv.len(),
4678                    _ => 1,
4679                })
4680                .unwrap_or(0);
4681            let mut data = Vec::with_capacity(nrow * ncol);
4682            for (_, val) in &list.values {
4683                if let Some(v) = val.as_vector() {
4684                    data.extend(v.to_doubles());
4685                } else {
4686                    data.extend(std::iter::repeat_n(None, nrow));
4687                }
4688            }
4689            return Some((nrow, ncol, data));
4690        }
4691        _ => return None,
4692    };
4693    let dim = v.get_attr("dim")?;
4694    let dim_vec = dim.as_vector()?;
4695    let dims = dim_vec.to_integers();
4696    if dims.len() != 2 {
4697        return None;
4698    }
4699    let nrow = dims[0].unwrap_or(0) as usize;
4700    let ncol = dims[1].unwrap_or(0) as usize;
4701    Some((nrow, ncol, v.to_doubles()))
4702}
4703
4704/// Sum of each row of a matrix or data frame.
4705///
4706/// @param x numeric matrix or data frame
4707/// @param na.rm logical: remove NAs before summing?
4708/// @return numeric vector of length nrow(x)
4709/// @namespace base
4710#[builtin(name = "rowSums", min_args = 1)]
4711fn builtin_row_sums(args: &[RValue], named: &[(String, RValue)]) -> Result<RValue, RError> {
4712    let (nrow, ncol, data) = matrix_dims_and_data(args).ok_or_else(|| {
4713        RError::new(
4714            RErrorKind::Argument,
4715            "'x' must be a matrix or data frame".to_string(),
4716        )
4717    })?;
4718    let na_rm = named
4719        .iter()
4720        .find(|(n, _)| n == "na.rm")
4721        .and_then(|(_, v)| v.as_vector()?.as_logical_scalar())
4722        .unwrap_or(false);
4723    let mut result = Vec::with_capacity(nrow);
4724    for i in 0..nrow {
4725        let mut sum = 0.0;
4726        let mut has_na = false;
4727        for j in 0..ncol {
4728            match data.get(j * nrow + i).copied().flatten() {
4729                Some(v) => sum += v,
4730                None if !na_rm => {
4731                    has_na = true;
4732                    break;
4733                }
4734                _ => {}
4735            }
4736        }
4737        result.push(if has_na { None } else { Some(sum) });
4738    }
4739    Ok(RValue::vec(Vector::Double(result.into())))
4740}
4741
4742/// Sum of each column of a matrix or data frame.
4743///
4744/// @param x numeric matrix or data frame
4745/// @param na.rm logical: remove NAs?
4746/// @return numeric vector of length ncol(x)
4747/// @namespace base
4748#[builtin(name = "colSums", min_args = 1)]
4749fn builtin_col_sums(args: &[RValue], named: &[(String, RValue)]) -> Result<RValue, RError> {
4750    let (nrow, ncol, data) = matrix_dims_and_data(args).ok_or_else(|| {
4751        RError::new(
4752            RErrorKind::Argument,
4753            "'x' must be a matrix or data frame".to_string(),
4754        )
4755    })?;
4756    let na_rm = named
4757        .iter()
4758        .find(|(n, _)| n == "na.rm")
4759        .and_then(|(_, v)| v.as_vector()?.as_logical_scalar())
4760        .unwrap_or(false);
4761    let mut result = Vec::with_capacity(ncol);
4762    for j in 0..ncol {
4763        let mut sum = 0.0;
4764        let mut has_na = false;
4765        for i in 0..nrow {
4766            match data.get(j * nrow + i).copied().flatten() {
4767                Some(v) => sum += v,
4768                None if !na_rm => {
4769                    has_na = true;
4770                    break;
4771                }
4772                _ => {}
4773            }
4774        }
4775        result.push(if has_na { None } else { Some(sum) });
4776    }
4777    Ok(RValue::vec(Vector::Double(result.into())))
4778}
4779
4780/// Mean of each row of a matrix or data frame.
4781///
4782/// @param x numeric matrix or data frame
4783/// @param na.rm logical: remove NAs?
4784/// @return numeric vector of length nrow(x)
4785/// @namespace base
4786#[builtin(name = "rowMeans", min_args = 1)]
4787fn builtin_row_means(args: &[RValue], named: &[(String, RValue)]) -> Result<RValue, RError> {
4788    let (nrow, ncol, data) = matrix_dims_and_data(args).ok_or_else(|| {
4789        RError::new(
4790            RErrorKind::Argument,
4791            "'x' must be a matrix or data frame".to_string(),
4792        )
4793    })?;
4794    let na_rm = named
4795        .iter()
4796        .find(|(n, _)| n == "na.rm")
4797        .and_then(|(_, v)| v.as_vector()?.as_logical_scalar())
4798        .unwrap_or(false);
4799    let mut result = Vec::with_capacity(nrow);
4800    for i in 0..nrow {
4801        let mut sum = 0.0;
4802        let mut count = 0usize;
4803        let mut has_na = false;
4804        for j in 0..ncol {
4805            match data.get(j * nrow + i).copied().flatten() {
4806                Some(v) => {
4807                    sum += v;
4808                    count += 1;
4809                }
4810                None if !na_rm => {
4811                    has_na = true;
4812                    break;
4813                }
4814                _ => {}
4815            }
4816        }
4817        result.push(if has_na || count == 0 {
4818            None
4819        } else {
4820            Some(sum / count as f64)
4821        });
4822    }
4823    Ok(RValue::vec(Vector::Double(result.into())))
4824}
4825
4826/// Mean of each column of a matrix or data frame.
4827///
4828/// @param x numeric matrix or data frame
4829/// @param na.rm logical: remove NAs?
4830/// @return numeric vector of length ncol(x)
4831/// @namespace base
4832#[builtin(name = "colMeans", min_args = 1)]
4833fn builtin_col_means(args: &[RValue], named: &[(String, RValue)]) -> Result<RValue, RError> {
4834    let (nrow, ncol, data) = matrix_dims_and_data(args).ok_or_else(|| {
4835        RError::new(
4836            RErrorKind::Argument,
4837            "'x' must be a matrix or data frame".to_string(),
4838        )
4839    })?;
4840    let na_rm = named
4841        .iter()
4842        .find(|(n, _)| n == "na.rm")
4843        .and_then(|(_, v)| v.as_vector()?.as_logical_scalar())
4844        .unwrap_or(false);
4845    let mut result = Vec::with_capacity(ncol);
4846    for j in 0..ncol {
4847        let mut sum = 0.0;
4848        let mut count = 0usize;
4849        let mut has_na = false;
4850        for i in 0..nrow {
4851            match data.get(j * nrow + i).copied().flatten() {
4852                Some(v) => {
4853                    sum += v;
4854                    count += 1;
4855                }
4856                None if !na_rm => {
4857                    has_na = true;
4858                    break;
4859                }
4860                _ => {}
4861            }
4862        }
4863        result.push(if has_na || count == 0 {
4864            None
4865        } else {
4866            Some(sum / count as f64)
4867        });
4868    }
4869    Ok(RValue::vec(Vector::Double(result.into())))
4870}
4871
4872/// Drop unused factor levels.
4873///
4874/// @param x factor
4875/// @return factor with unused levels removed
4876/// @namespace base
4877#[builtin(min_args = 1)]
4878fn builtin_droplevels(args: &[RValue], _: &[(String, RValue)]) -> Result<RValue, RError> {
4879    match args.first() {
4880        Some(RValue::Vector(v)) => {
4881            let codes = v.inner.to_integers();
4882            let levels = match v.get_attr("levels") {
4883                Some(RValue::Vector(lv)) => lv.to_characters(),
4884                _ => return Ok(RValue::Vector(v.clone())),
4885            };
4886            let used: std::collections::BTreeSet<usize> = codes
4887                .iter()
4888                .filter_map(|c| c.and_then(|i| usize::try_from(i).ok()))
4889                .collect();
4890            let mut new_levels = Vec::new();
4891            let mut old_to_new = std::collections::HashMap::new();
4892            for (old_idx, level) in levels.iter().enumerate() {
4893                if used.contains(&(old_idx + 1)) {
4894                    old_to_new.insert(old_idx + 1, new_levels.len() + 1);
4895                    new_levels.push(level.clone());
4896                }
4897            }
4898            let new_codes: Vec<Option<i64>> = codes
4899                .iter()
4900                .map(|c| c.and_then(|i| old_to_new.get(&(i as usize)).map(|&new| new as i64)))
4901                .collect();
4902            let mut rv = RVector::from(Vector::Integer(new_codes.into()));
4903            rv.set_attr(
4904                "levels".to_string(),
4905                RValue::vec(Vector::Character(new_levels.into())),
4906            );
4907            rv.set_attr(
4908                "class".to_string(),
4909                RValue::vec(Vector::Character(vec![Some("factor".to_string())].into())),
4910            );
4911            Ok(RValue::Vector(rv))
4912        }
4913        _ => Ok(args.first().cloned().unwrap_or(RValue::Null)),
4914    }
4915}
4916
4917// region: sweep and kronecker
4918
4919/// Sweep a summary statistic from each row or column of a matrix.
4920///
4921/// @param x a numeric matrix
4922/// @param MARGIN 1 for rows, 2 for columns
4923/// @param STATS a numeric vector of statistics to sweep out
4924/// @param FUN the function to use: "-" (default), "+", "*", "/"
4925/// @return a matrix of the same dimensions as x
4926/// @namespace base
4927#[builtin(min_args = 3)]
4928fn builtin_sweep(args: &[RValue], named: &[(String, RValue)]) -> Result<RValue, RError> {
4929    let (nrow, ncol, data) = matrix_dims_and_data(args).ok_or_else(|| {
4930        RError::new(
4931            RErrorKind::Argument,
4932            "'x' must be a matrix or data frame".to_string(),
4933        )
4934    })?;
4935
4936    // MARGIN: positional arg[1] or named
4937    let margin_val = named
4938        .iter()
4939        .find(|(n, _)| n == "MARGIN")
4940        .map(|(_, v)| v)
4941        .or_else(|| args.get(1));
4942    let margin = match margin_val {
4943        Some(RValue::Vector(v)) => v
4944            .as_double_scalar()
4945            .or_else(|| v.as_integer_scalar().map(|i| i as f64))
4946            .ok_or_else(|| {
4947                RError::new(
4948                    RErrorKind::Argument,
4949                    "MARGIN must be 1 (rows) or 2 (columns)".to_string(),
4950                )
4951            })? as usize,
4952        _ => {
4953            return Err(RError::new(
4954                RErrorKind::Argument,
4955                "MARGIN must be 1 (rows) or 2 (columns)".to_string(),
4956            ))
4957        }
4958    };
4959    if margin != 1 && margin != 2 {
4960        return Err(RError::new(
4961            RErrorKind::Argument,
4962            format!("MARGIN must be 1 (rows) or 2 (columns), got {}", margin),
4963        ));
4964    }
4965
4966    // STATS: positional arg[2] or named
4967    let stats_val = named
4968        .iter()
4969        .find(|(n, _)| n == "STATS")
4970        .map(|(_, v)| v)
4971        .or_else(|| args.get(2));
4972    let stats: Vec<f64> = match stats_val {
4973        Some(RValue::Vector(v)) => v
4974            .to_doubles()
4975            .into_iter()
4976            .map(|d| d.unwrap_or(f64::NAN))
4977            .collect(),
4978        _ => {
4979            return Err(RError::new(
4980                RErrorKind::Argument,
4981                "STATS must be a numeric vector".to_string(),
4982            ))
4983        }
4984    };
4985
4986    // Validate STATS length matches the swept dimension
4987    let expected_len = if margin == 1 { nrow } else { ncol };
4988    if stats.len() != expected_len {
4989        return Err(RError::new(
4990            RErrorKind::Argument,
4991            format!(
4992                "STATS has length {} but MARGIN {} has {} {}",
4993                stats.len(),
4994                margin,
4995                expected_len,
4996                if margin == 1 { "rows" } else { "columns" }
4997            ),
4998        ));
4999    }
5000
5001    // FUN: positional arg[3] or named — default is "-"
5002    let fun_val = named
5003        .iter()
5004        .find(|(n, _)| n == "FUN")
5005        .map(|(_, v)| v)
5006        .or_else(|| args.get(3));
5007    let fun_str = match fun_val {
5008        Some(RValue::Vector(v)) => v.as_character_scalar().unwrap_or_else(|| "-".to_string()),
5009        None => "-".to_string(),
5010        _ => "-".to_string(),
5011    };
5012
5013    let apply_fn: fn(f64, f64) -> f64 = match fun_str.as_str() {
5014        "-" => |a, b| a - b,
5015        "+" => |a, b| a + b,
5016        "*" => |a, b| a * b,
5017        "/" => |a, b| a / b,
5018        other => {
5019            return Err(RError::new(
5020                RErrorKind::Argument,
5021                format!(
5022                    "FUN must be one of \"-\", \"+\", \"*\", \"/\", got {:?}",
5023                    other
5024                ),
5025            ))
5026        }
5027    };
5028
5029    // Sweep: data is column-major (flat[j * nrow + i] = matrix[i, j])
5030    let mut result: Vec<Option<f64>> = Vec::with_capacity(nrow * ncol);
5031    for j in 0..ncol {
5032        for i in 0..nrow {
5033            let idx = j * nrow + i;
5034            let val = data[idx];
5035            let stat = if margin == 1 { stats[i] } else { stats[j] };
5036            result.push(val.map(|v| apply_fn(v, stat)));
5037        }
5038    }
5039
5040    let (row_names, col_names) = matrix_dimnames(args.first().unwrap_or(&RValue::Null));
5041    let mut rv = RVector::from(Vector::Double(result.into()));
5042    set_matrix_attrs(&mut rv, nrow, ncol, row_names, col_names)?;
5043    Ok(RValue::Vector(rv))
5044}
5045
5046/// Kronecker product of two matrices (or vectors treated as single-column matrices).
5047///
5048/// The default FUN is "*" (standard Kronecker product). The result has
5049/// dimensions (nrow(A)*nrow(B)) x (ncol(A)*ncol(B)).
5050///
5051/// Can also be called via the `%x%` operator: A %x% B.
5052///
5053/// @param A numeric matrix or vector
5054/// @param B numeric matrix or vector
5055/// @param FUN the function to apply element-wise: "*" (default), "+", "-", "/"
5056/// @return a numeric matrix
5057/// @namespace base
5058#[builtin(min_args = 2)]
5059fn builtin_kronecker(args: &[RValue], named: &[(String, RValue)]) -> Result<RValue, RError> {
5060    // FUN: positional arg[2] or named — default is "*"
5061    let fun_val = named
5062        .iter()
5063        .find(|(n, _)| n == "FUN")
5064        .map(|(_, v)| v)
5065        .or_else(|| args.get(2));
5066    let fun_str = match fun_val {
5067        Some(RValue::Vector(v)) => v.as_character_scalar().unwrap_or_else(|| "*".to_string()),
5068        None => "*".to_string(),
5069        _ => "*".to_string(),
5070    };
5071
5072    let apply_fn: fn(f64, f64) -> f64 = match fun_str.as_str() {
5073        "*" => |a, b| a * b,
5074        "+" => |a, b| a + b,
5075        "-" => |a, b| a - b,
5076        "/" => |a, b| a / b,
5077        other => {
5078            return Err(RError::new(
5079                RErrorKind::Argument,
5080                format!(
5081                    "FUN must be one of \"*\", \"+\", \"-\", \"/\", got {:?}",
5082                    other
5083                ),
5084            ))
5085        }
5086    };
5087
5088    eval_kronecker_with_fn(
5089        args.first().unwrap_or(&RValue::Null),
5090        args.get(1).unwrap_or(&RValue::Null),
5091        apply_fn,
5092    )
5093}
5094
5095/// Extract matrix dimensions from a value, treating plain vectors as column vectors.
5096fn kronecker_dims_and_data(val: &RValue) -> Result<(usize, usize, Vec<Option<f64>>), RError> {
5097    match val {
5098        RValue::Vector(v) => {
5099            let data = v.to_doubles();
5100            match v.get_attr("dim") {
5101                Some(dim_val) => {
5102                    let dim_vec = dim_val.as_vector().ok_or_else(|| {
5103                        RError::new(RErrorKind::Type, "invalid dim attribute".to_string())
5104                    })?;
5105                    let dims = dim_vec.to_integers();
5106                    if dims.len() != 2 {
5107                        return Err(RError::new(
5108                            RErrorKind::Argument,
5109                            "kronecker() requires matrix arguments (2-d dim)".to_string(),
5110                        ));
5111                    }
5112                    let nrow = usize::try_from(dims[0].unwrap_or(0))?;
5113                    let ncol = usize::try_from(dims[1].unwrap_or(0))?;
5114                    Ok((nrow, ncol, data))
5115                }
5116                // Plain vector → treat as column vector (n x 1)
5117                None => {
5118                    let n = data.len();
5119                    Ok((n, 1, data))
5120                }
5121            }
5122        }
5123        _ => Err(RError::new(
5124            RErrorKind::Argument,
5125            "kronecker() requires numeric matrix/vector arguments".to_string(),
5126        )),
5127    }
5128}
5129
5130fn eval_kronecker_with_fn(
5131    a: &RValue,
5132    b: &RValue,
5133    fun: fn(f64, f64) -> f64,
5134) -> Result<RValue, RError> {
5135    let (a_nrow, a_ncol, a_data) = kronecker_dims_and_data(a)?;
5136    let (b_nrow, b_ncol, b_data) = kronecker_dims_and_data(b)?;
5137
5138    let out_nrow = a_nrow * b_nrow;
5139    let out_ncol = a_ncol * b_ncol;
5140    let mut result: Vec<Option<f64>> = Vec::with_capacity(out_nrow * out_ncol);
5141
5142    // Build column-major result:
5143    // result[(i_a * b_nrow + i_b), (j_a * b_ncol + j_b)] = a[i_a, j_a] * b[i_b, j_b]
5144    // In column-major flat layout: result[out_col * out_nrow + out_row]
5145    for j_a in 0..a_ncol {
5146        for j_b in 0..b_ncol {
5147            for i_a in 0..a_nrow {
5148                for i_b in 0..b_nrow {
5149                    let a_val = a_data[j_a * a_nrow + i_a];
5150                    let b_val = b_data[j_b * b_nrow + i_b];
5151                    result.push(match (a_val, b_val) {
5152                        (Some(av), Some(bv)) => Some(fun(av, bv)),
5153                        _ => None,
5154                    });
5155                }
5156            }
5157        }
5158    }
5159
5160    let mut rv = RVector::from(Vector::Double(result.into()));
5161    set_matrix_attrs(&mut rv, out_nrow, out_ncol, None, None)?;
5162    Ok(RValue::Vector(rv))
5163}
5164
5165/// Evaluate the `%x%` (Kronecker product) operator.
5166///
5167/// Called from `ops.rs` when the parser encounters `A %x% B`.
5168pub fn eval_kronecker(left: &RValue, right: &RValue) -> Result<RValue, RError> {
5169    eval_kronecker_with_fn(left, right, |a, b| a * b)
5170}
5171
5172// endregion
5173
5174// region: Matrix index helpers (row, col, slice.index)
5175
5176/// Helper: extract matrix dimensions (nrow, ncol) from any matrix-like object.
5177fn matrix_dims(arg: &RValue) -> Option<(usize, usize)> {
5178    match arg {
5179        RValue::Vector(v) => {
5180            let dim = v.get_attr("dim")?;
5181            let dim_vec = dim.as_vector()?;
5182            let dims = dim_vec.to_integers();
5183            if dims.len() == 2 {
5184                Some((
5185                    usize::try_from(dims[0]?).ok()?,
5186                    usize::try_from(dims[1]?).ok()?,
5187                ))
5188            } else {
5189                None
5190            }
5191        }
5192        RValue::List(l) => {
5193            if let Some(dim) = l.get_attr("dim") {
5194                let dim_vec = dim.as_vector()?;
5195                let dims = dim_vec.to_integers();
5196                if dims.len() == 2 {
5197                    return Some((
5198                        usize::try_from(dims[0]?).ok()?,
5199                        usize::try_from(dims[1]?).ok()?,
5200                    ));
5201                }
5202            }
5203            // data frame fallback
5204            let ncol = l.values.len();
5205            let nrow = l
5206                .values
5207                .first()
5208                .map(|(_, v)| match v {
5209                    RValue::Vector(rv) => rv.len(),
5210                    _ => 1,
5211                })
5212                .unwrap_or(0);
5213            Some((nrow, ncol))
5214        }
5215        _ => None,
5216    }
5217}
5218
5219/// Return a matrix of row indices (same dim as input, each element = its row number).
5220///
5221/// @param x matrix or matrix-like object (must have dim attribute)
5222/// @return integer matrix where element [i,j] = i
5223/// @namespace base
5224#[builtin(min_args = 1)]
5225fn builtin_row(args: &[RValue], _: &[(String, RValue)]) -> Result<RValue, RError> {
5226    let (nrow, ncol) = matrix_dims(args.first().unwrap_or(&RValue::Null)).ok_or_else(|| {
5227        RError::new(
5228            RErrorKind::Argument,
5229            "a matrix-like object is required as argument to 'row'".to_string(),
5230        )
5231    })?;
5232    // Column-major: for column j, row i, index = j * nrow + i
5233    let mut data: Vec<Option<i64>> = Vec::with_capacity(nrow * ncol);
5234    for _j in 0..ncol {
5235        for i in 0..nrow {
5236            data.push(Some(i64::try_from(i)? + 1));
5237        }
5238    }
5239    let mut rv = RVector::from(Vector::Integer(data.into()));
5240    rv.set_attr(
5241        "dim".to_string(),
5242        RValue::vec(Vector::Integer(
5243            vec![Some(i64::try_from(nrow)?), Some(i64::try_from(ncol)?)].into(),
5244        )),
5245    );
5246    Ok(RValue::Vector(rv))
5247}
5248
5249/// Return a matrix of column indices (same dim as input, each element = its column number).
5250///
5251/// @param x matrix or matrix-like object (must have dim attribute)
5252/// @return integer matrix where element [i,j] = j
5253/// @namespace base
5254#[builtin(min_args = 1)]
5255fn builtin_col(args: &[RValue], _: &[(String, RValue)]) -> Result<RValue, RError> {
5256    let (nrow, ncol) = matrix_dims(args.first().unwrap_or(&RValue::Null)).ok_or_else(|| {
5257        RError::new(
5258            RErrorKind::Argument,
5259            "a matrix-like object is required as argument to 'col'".to_string(),
5260        )
5261    })?;
5262    let mut data: Vec<Option<i64>> = Vec::with_capacity(nrow * ncol);
5263    for j in 0..ncol {
5264        for _i in 0..nrow {
5265            data.push(Some(i64::try_from(j)? + 1));
5266        }
5267    }
5268    let mut rv = RVector::from(Vector::Integer(data.into()));
5269    rv.set_attr(
5270        "dim".to_string(),
5271        RValue::vec(Vector::Integer(
5272            vec![Some(i64::try_from(nrow)?), Some(i64::try_from(ncol)?)].into(),
5273        )),
5274    );
5275    Ok(RValue::Vector(rv))
5276}
5277
5278/// Return an array of indices along a specified margin.
5279///
5280/// For a matrix, `MARGIN = 1` gives row indices (same as `row()`),
5281/// `MARGIN = 2` gives column indices (same as `col()`).
5282///
5283/// @param x array (must have dim attribute)
5284/// @param MARGIN integer: which dimension to generate indices for (1-based)
5285/// @return integer array of same dimensions as x
5286/// @namespace base
5287#[builtin(name = "slice.index", min_args = 2)]
5288fn builtin_slice_index(args: &[RValue], named: &[(String, RValue)]) -> Result<RValue, RError> {
5289    let x = args
5290        .first()
5291        .ok_or_else(|| RError::new(RErrorKind::Argument, "'x' must be an array".to_string()))?;
5292
5293    let margin_val = named
5294        .iter()
5295        .find(|(n, _)| n == "MARGIN")
5296        .map(|(_, v)| v)
5297        .or(args.get(1))
5298        .ok_or_else(|| {
5299            RError::new(
5300                RErrorKind::Argument,
5301                "'MARGIN' argument is required".to_string(),
5302            )
5303        })?;
5304
5305    let margin = margin_val
5306        .as_vector()
5307        .and_then(|v| v.as_integer_scalar())
5308        .ok_or_else(|| {
5309            RError::new(
5310                RErrorKind::Argument,
5311                "'MARGIN' must be an integer scalar".to_string(),
5312            )
5313        })?;
5314
5315    let dim_attr = match x {
5316        RValue::Vector(v) => v.get_attr("dim"),
5317        RValue::List(l) => l.get_attr("dim"),
5318        _ => None,
5319    };
5320
5321    let dims: Vec<i64> = dim_attr
5322        .and_then(|d| d.as_vector())
5323        .map(|v| {
5324            v.to_integers()
5325                .into_iter()
5326                .map(|d| d.unwrap_or(0))
5327                .collect()
5328        })
5329        .ok_or_else(|| {
5330            RError::new(
5331                RErrorKind::Argument,
5332                "'x' must be an array (needs dim attribute)".to_string(),
5333            )
5334        })?;
5335
5336    let ndim = dims.len();
5337    let margin_idx = usize::try_from(margin - 1).map_err(|_| {
5338        RError::new(
5339            RErrorKind::Argument,
5340            format!("'MARGIN' = {} is out of range [1, {}]", margin, ndim),
5341        )
5342    })?;
5343
5344    if margin_idx >= ndim {
5345        return Err(RError::new(
5346            RErrorKind::Argument,
5347            format!("'MARGIN' = {} is out of range [1, {}]", margin, ndim),
5348        ));
5349    }
5350
5351    let total: usize = dims
5352        .iter()
5353        .map(|&d| usize::try_from(d).unwrap_or(0))
5354        .product();
5355    let mut data: Vec<Option<i64>> = Vec::with_capacity(total);
5356
5357    // Stride for the target dimension = product of dims before it
5358    let stride: usize = dims[..margin_idx]
5359        .iter()
5360        .map(|&d| usize::try_from(d).unwrap_or(0))
5361        .product();
5362    let dim_size = usize::try_from(dims[margin_idx]).unwrap_or(0);
5363
5364    for linear in 0..total {
5365        // The index along dimension margin_idx
5366        let idx_along_margin = (linear / stride) % dim_size;
5367        data.push(Some(i64::try_from(idx_along_margin)? + 1));
5368    }
5369
5370    let dim_values: Vec<Option<i64>> = dims.into_iter().map(Some).collect();
5371    let mut rv = RVector::from(Vector::Integer(data.into()));
5372    rv.set_attr(
5373        "dim".to_string(),
5374        RValue::vec(Vector::Integer(dim_values.into())),
5375    );
5376    Ok(RValue::Vector(rv))
5377}
5378
5379// endregion