Skip to main content

ccalc_engine/
eval.rs

1use std::cell::{Cell, RefCell};
2use std::collections::{HashMap, HashSet};
3use std::io::Write;
4
5use indexmap::IndexMap;
6use ndarray::Array2;
7use num_complex::Complex;
8use rand::{Rng, SeedableRng, rngs::SmallRng};
9
10use crate::env::{Env, LambdaFn, Value};
11use crate::io::IoContext;
12
13// ── User function call hook ──────────────────────────────────────────────────
14
15/// Signature for the hook that executes named user-defined functions.
16///
17/// Registered once by `exec::init()` before the REPL loop starts.
18/// Called by `eval_inner` when a `Value::Function` is invoked.
19/// `name` is the function name (from the call site); `caller_env` is passed so
20/// the function body can access other user-defined functions (enabling recursion
21/// and mutual recursion).
22pub type FnCallHook = fn(
23    name: &str,
24    func: &Value,
25    args: &[Value],
26    caller_env: &Env,
27    io: &mut IoContext,
28) -> Result<Value, String>;
29
30thread_local! {
31    static FN_CALL_HOOK: Cell<Option<FnCallHook>> = const { Cell::new(None) };
32}
33
34/// Registers the hook that executes named user-defined functions.
35///
36/// Must be called by `exec::init()` before any user function can be called.
37pub fn set_fn_call_hook(f: FnCallHook) {
38    FN_CALL_HOOK.with(|c| c.set(Some(f)));
39}
40
41// ── Autoload hook ───────────────────────────────────────────────────────────
42
43/// Signature for the hook that auto-loads a function file by name.
44///
45/// Called by `eval_inner` when a name is not found in the environment and not
46/// a built-in. The hook searches for `<name>.calc` / `<name>.m` on the path
47/// and, if found, inserts the primary function into the autoload cache via
48/// [`autoload_cache_insert`]. Returns `true` if the function was loaded.
49pub type AutoloadHook = fn(name: &str) -> bool;
50
51thread_local! {
52    static AUTOLOAD_HOOK: Cell<Option<AutoloadHook>> = const { Cell::new(None) };
53    /// Cache of autoloaded functions — populated by the autoload hook, read by eval_inner.
54    static AUTOLOAD_CACHE: RefCell<Env> = RefCell::new(Env::new());
55    /// Names that were searched and NOT found on the path — avoids repeated filesystem
56    /// stat() calls for built-in names (sin, cos, …) inside tight loops.
57    static AUTOLOAD_MISS_CACHE: RefCell<HashSet<String>> = RefCell::new(HashSet::new());
58}
59
60/// Registers the autoload hook. Called by `exec::init()`.
61pub fn set_autoload_hook(f: AutoloadHook) {
62    AUTOLOAD_HOOK.with(|c| c.set(Some(f)));
63}
64
65/// Inserts a function into the autoload cache. Called by `exec::try_autoload`.
66pub fn autoload_cache_insert(name: String, val: Value) {
67    AUTOLOAD_CACHE.with(|c| c.borrow_mut().insert(name, val));
68}
69
70/// Clears the negative-autoload miss cache.
71///
72/// Call this when the session path changes so that names previously not found
73/// on the old path can be re-searched on the updated path.
74pub fn clear_autoload_miss_cache() {
75    AUTOLOAD_MISS_CACHE.with(|c| c.borrow_mut().clear());
76}
77
78/// Returns an autoloaded function by name, triggering the autoload hook if needed.
79///
80/// Checks the positive cache first, then the negative (miss) cache, and only fires
81/// the filesystem hook when the name has not been tried before.  Returns `None` when
82/// neither the cache nor the hook can resolve the name.
83pub fn resolve_autoloaded(name: &str) -> Option<Value> {
84    let cached = AUTOLOAD_CACHE.with(|c| c.borrow().get(name).cloned());
85    if cached.is_some() {
86        return cached;
87    }
88    if AUTOLOAD_MISS_CACHE.with(|c| c.borrow().contains(name)) {
89        return None;
90    }
91    let hook = AUTOLOAD_HOOK.with(|c| c.get());
92    if let Some(f) = hook {
93        f(name);
94    }
95    let found = AUTOLOAD_CACHE.with(|c| c.borrow().get(name).cloned());
96    if found.is_none() {
97        AUTOLOAD_MISS_CACHE.with(|c| c.borrow_mut().insert(name.to_string()));
98    }
99    found
100}
101
102// ── Eval-string hook (set by exec::init) ────────────────────────────────────
103
104/// Executes a code string against an immutable snapshot of the env and returns `ans`.
105///
106/// Used by [`call_builtin`] when `eval()` appears in expression context
107/// (e.g. `y = eval('2+2')`). Env mutations inside the string do **not** persist —
108/// the hook executes against a clone. For env-mutating eval, use `eval()` as a
109/// standalone statement.
110pub type EvalStrHook = fn(code: &str, env: &Env) -> Result<Value, String>;
111
112thread_local! {
113    static EVAL_STR_HOOK: Cell<Option<EvalStrHook>> = const { Cell::new(None) };
114}
115
116/// Registers the hook that executes a code string in expression context.
117///
118/// Must be called by `exec::init()` before any `eval()` expression-context call.
119pub fn set_eval_str_hook(f: EvalStrHook) {
120    EVAL_STR_HOOK.with(|c| c.set(Some(f)));
121}
122
123fn call_eval_str_hook(code: &str, env: &Env) -> Result<Value, String> {
124    match EVAL_STR_HOOK.with(|c| c.get()) {
125        Some(hook) => hook(code, env),
126        None => Err("eval: exec::init() not called".to_string()),
127    }
128}
129
130// ── Tic timer (thread-local) ─────────────────────────────────────────────────
131
132thread_local! {
133    /// Start time set by the most recent `tic` call.
134    static TIC_TIME: Cell<Option<std::time::Instant>> = const { Cell::new(None) };
135}
136
137// ── Last error (thread-local) ────────────────────────────────────────────────
138
139thread_local! {
140    static LAST_ERR: RefCell<String> = const { RefCell::new(String::new()) };
141}
142
143/// Sets the last-error string (called on every caught runtime error).
144pub fn set_last_err(msg: &str) {
145    LAST_ERR.with(|e| *e.borrow_mut() = msg.to_string());
146}
147
148/// Returns the last-error string.
149pub fn get_last_err() -> String {
150    LAST_ERR.with(|e| e.borrow().clone())
151}
152
153// ── Nargout (number of expected outputs, set by exec_stmts) ─────────────────
154
155thread_local! {
156    static NARGOUT: Cell<usize> = const { Cell::new(1) };
157}
158
159/// Sets the number of output values requested by the calling assignment statement.
160///
161/// Called by `exec_stmts` before evaluating the RHS expression, so that
162/// multi-output built-ins (`eig`, `svd`, `lu`, `qr`) can determine whether to
163/// return a full `Value::Tuple` or a single value.
164pub fn set_nargout(n: usize) {
165    NARGOUT.with(|c| c.set(n));
166}
167
168fn get_nargout() -> usize {
169    NARGOUT.with(|c| c.get())
170}
171
172// ── Display context (thread-local, set by exec_stmts) ────────────────────────
173
174thread_local! {
175    static DISPLAY_FMT:     RefCell<FormatMode> = const { RefCell::new(FormatMode::Short) };
176    static DISPLAY_BASE:    Cell<Base>           = const { Cell::new(Base::Dec) };
177    static DISPLAY_COMPACT: Cell<bool>           = const { Cell::new(false) };
178}
179
180/// Sets the display context used when executing function bodies.
181///
182/// Called at the start of `exec_stmts` so that named functions called from
183/// within a block inherit the caller's display settings.
184pub fn set_display_ctx(fmt: &FormatMode, base: Base, compact: bool) {
185    DISPLAY_FMT.with(|f| *f.borrow_mut() = fmt.clone());
186    DISPLAY_BASE.with(|b| b.set(base));
187    DISPLAY_COMPACT.with(|c| c.set(compact));
188}
189
190/// Returns the current display format mode stored in the thread-local context.
191pub fn get_display_fmt() -> FormatMode {
192    DISPLAY_FMT.with(|f| f.borrow().clone())
193}
194
195/// Returns the current numeric base stored in the thread-local context.
196pub fn get_display_base() -> Base {
197    DISPLAY_BASE.with(|b| b.get())
198}
199
200/// Returns the current compact flag stored in the thread-local context.
201pub fn get_display_compact() -> bool {
202    DISPLAY_COMPACT.with(|c| c.get())
203}
204
205// ── Global variable store ────────────────────────────────────────────────────
206
207thread_local! {
208    /// Shared global workspace — variables declared `global` in any scope live here.
209    ///
210    /// Persists for the lifetime of the process. Each call to `global x` in any scope
211    /// makes `x` refer to this store rather than the local environment.
212    static GLOBAL_ENV: RefCell<Env> = RefCell::new(Env::new());
213
214    /// Stack of per-scope global name sets.
215    ///
216    /// Frame 0 = top level / script scope; each `call_user_function` call pushes a new frame
217    /// and pops it on return. `global x` in a scope adds `x` to the current (top) frame.
218    static GLOBAL_NAMES_STACK: RefCell<Vec<HashSet<String>>> =
219        RefCell::new(vec![HashSet::new()]);
220}
221
222/// Pushes an empty global-names frame (called on function entry by `exec.rs`).
223pub fn global_frame_push() {
224    GLOBAL_NAMES_STACK.with(|s| s.borrow_mut().push(HashSet::new()));
225}
226
227/// Pops the top global-names frame (called on function exit by `exec.rs`).
228pub fn global_frame_pop() {
229    GLOBAL_NAMES_STACK.with(|s| {
230        s.borrow_mut().pop();
231    });
232}
233
234/// Declares `name` as global in the current scope.
235pub fn global_declare(name: &str) {
236    GLOBAL_NAMES_STACK.with(|s| {
237        if let Some(frame) = s.borrow_mut().last_mut() {
238            frame.insert(name.to_string());
239        }
240    });
241}
242
243/// Returns `true` if `name` is declared global in the innermost active scope.
244pub fn is_global(name: &str) -> bool {
245    GLOBAL_NAMES_STACK.with(|s| s.borrow().last().is_some_and(|f| f.contains(name)))
246}
247
248/// Gets a value from the shared global store.
249pub fn global_get(name: &str) -> Option<Value> {
250    GLOBAL_ENV.with(|e| e.borrow().get(name).cloned())
251}
252
253/// Sets a value in the shared global store.
254pub fn global_set(name: &str, val: Value) {
255    GLOBAL_ENV.with(|e| e.borrow_mut().insert(name.to_string(), val));
256}
257
258/// Initialises `name` in the global store to `Scalar(0.0)` if not already present.
259pub fn global_init_if_absent(name: &str) {
260    GLOBAL_ENV.with(|e| {
261        e.borrow_mut()
262            .entry(name.to_string())
263            .or_insert(Value::Scalar(0.0));
264    });
265}
266
267/// Refreshes all names declared global in the current scope from `GLOBAL_ENV` into `env`.
268///
269/// Called at the end of `exec_stmts` to ensure that modifications made to global variables
270/// inside called functions are visible to the current scope's environment.
271pub fn global_refresh_into_env(env: &mut crate::env::Env) {
272    GLOBAL_NAMES_STACK.with(|s| {
273        GLOBAL_ENV.with(|ge| {
274            if let Some(frame) = s.borrow().last() {
275                let store = ge.borrow();
276                for name in frame {
277                    if let Some(val) = store.get(name) {
278                        env.insert(name.clone(), val.clone());
279                    }
280                }
281            }
282        });
283    });
284}
285
286// ── Persistent variable store ────────────────────────────────────────────────
287
288thread_local! {
289    /// Persistent variable values — keyed by `"funcname\x00varname"`.
290    ///
291    /// Values survive individual function calls and are restored on the next call
292    /// to the same function.
293    static PERSISTENT_STORE: RefCell<HashMap<String, Value>> =
294        RefCell::new(HashMap::new());
295
296    /// Stack of function names for constructing persistent-store keys.
297    ///
298    /// Empty string = top-level scope. `call_user_function` pushes the function name
299    /// before executing the body and pops it on return.
300    static FUNC_NAME_STACK: RefCell<Vec<String>> =
301        RefCell::new(vec![String::new()]);
302
303    /// Stack of per-scope persistent name sets — mirrors `GLOBAL_NAMES_STACK`.
304    static PERSISTENT_NAMES_STACK: RefCell<Vec<HashSet<String>>> =
305        RefCell::new(vec![HashSet::new()]);
306}
307
308/// Pushes a function scope for persistent tracking (called on function entry).
309pub fn persistent_frame_push(func_name: &str) {
310    FUNC_NAME_STACK.with(|s| s.borrow_mut().push(func_name.to_string()));
311    PERSISTENT_NAMES_STACK.with(|s| s.borrow_mut().push(HashSet::new()));
312}
313
314/// Pops the persistent frame and returns `(func_name, declared_persistent_names)`.
315pub fn persistent_frame_pop() -> (String, HashSet<String>) {
316    let func_name = FUNC_NAME_STACK.with(|s| s.borrow_mut().pop().unwrap_or_default());
317    let names = PERSISTENT_NAMES_STACK.with(|s| s.borrow_mut().pop().unwrap_or_default());
318    (func_name, names)
319}
320
321/// Declares `name` as persistent in the current function scope.
322pub fn persistent_declare(name: &str) {
323    PERSISTENT_NAMES_STACK.with(|s| {
324        if let Some(frame) = s.borrow_mut().last_mut() {
325            frame.insert(name.to_string());
326        }
327    });
328}
329
330/// Gets a saved persistent value for `(func_name, var_name)`.
331pub fn persistent_load(func_name: &str, var_name: &str) -> Option<Value> {
332    let key = format!("{func_name}\x00{var_name}");
333    PERSISTENT_STORE.with(|s| s.borrow().get(&key).cloned())
334}
335
336/// Saves a persistent value for `(func_name, var_name)`.
337pub fn persistent_save(func_name: &str, var_name: &str, val: Value) {
338    let key = format!("{func_name}\x00{var_name}");
339    PERSISTENT_STORE.with(|s| s.borrow_mut().insert(key, val));
340}
341
342/// Returns the name of the currently executing function (top of `FUNC_NAME_STACK`).
343///
344/// Returns an empty string when executing at the top level (REPL / script scope).
345pub fn current_func_name() -> String {
346    FUNC_NAME_STACK.with(|s| s.borrow().last().cloned().unwrap_or_default())
347}
348
349/// Returns `true` if `name` is declared `persistent` in the current function frame.
350pub fn is_persistent(name: &str) -> bool {
351    PERSISTENT_NAMES_STACK.with(|s| s.borrow().last().is_some_and(|frame| frame.contains(name)))
352}
353
354// ── Random-number state ──────────────────────────────────────────────────────
355
356thread_local! {
357    /// Per-thread PRNG used by `rand`, `randn`, and `randi`.
358    ///
359    /// Seeded from OS entropy on first use. Reseed with `rng(seed)` or `rng('shuffle')`.
360    static RNG: RefCell<SmallRng> = RefCell::new(SmallRng::from_entropy());
361}
362
363/// Reseeds the thread-local RNG with the given 64-bit seed.
364pub fn rng_seed(seed: u64) {
365    RNG.with(|r| *r.borrow_mut() = SmallRng::seed_from_u64(seed));
366}
367
368/// Reseeds the thread-local RNG from OS entropy.
369pub fn rng_shuffle() {
370    RNG.with(|r| *r.borrow_mut() = SmallRng::from_entropy());
371}
372
373/// Generates one uniform [0, 1) sample.
374fn rand_uniform() -> f64 {
375    RNG.with(|r| r.borrow_mut().gen_range(0.0_f64..1.0))
376}
377
378/// Generates one standard-normal sample via the Box-Muller transform.
379fn rand_normal() -> f64 {
380    let u1 = rand_uniform().max(f64::EPSILON);
381    let u2 = rand_uniform();
382    (-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos()
383}
384
385// ── AST types ────────────────────────────────────────────────────────────────
386
387/// An expression node in the AST.
388///
389/// Produced by the parser and consumed by [`eval`] / [`eval_with_io`].
390#[derive(Debug, Clone)]
391pub enum Expr {
392    /// A numeric literal (e.g. `3`, `2.5`, `1e-3`).
393    Number(f64),
394    /// A variable or constant reference (e.g. `x`, `pi`, `ans`).
395    Var(String),
396    /// Arithmetic negation: `-expr`.
397    UnaryMinus(Box<Expr>),
398    /// Logical NOT: `~expr`. Result is 1.0 if expr == 0.0, else 0.0.
399    UnaryNot(Box<Expr>),
400    /// Binary operation: `lhs op rhs`.
401    BinOp(Box<Expr>, Op, Box<Expr>),
402    /// Function call or variable indexing: `name(arg1, arg2, ...)`.
403    ///
404    /// Disambiguation happens at eval time: if `name` exists in the environment
405    /// it is treated as indexing, otherwise as a built-in or user function call.
406    Call(String, Vec<Expr>),
407    /// Matrix literal: `[row1; row2; ...]` where each row is a list of expressions.
408    Matrix(Vec<Vec<Expr>>),
409    /// Conjugate transpose: `A'`. For complex scalars, returns the conjugate.
410    Transpose(Box<Expr>),
411    /// Range expression: `start:stop` or `start:step:stop`.
412    /// Evaluates to a 1×N row vector.
413    Range(Box<Expr>, Option<Box<Expr>>, Box<Expr>),
414    /// Bare `:` used as an all-elements index in `A(:,j)` or `A(i,:)`.
415    /// Only valid as an argument inside an indexing expression.
416    Colon,
417    /// Single-quoted char array literal.
418    StrLiteral(String),
419    /// Double-quoted string object literal.
420    StringObjLiteral(String),
421    /// Anonymous function: `@(params) body_expr`.
422    ///
423    /// At evaluation time this is converted to `Value::Lambda`, capturing the
424    /// current environment as a lexical closure.
425    Lambda {
426        /// Parameter names in declaration order (e.g. `["x", "n"]`).
427        params: Vec<String>,
428        /// Body expression evaluated when the lambda is called.
429        body: Box<Expr>,
430        /// Source text for display (e.g. `@(x) x.^2 + 1`), stored at parse time.
431        source: String,
432    },
433    /// Non-conjugate (plain) transpose: `A.'`.
434    ///
435    /// Transposes without complex conjugation. For real matrices, identical to `A'`.
436    /// For complex: `z.'` returns `z` unchanged (no sign flip on imaginary part).
437    PlainTranspose(Box<Expr>),
438    /// Cell array literal: `{e1, e2, e3}`.
439    ///
440    /// Evaluates each element and produces `Value::Cell`.
441    CellLiteral(Vec<Expr>),
442    /// Cell array brace-indexing: `c{i}`.
443    ///
444    /// The first expression must evaluate to `Value::Cell`; the second is the
445    /// 1-based integer index.
446    CellIndex(Box<Expr>, Box<Expr>),
447    /// Function handle: `@funcname`.
448    ///
449    /// Produces a `Value::Lambda` that forwards its arguments to the named
450    /// built-in or user function.
451    FuncHandle(String),
452    /// Struct field read: `s.field` or chained `s.a.b` (parsed as `FieldGet(FieldGet(s,"a"),"b")`).
453    ///
454    /// At eval time the base expression must evaluate to `Value::Struct`.
455    FieldGet(Box<Expr>, String),
456    /// Package-qualified function call: `pkg.func(args)` or `pkg.sub.func(args)`.
457    ///
458    /// `segments` holds the dot-separated name components, e.g. `["utils", "my_function"]`.
459    /// At eval time:
460    /// - If `segments[0]` is in the environment (a struct or callable), the chain is followed
461    ///   as field accesses and the final value is called with the given arguments.
462    /// - Otherwise, the segments are treated as a package call: the autoload hook searches
463    ///   for `+utils/my_function.calc` (or `+utils/+sub/func.calc` for nested packages)
464    ///   on the session path and loads the function on demand.
465    DotCall(Vec<String>, Vec<Expr>),
466    /// Not-a-Time sentinel: `NaT`. Evaluates to `Value::DateTime(f64::NAN)`.
467    NaT,
468}
469
470/// A binary operator used in [`Expr::BinOp`].
471#[derive(Debug, Clone)]
472pub enum Op {
473    /// Addition: `a + b` or element-wise matrix addition.
474    Add,
475    /// Subtraction: `a - b` or element-wise matrix subtraction.
476    Sub,
477    /// Multiplication: scalar `a * b` or matrix product `A * B`.
478    Mul,
479    /// Division: scalar `a / b` or matrix right-division `A / B` (solves `X * B = A`).
480    Div,
481    /// Exponentiation: scalar `a ^ b` or matrix power `A ^ n`.
482    Pow,
483    /// Element-wise multiplication: `A .* B`.
484    ElemMul,
485    /// Element-wise division: `A ./ B`.
486    ElemDiv,
487    /// Element-wise exponentiation: `A .^ B`.
488    ElemPow,
489    // --- Comparison (element-wise, return 0.0/1.0) ---
490    /// Equality comparison: `a == b`. Returns 1.0 if equal, 0.0 otherwise.
491    Eq,
492    /// Inequality comparison: `a ~= b`. Returns 1.0 if not equal, 0.0 otherwise.
493    NotEq,
494    /// Less-than comparison: `a < b`.
495    Lt,
496    /// Greater-than comparison: `a > b`.
497    Gt,
498    /// Less-than-or-equal comparison: `a <= b`.
499    LtEq,
500    /// Greater-than-or-equal comparison: `a >= b`.
501    GtEq,
502    // --- Short-circuit logical (scalars only) ---
503    /// Short-circuit logical AND: `a && b`. Only evaluates `b` if `a` is truthy.
504    And,
505    /// Short-circuit logical OR: `a || b`. Only evaluates `b` if `a` is falsy.
506    Or,
507    // --- Element-wise logical (matrices allowed, no short-circuit) ---
508    /// Element-wise logical AND: `A & B`. Evaluates both sides; works on matrices.
509    ElemAnd,
510    /// Element-wise logical OR: `A | B`. Evaluates both sides; works on matrices.
511    ElemOr,
512    /// Left division: `A \ b` solves `A*x = b`. Scalar: `a \ b = b / a`.
513    LDiv,
514}
515
516/// The numeric base used when displaying integer-valued scalars.
517#[derive(Debug, Clone, Copy, PartialEq, Default)]
518pub enum Base {
519    /// Decimal (base 10) — the default.
520    #[default]
521    Dec,
522    /// Hexadecimal (base 16), prefix `0x` (e.g. `0xff`).
523    Hex,
524    /// Binary (base 2), prefix `0b` (e.g. `0b1010`).
525    Bin,
526    /// Octal (base 8), prefix `0o` (e.g. `0o17`).
527    Oct,
528}
529
530/// Controls how numbers are displayed (MATLAB-compatible format modes).
531#[derive(Debug, Clone, PartialEq)]
532pub enum FormatMode {
533    /// 5 significant digits, auto fixed/scientific (MATLAB `format short`).
534    Short,
535    /// 15 significant digits, auto fixed/scientific (MATLAB `format long`).
536    Long,
537    /// Always scientific notation, 4 decimal places — 5 sig digits.
538    ShortE,
539    /// Always scientific notation, 14 decimal places — 15 sig digits.
540    LongE,
541    /// Same as `Short` for scalars (MATLAB `format shortG`).
542    ShortG,
543    /// Same as `Long` for scalars (MATLAB `format longG`).
544    LongG,
545    /// Fixed 2 decimal places — currency (MATLAB `format bank`).
546    Bank,
547    /// Rational approximation `p/q` (MATLAB `format rat`).
548    Rat,
549    /// IEEE 754 hexadecimal bit pattern, 16 uppercase hex digits (MATLAB `format hex`).
550    Hex,
551    /// Sign character only: `+`, `-`, or ` ` for zero (MATLAB `format +`).
552    Plus,
553    /// N decimal places, auto fixed/scientific — legacy precision= setting.
554    Custom(usize),
555}
556
557impl Default for FormatMode {
558    fn default() -> Self {
559        FormatMode::Custom(10)
560    }
561}
562
563impl FormatMode {
564    /// Human-readable name for display in `config` / status messages.
565    pub fn name(&self) -> String {
566        match self {
567            FormatMode::Short => "short".to_string(),
568            FormatMode::Long => "long".to_string(),
569            FormatMode::ShortE => "shortE".to_string(),
570            FormatMode::LongE => "longE".to_string(),
571            FormatMode::ShortG => "shortG".to_string(),
572            FormatMode::LongG => "longG".to_string(),
573            FormatMode::Bank => "bank".to_string(),
574            FormatMode::Rat => "rat".to_string(),
575            FormatMode::Hex => "hex".to_string(),
576            FormatMode::Plus => "+".to_string(),
577            FormatMode::Custom(n) => format!("custom({n})"),
578        }
579    }
580}
581
582/// Evaluates an expression without file I/O context.
583/// This is the public API used by tests and non-I/O evaluation paths.
584pub fn eval(expr: &Expr, env: &Env) -> Result<Value, String> {
585    eval_inner(expr, env, None)
586}
587
588/// Evaluates an expression with an I/O context (file descriptor table).
589/// Used by the REPL to support `fopen`/`fclose`/`fgetl`/`fgets`/`fprintf(fd,...)`.
590pub fn eval_with_io(expr: &Expr, env: &Env, io: &mut IoContext) -> Result<Value, String> {
591    eval_inner(expr, env, Some(io))
592}
593
594fn eval_inner(expr: &Expr, env: &Env, mut io: Option<&mut IoContext>) -> Result<Value, String> {
595    match expr {
596        Expr::Number(n) => Ok(Value::Scalar(*n)),
597        Expr::Var(name) => env.get(name).cloned().ok_or(()).or_else(|_| {
598            // Check the shared global store when the name is declared global in this scope.
599            if is_global(name)
600                && let Some(val) = global_get(name)
601            {
602                return Ok(val);
603            }
604            // 'e' falls back to Euler's number if not defined in env
605            if name == "e" {
606                return Ok(Value::Scalar(std::f64::consts::E));
607            }
608            // Try as a zero-argument built-in call (e.g., `tic`, `toc` written without parens).
609            if let Ok(val) = call_builtin(name, &[], env, io.as_deref_mut()) {
610                return Ok(val);
611            }
612            let hint = suggest_similar(name, env);
613            match hint {
614                Some(s) => Err(format!("Undefined variable '{name}'; did you mean '{s}'?")),
615                None => Err(format!("Undefined variable: '{name}'")),
616            }
617        }),
618        Expr::UnaryMinus(e) => match eval_inner(e, env, io)? {
619            Value::Void => Err("Unary minus is not applicable to void".to_string()),
620            Value::Scalar(n) => Ok(Value::Scalar(-n)),
621            Value::Matrix(m) => Ok(Value::Matrix(m.mapv(|x| -x))),
622            Value::Complex(re, im) => Ok(Value::Complex(-re, -im)),
623            Value::ComplexMatrix(m) => Ok(Value::ComplexMatrix(m.mapv(|c| -c))),
624            Value::Str(s) => match str_to_numeric(&s) {
625                Value::Scalar(n) => Ok(Value::Scalar(-n)),
626                Value::Matrix(m) => Ok(Value::Matrix(m.mapv(|x| -x))),
627                _ => unreachable!(),
628            },
629            Value::StringObj(_) => {
630                Err("Unary minus is not applicable to string objects".to_string())
631            }
632            Value::Lambda(_)
633            | Value::Function { .. }
634            | Value::Tuple(_)
635            | Value::Cell(_)
636            | Value::Struct(_)
637            | Value::StructArray(_)
638            | Value::DateTime(_)
639            | Value::Duration(_)
640            | Value::DateTimeArray(_)
641            | Value::DurationArray(_) => {
642                Err("Unary minus is not applicable to this type".to_string())
643            }
644        },
645        Expr::UnaryNot(e) => match eval_inner(e, env, io)? {
646            Value::Void => Err("Logical NOT is not applicable to void".to_string()),
647            Value::Scalar(n) => Ok(Value::Scalar(if n == 0.0 { 1.0 } else { 0.0 })),
648            Value::Matrix(m) => Ok(Value::Matrix(m.mapv(|x| if x == 0.0 { 1.0 } else { 0.0 }))),
649            Value::Complex(re, im) => Ok(Value::Scalar(if re == 0.0 && im == 0.0 {
650                1.0
651            } else {
652                0.0
653            })),
654            Value::ComplexMatrix(m) => {
655                Ok(Value::Matrix(m.mapv(|c| {
656                    if c.re == 0.0 && c.im == 0.0 { 1.0 } else { 0.0 }
657                })))
658            }
659            Value::Str(s) => match str_to_numeric(&s) {
660                Value::Scalar(n) => Ok(Value::Scalar(if n == 0.0 { 1.0 } else { 0.0 })),
661                Value::Matrix(m) => Ok(Value::Matrix(m.mapv(|x| if x == 0.0 { 1.0 } else { 0.0 }))),
662                _ => unreachable!(),
663            },
664            Value::StringObj(_) => {
665                Err("Logical NOT is not applicable to string objects".to_string())
666            }
667            Value::Lambda(_)
668            | Value::Function { .. }
669            | Value::Tuple(_)
670            | Value::Cell(_)
671            | Value::Struct(_)
672            | Value::StructArray(_)
673            | Value::DateTime(_)
674            | Value::Duration(_)
675            | Value::DateTimeArray(_)
676            | Value::DurationArray(_) => {
677                Err("Logical NOT is not applicable to this type".to_string())
678            }
679        },
680        Expr::BinOp(left, op, right) => {
681            let l = eval_inner(left, env, io.as_deref_mut())?;
682            let r = eval_inner(right, env, io)?;
683            eval_binop(l, op, r)
684        }
685        Expr::Call(name, args) => {
686            // try(expr, default) — special form: evaluate expr; on error evaluate default.
687            // Arguments are NOT pre-evaluated; lazy semantics.
688            if name == "try" && args.len() == 2 {
689                return match eval_inner(&args[0], env, io.as_deref_mut()) {
690                    Ok(v) => Ok(v),
691                    Err(msg) => {
692                        set_last_err(&msg);
693                        eval_inner(&args[1], env, io.as_deref_mut())
694                    }
695                };
696            }
697
698            // If the name resolves to a variable in env, check its type.
699            // User functions (Lambda, Function) are called; other values are indexed.
700            // Variables shadow built-in function names (Octave semantics).
701            //
702            // Non-function variables are forwarded via borrow (no clone) to avoid
703            // copying large matrix values on every indexed read (e.g. x(k) in a loop).
704            if let Some(env_val) = env.get(name) {
705                if !matches!(env_val, Value::Lambda(_) | Value::Function { .. }) {
706                    return eval_index(env_val, args, env);
707                }
708                // Lambda/Function: clone is cheap (Rc for Lambda, Strings for Function).
709                let val = env_val.clone();
710                match &val {
711                    Value::Lambda(f) => {
712                        // Evaluate arguments and call the closure directly.
713                        // Empty call → inject ans (convenience: sq() = sq(ans)).
714                        let mut evaled = Vec::with_capacity(args.len().max(1));
715                        for a in args {
716                            evaled.push(eval_inner(a, env, io.as_deref_mut())?);
717                        }
718                        if evaled.is_empty() {
719                            evaled.push(env.get("ans").cloned().unwrap_or(Value::Scalar(0.0)));
720                        }
721                        let f = f.clone();
722                        return f.0(&evaled, io);
723                    }
724                    Value::Function { .. } => {
725                        // Evaluate arguments and dispatch to the registered hook in exec.rs.
726                        // User functions receive the raw arg list — NO ans injection. Empty call
727                        // means no arguments (varargin = {}), matching MATLAB semantics.
728                        let mut evaled = Vec::with_capacity(args.len());
729                        for a in args {
730                            evaled.push(eval_inner(a, env, io.as_deref_mut())?);
731                        }
732                        return match io.as_deref_mut() {
733                            Some(io_ref) => FN_CALL_HOOK.with(|c| match c.get() {
734                                Some(hook) => hook(name, &val, &evaled, env, io_ref),
735                                None => Err(format!(
736                                    "'{name}': user function execution not initialized \
737                                         (call exec::init() first)"
738                                )),
739                            }),
740                            None => {
741                                // No I/O context — create a temporary one (functions that do
742                                // file I/O in this path will silently fail to open files).
743                                let mut tmp_io = IoContext::new();
744                                FN_CALL_HOOK.with(|c| match c.get() {
745                                    Some(hook) => hook(name, &val, &evaled, env, &mut tmp_io),
746                                    None => Err(format!(
747                                        "'{name}': user function execution not initialized"
748                                    )),
749                                })
750                            }
751                        };
752                    }
753                    _ => unreachable!(),
754                }
755            }
756            // Autoload: search for <name>.calc / <name>.m if not in env.
757            // Check positive cache → negative (miss) cache → fire filesystem hook.
758            // Names that fail the hook are recorded in the miss cache so the
759            // filesystem is not searched again within the same session.
760            let autoloaded_val = AUTOLOAD_CACHE
761                .with(|c| c.borrow().get(name).cloned())
762                .or_else(|| {
763                    if AUTOLOAD_MISS_CACHE.with(|c| c.borrow().contains(name.as_str())) {
764                        return None;
765                    }
766                    let loaded = AUTOLOAD_HOOK
767                        .with(|c| c.get())
768                        .is_some_and(|hook| hook(name));
769                    if loaded {
770                        AUTOLOAD_CACHE.with(|c| c.borrow().get(name).cloned())
771                    } else {
772                        AUTOLOAD_MISS_CACHE.with(|c| c.borrow_mut().insert(name.to_string()));
773                        None
774                    }
775                });
776            if let Some(val) = autoloaded_val {
777                let mut evaled = Vec::with_capacity(args.len());
778                for a in args {
779                    evaled.push(eval_inner(a, env, io.as_deref_mut())?);
780                }
781                return match io.as_deref_mut() {
782                    Some(io_ref) => FN_CALL_HOOK.with(|c| match c.get() {
783                        Some(hook) => hook(name, &val, &evaled, env, io_ref),
784                        None => Err(format!("'{name}': exec::init() not called")),
785                    }),
786                    None => {
787                        let mut tmp_io = IoContext::new();
788                        FN_CALL_HOOK.with(|c| match c.get() {
789                            Some(hook) => hook(name, &val, &evaled, env, &mut tmp_io),
790                            None => Err(format!("'{name}': exec::init() not called")),
791                        })
792                    }
793                };
794            }
795
796            // Builtin path: empty call → inject ans (sqrt() = sqrt(ans)).
797            let mut evaled = Vec::with_capacity(args.len().max(1));
798            for a in args {
799                evaled.push(eval_inner(a, env, io.as_deref_mut())?);
800            }
801            // Don't inject ans for functions that take explicit struct/cell args
802            // or constructors where zero args is meaningful.
803            let no_ans_inject = matches!(
804                name.as_str(),
805                "struct"
806                    | "fieldnames"
807                    | "isfield"
808                    | "rmfield"
809                    | "isstruct"
810                    | "cell"
811                    | "iscell"
812                    | "isempty"
813                    | "cellfun"
814                    | "error"
815                    | "warning"
816                    | "lasterr"
817                    | "pcall"
818                    | "rand"
819                    | "randn"
820                    | "rng"
821                    | "tic"
822                    | "toc"
823            );
824            if evaled.is_empty() && !no_ans_inject {
825                evaled.push(env.get("ans").cloned().unwrap_or(Value::Scalar(0.0)));
826            }
827            call_builtin(name, &evaled, env, io)
828        }
829
830        Expr::Lambda {
831            params,
832            body,
833            source,
834        } => {
835            // Capture the current environment and body expression at definition time.
836            // The resulting Value::Lambda is a closure that binds params on each call.
837            let captured_env = env.clone();
838            let captured_params = params.clone();
839            let captured_body = *body.clone();
840            let src = source.clone();
841            let lambda = LambdaFn(
842                std::rc::Rc::new(move |args: &[Value], io: Option<&mut IoContext>| {
843                    // Allow up to params.len()+1 args: the parser injects `ans` for empty f() calls.
844                    let effective = if args.len() > captured_params.len() {
845                        if args.len() > captured_params.len() + 1 {
846                            return Err(format!(
847                                "Lambda: too many arguments (expected at most {}, got {})",
848                                captured_params.len(),
849                                args.len()
850                            ));
851                        }
852                        &args[..captured_params.len()]
853                    } else {
854                        args
855                    };
856                    let mut local_env = captured_env.clone();
857                    for (p, a) in captured_params.iter().zip(effective.iter()) {
858                        local_env.insert(p.clone(), a.clone());
859                    }
860                    local_env.insert("nargin".to_string(), Value::Scalar(effective.len() as f64));
861                    eval_inner(&captured_body, &local_env, io)
862                }),
863                src,
864            );
865            Ok(Value::Lambda(lambda))
866        }
867        Expr::CellLiteral(elems) => {
868            let mut vals = Vec::with_capacity(elems.len());
869            for e in elems {
870                vals.push(eval_inner(e, env, io.as_deref_mut())?);
871            }
872            Ok(Value::Cell(vals))
873        }
874        Expr::CellIndex(cell_expr, idx_expr) => {
875            let cell = eval_inner(cell_expr, env, io.as_deref_mut())?;
876            let idx = eval_inner(idx_expr, env, io)?;
877            match (cell, idx) {
878                (Value::Cell(v), Value::Scalar(i)) => {
879                    let i = i as isize;
880                    if i < 1 || i as usize > v.len() {
881                        Err(format!("Cell index {} out of range (1..{})", i, v.len()))
882                    } else {
883                        Ok(v[(i - 1) as usize].clone())
884                    }
885                }
886                (Value::Cell(_), _) => Err("Cell index must be a scalar integer".to_string()),
887                _ => Err("Brace indexing '{}' is only valid on cell arrays".to_string()),
888            }
889        }
890        Expr::FieldGet(base_expr, field) => {
891            let base_val = eval_inner(base_expr, env, io)?;
892            match base_val {
893                Value::Struct(map) => map
894                    .get(field)
895                    .cloned()
896                    .ok_or_else(|| format!("No field '{field}' in struct")),
897                // s.field on a struct array — collect field values across all elements
898                Value::StructArray(arr) => {
899                    let mut values: Vec<Value> = Vec::with_capacity(arr.len());
900                    for (idx, elem) in arr.iter().enumerate() {
901                        let v = elem.get(field).cloned().ok_or_else(|| {
902                            format!("No field '{field}' in struct array element {}", idx + 1)
903                        })?;
904                        values.push(v);
905                    }
906                    // If all values are scalars, return a 1×N matrix; otherwise a cell.
907                    let all_scalar = values.iter().all(|v| matches!(v, Value::Scalar(_)));
908                    if all_scalar {
909                        let nums: Vec<f64> = values
910                            .into_iter()
911                            .map(|v| {
912                                if let Value::Scalar(n) = v {
913                                    n
914                                } else {
915                                    unreachable!()
916                                }
917                            })
918                            .collect();
919                        let n = nums.len();
920                        Ok(Value::Matrix(Array2::from_shape_vec((1, n), nums).unwrap()))
921                    } else {
922                        Ok(Value::Cell(values))
923                    }
924                }
925                _ => Err(format!(
926                    "Cannot access field '{field}' on a non-struct value"
927                )),
928            }
929        }
930        Expr::DotCall(segs, args) => {
931            let qualified = segs.join(".");
932            // If the head segment is a variable, follow the field chain and call the result.
933            if let Some(head_val) = env.get(&segs[0]).cloned() {
934                let mut val = head_val;
935                for field in &segs[1..] {
936                    val = match val {
937                        Value::Struct(ref map) => map
938                            .get(field)
939                            .cloned()
940                            .ok_or_else(|| format!("No field '{field}' in struct"))?,
941                        _ => {
942                            return Err(format!(
943                                "Cannot access field '{field}' on a non-struct value"
944                            ));
945                        }
946                    };
947                }
948                let mut evaled = Vec::with_capacity(args.len());
949                for a in args {
950                    evaled.push(eval_inner(a, env, io.as_deref_mut())?);
951                }
952                return match val {
953                    Value::Lambda(f) => {
954                        if evaled.is_empty() {
955                            evaled.push(env.get("ans").cloned().unwrap_or(Value::Scalar(0.0)));
956                        }
957                        f.0(&evaled, io)
958                    }
959                    Value::Function { .. } => match io.as_deref_mut() {
960                        Some(io_ref) => FN_CALL_HOOK.with(|c| match c.get() {
961                            Some(hook) => hook(&qualified, &val, &evaled, env, io_ref),
962                            None => Err(format!("'{qualified}': exec::init() not called")),
963                        }),
964                        None => {
965                            let mut tmp_io = IoContext::new();
966                            FN_CALL_HOOK.with(|c| match c.get() {
967                                Some(hook) => hook(&qualified, &val, &evaled, env, &mut tmp_io),
968                                None => Err(format!("'{qualified}': exec::init() not called")),
969                            })
970                        }
971                    },
972                    _ => Err(format!("'{qualified}': not a callable")),
973                };
974            }
975            // Package call: autoload from +pkg/func.calc then invoke.
976            let cached = AUTOLOAD_CACHE.with(|c| c.borrow().get(&qualified).cloned());
977            let autoloaded_val = cached.or_else(|| {
978                let loaded = AUTOLOAD_HOOK
979                    .with(|c| c.get())
980                    .is_some_and(|hook| hook(&qualified));
981                if loaded {
982                    AUTOLOAD_CACHE.with(|c| c.borrow().get(&qualified).cloned())
983                } else {
984                    None
985                }
986            });
987            if let Some(val) = autoloaded_val {
988                let mut evaled = Vec::with_capacity(args.len());
989                for a in args {
990                    evaled.push(eval_inner(a, env, io.as_deref_mut())?);
991                }
992                return match io.as_deref_mut() {
993                    Some(io_ref) => FN_CALL_HOOK.with(|c| match c.get() {
994                        Some(hook) => hook(&qualified, &val, &evaled, env, io_ref),
995                        None => Err(format!("'{qualified}': exec::init() not called")),
996                    }),
997                    None => {
998                        let mut tmp_io = IoContext::new();
999                        FN_CALL_HOOK.with(|c| match c.get() {
1000                            Some(hook) => hook(&qualified, &val, &evaled, env, &mut tmp_io),
1001                            None => Err(format!("'{qualified}': exec::init() not called")),
1002                        })
1003                    }
1004                };
1005            }
1006            Err(format!("Unknown package function: '{qualified}'"))
1007        }
1008        Expr::FuncHandle(name) => {
1009            let name = name.clone();
1010            let captured_env = env.clone();
1011            let src = format!("@{name}");
1012            let lambda = LambdaFn(
1013                std::rc::Rc::new(move |args: &[Value], io: Option<&mut IoContext>| {
1014                    // First try the environment (user-defined function), then fall back to builtin.
1015                    if let Some(f) = captured_env.get(&name) {
1016                        let f = f.clone();
1017                        call_function_value(&f, args, io)
1018                    } else {
1019                        call_builtin(&name, args, &captured_env, io)
1020                    }
1021                }),
1022                src,
1023            );
1024            Ok(Value::Lambda(lambda))
1025        }
1026        Expr::PlainTranspose(e) => match eval_inner(e, env, io)? {
1027            Value::Void => Err("Transpose is not applicable to void".to_string()),
1028            Value::Scalar(n) => Ok(Value::Scalar(n)),
1029            Value::Matrix(m) => Ok(Value::Matrix(m.t().to_owned())),
1030            // Plain transpose: no conjugation — imaginary part unchanged
1031            Value::Complex(re, im) => Ok(Value::Complex(re, im)),
1032            // Plain transpose of complex matrix: swap axes only, no conjugation
1033            Value::ComplexMatrix(m) => Ok(Value::ComplexMatrix(m.t().to_owned())),
1034            Value::Str(s) => Ok(Value::Str(s)),
1035            Value::StringObj(s) => Ok(Value::StringObj(s)),
1036            // Arrays: orientation is ignored (Vec<f64> is always 1-D), return as-is.
1037            v @ (Value::DateTimeArray(_) | Value::DurationArray(_)) => Ok(v),
1038            Value::Lambda(_)
1039            | Value::Function { .. }
1040            | Value::Tuple(_)
1041            | Value::Cell(_)
1042            | Value::Struct(_)
1043            | Value::StructArray(_)
1044            | Value::DateTime(_)
1045            | Value::Duration(_) => Err("Transpose is not applicable to this type".to_string()),
1046        },
1047        Expr::Colon => Err("':' is only valid inside index expressions".to_string()),
1048        Expr::NaT => Ok(Value::DateTime(f64::NAN)),
1049        Expr::Matrix(rows) => {
1050            if rows.is_empty() {
1051                return Ok(Value::Matrix(Array2::<f64>::zeros((0, 0))));
1052            }
1053
1054            // Pass 1: evaluate all elements, skipping empty rows.
1055            let mut evaluated: Vec<Vec<Value>> = Vec::with_capacity(rows.len());
1056            for row in rows {
1057                if row.is_empty() {
1058                    continue;
1059                }
1060                let mut ev_row: Vec<Value> = Vec::with_capacity(row.len());
1061                for elem_expr in row {
1062                    ev_row.push(eval_inner(elem_expr, env, io.as_deref_mut())?);
1063                }
1064                evaluated.push(ev_row);
1065            }
1066            if evaluated.is_empty() {
1067                return Ok(Value::Matrix(Array2::<f64>::zeros((0, 0))));
1068            }
1069
1070            // Pass 2: detect if any element is complex (scan the entire evaluated grid).
1071            let has_complex = evaluated
1072                .iter()
1073                .flat_map(|row| row.iter())
1074                .any(|v| matches!(v, Value::Complex(_, _) | Value::ComplexMatrix(_)));
1075
1076            // Dispatch on element kind (complex takes priority over numeric).
1077            enum MatKind {
1078                ComplexNumeric,
1079                Numeric,
1080                DateTime,
1081                Duration,
1082                Str,
1083            }
1084            let kind = if has_complex {
1085                MatKind::ComplexNumeric
1086            } else {
1087                match &evaluated[0][0] {
1088                    Value::Scalar(_) | Value::Matrix(_) => MatKind::Numeric,
1089                    Value::DateTime(_) | Value::DateTimeArray(_) => MatKind::DateTime,
1090                    Value::Duration(_) | Value::DurationArray(_) => MatKind::Duration,
1091                    Value::Str(_) | Value::StringObj(_) => MatKind::Str,
1092                    Value::Void => {
1093                        return Err("Void value cannot be used in matrix literal".to_string());
1094                    }
1095                    Value::Lambda(_)
1096                    | Value::Function { .. }
1097                    | Value::Tuple(_)
1098                    | Value::Cell(_)
1099                    | Value::Struct(_)
1100                    | Value::StructArray(_) => {
1101                        return Err("This type cannot be used in matrix literals".to_string());
1102                    }
1103                    // Cannot reach here — has_complex covers this
1104                    Value::Complex(_, _) | Value::ComplexMatrix(_) => unreachable!(),
1105                }
1106            };
1107
1108            match kind {
1109                MatKind::ComplexNumeric => {
1110                    // Build a ComplexMatrix by upcasting all elements.
1111                    // Each element can be Scalar, Complex, Matrix (real), or ComplexMatrix.
1112                    let mut row_blocks: Vec<Array2<Complex<f64>>> =
1113                        Vec::with_capacity(evaluated.len());
1114                    for ev_row in &evaluated {
1115                        let mut elem_mats: Vec<Array2<Complex<f64>>> =
1116                            Vec::with_capacity(ev_row.len());
1117                        for val in ev_row {
1118                            let block: Array2<Complex<f64>> = match val {
1119                                Value::Scalar(n) => {
1120                                    Array2::from_elem((1, 1), Complex::new(*n, 0.0))
1121                                }
1122                                Value::Complex(re, im) => {
1123                                    Array2::from_elem((1, 1), Complex::new(*re, *im))
1124                                }
1125                                Value::Matrix(m) => cm_from_real(m),
1126                                Value::ComplexMatrix(m) => m.clone(),
1127                                _ => {
1128                                    return Err(
1129                                        "This type cannot be used in a complex matrix literal"
1130                                            .to_string(),
1131                                    );
1132                                }
1133                            };
1134                            elem_mats.push(block);
1135                        }
1136                        let nrows = elem_mats[0].nrows();
1137                        for (i, m) in elem_mats.iter().enumerate().skip(1) {
1138                            if m.nrows() != nrows {
1139                                return Err(format!(
1140                                    "Matrix row height mismatch: expected {} rows, element {} has {} rows",
1141                                    nrows,
1142                                    i + 1,
1143                                    m.nrows()
1144                                ));
1145                            }
1146                        }
1147                        let ncols: usize = elem_mats.iter().map(|m| m.ncols()).sum();
1148                        let mut flat: Vec<Complex<f64>> = Vec::with_capacity(nrows * ncols);
1149                        for r in 0..nrows {
1150                            for m in &elem_mats {
1151                                flat.extend(m.row(r).iter().copied());
1152                            }
1153                        }
1154                        row_blocks.push(
1155                            Array2::from_shape_vec((nrows, ncols), flat)
1156                                .map_err(|e| format!("Matrix shape error: {e}"))?,
1157                        );
1158                    }
1159                    if row_blocks.is_empty() {
1160                        return Ok(Value::ComplexMatrix(Array2::zeros((0, 0))));
1161                    }
1162                    let ncols = row_blocks[0].ncols();
1163                    for (i, blk) in row_blocks.iter().enumerate().skip(1) {
1164                        if blk.ncols() != ncols {
1165                            return Err(format!(
1166                                "Matrix column count mismatch: expected {} columns, row {} has {} columns",
1167                                ncols,
1168                                i + 1,
1169                                blk.ncols()
1170                            ));
1171                        }
1172                    }
1173                    let total_rows: usize = row_blocks.iter().map(|b| b.nrows()).sum();
1174                    let mut flat: Vec<Complex<f64>> = Vec::with_capacity(total_rows * ncols);
1175                    for blk in &row_blocks {
1176                        flat.extend(blk.iter().copied());
1177                    }
1178                    let m = Array2::from_shape_vec((total_rows, ncols), flat)
1179                        .map_err(|e| format!("Matrix shape error: {e}"))?;
1180                    Ok(Value::ComplexMatrix(m))
1181                }
1182                MatKind::DateTime => {
1183                    let mut ts: Vec<f64> = Vec::new();
1184                    for ev_row in &evaluated {
1185                        for val in ev_row {
1186                            match val {
1187                                Value::DateTime(t) => ts.push(*t),
1188                                Value::DateTimeArray(v) => ts.extend_from_slice(v),
1189                                _ => {
1190                                    return Err(
1191                                        "Matrix literal: cannot mix datetime with other types"
1192                                            .to_string(),
1193                                    );
1194                                }
1195                            }
1196                        }
1197                    }
1198                    Ok(Value::DateTimeArray(ts))
1199                }
1200                MatKind::Duration => {
1201                    let mut sv: Vec<f64> = Vec::new();
1202                    for ev_row in &evaluated {
1203                        for val in ev_row {
1204                            match val {
1205                                Value::Duration(s) => sv.push(*s),
1206                                Value::DurationArray(v) => sv.extend_from_slice(v),
1207                                _ => {
1208                                    return Err(
1209                                        "Matrix literal: cannot mix duration with other types"
1210                                            .to_string(),
1211                                    );
1212                                }
1213                            }
1214                        }
1215                    }
1216                    Ok(Value::DurationArray(sv))
1217                }
1218                MatKind::Numeric => {
1219                    // Each row is horizontally concatenated into an Array2 block;
1220                    // blocks are then vertically concatenated.
1221                    let mut row_blocks: Vec<Array2<f64>> = Vec::with_capacity(evaluated.len());
1222                    for ev_row in &evaluated {
1223                        let mut elem_mats: Vec<Array2<f64>> = Vec::with_capacity(ev_row.len());
1224                        for val in ev_row {
1225                            match val {
1226                                Value::Scalar(n) => {
1227                                    elem_mats.push(Array2::from_elem((1, 1), *n));
1228                                }
1229                                Value::Matrix(m) => elem_mats.push(m.clone()),
1230                                Value::Void => {
1231                                    return Err(
1232                                        "Void value cannot be used in matrix literal".to_string()
1233                                    );
1234                                }
1235                                // In numeric context, char arrays contribute their
1236                                // Unicode code values — MATLAB compatible: [65 'b'] = [65 98]
1237                                Value::Str(s) | Value::StringObj(s) => {
1238                                    let codes: Vec<f64> =
1239                                        s.chars().map(|c| c as u32 as f64).collect();
1240                                    let mat = if codes.is_empty() {
1241                                        Array2::<f64>::zeros((1, 0))
1242                                    } else {
1243                                        Array2::from_shape_vec((1, codes.len()), codes)
1244                                            .map_err(|e| format!("Matrix shape error: {e}"))?
1245                                    };
1246                                    elem_mats.push(mat);
1247                                }
1248                                _ => {
1249                                    return Err(
1250                                        "This type cannot be used in matrix literals".to_string()
1251                                    );
1252                                }
1253                            }
1254                        }
1255                        let nrows = elem_mats[0].nrows();
1256                        for (i, m) in elem_mats.iter().enumerate().skip(1) {
1257                            if m.nrows() != nrows {
1258                                return Err(format!(
1259                                    "Matrix row height mismatch: expected {} rows, element {} has {} rows",
1260                                    nrows,
1261                                    i + 1,
1262                                    m.nrows()
1263                                ));
1264                            }
1265                        }
1266                        let ncols: usize = elem_mats.iter().map(|m| m.ncols()).sum();
1267                        let mut flat: Vec<f64> = Vec::with_capacity(nrows * ncols);
1268                        for r in 0..nrows {
1269                            for m in &elem_mats {
1270                                flat.extend(m.row(r).iter().copied());
1271                            }
1272                        }
1273                        row_blocks.push(
1274                            Array2::from_shape_vec((nrows, ncols), flat)
1275                                .map_err(|e| format!("Matrix shape error: {e}"))?,
1276                        );
1277                    }
1278                    if row_blocks.is_empty() {
1279                        return Ok(Value::Matrix(Array2::<f64>::zeros((0, 0))));
1280                    }
1281                    let ncols = row_blocks[0].ncols();
1282                    if ncols == 0 {
1283                        let total_rows: usize = row_blocks.iter().map(|b| b.nrows()).sum();
1284                        return Ok(Value::Matrix(Array2::zeros((total_rows, 0))));
1285                    }
1286                    for (i, blk) in row_blocks.iter().enumerate().skip(1) {
1287                        if blk.ncols() != ncols {
1288                            return Err(format!(
1289                                "Matrix column count mismatch: expected {} columns, row {} has {} columns",
1290                                ncols,
1291                                i + 1,
1292                                blk.ncols()
1293                            ));
1294                        }
1295                    }
1296                    let total_rows: usize = row_blocks.iter().map(|b| b.nrows()).sum();
1297                    let mut flat: Vec<f64> = Vec::with_capacity(total_rows * ncols);
1298                    for blk in &row_blocks {
1299                        flat.extend(blk.iter().copied());
1300                    }
1301                    let m = Array2::from_shape_vec((total_rows, ncols), flat)
1302                        .map_err(|e| format!("Matrix shape error: {e}"))?;
1303                    Ok(Value::Matrix(m))
1304                }
1305                MatKind::Str => {
1306                    if evaluated.len() > 1 {
1307                        return Err("Multi-row char-array literals are not supported".to_string());
1308                    }
1309                    let mut out = String::new();
1310                    for val in &evaluated[0] {
1311                        match val {
1312                            Value::Str(s) | Value::StringObj(s) => out.push_str(s),
1313                            Value::Scalar(n) => {
1314                                let code = n.round();
1315                                out.push(
1316                                    char::from_u32(code as u32)
1317                                        .ok_or_else(|| format!("char: invalid code {n}"))?,
1318                                );
1319                            }
1320                            Value::Matrix(m) => {
1321                                for &n in m.iter() {
1322                                    out.push(
1323                                        char::from_u32(n.round() as u32)
1324                                            .ok_or_else(|| format!("char: invalid code {n}"))?,
1325                                    );
1326                                }
1327                            }
1328                            _ => {
1329                                return Err(
1330                                    "This type cannot be used in a char-array literal".to_string()
1331                                );
1332                            }
1333                        }
1334                    }
1335                    Ok(Value::Str(out))
1336                }
1337            }
1338        }
1339        Expr::Transpose(e) => match eval_inner(e, env, io)? {
1340            Value::Void => Err("Transpose is not applicable to void".to_string()),
1341            Value::Scalar(n) => Ok(Value::Scalar(n)),
1342            Value::Matrix(m) => Ok(Value::Matrix(m.t().to_owned())),
1343            Value::Complex(re, im) => Ok(Value::Complex(re, -im)),
1344            // Conjugate transpose (Hermitian): transpose axes + conjugate each element
1345            Value::ComplexMatrix(m) => Ok(Value::ComplexMatrix(m.t().mapv(|c| c.conj()))),
1346            // Transpose of a char array or string object: return as-is (1×N not fully supported)
1347            Value::Str(s) => Ok(Value::Str(s)),
1348            Value::StringObj(s) => Ok(Value::StringObj(s)),
1349            // Arrays: orientation is ignored (Vec<f64> is always 1-D), return as-is.
1350            v @ (Value::DateTimeArray(_) | Value::DurationArray(_)) => Ok(v),
1351            Value::Lambda(_)
1352            | Value::Function { .. }
1353            | Value::Tuple(_)
1354            | Value::Cell(_)
1355            | Value::Struct(_)
1356            | Value::StructArray(_)
1357            | Value::DateTime(_)
1358            | Value::Duration(_) => Err("Transpose is not applicable to this type".to_string()),
1359        },
1360        Expr::StrLiteral(s) => Ok(Value::Str(s.clone())),
1361        Expr::StringObjLiteral(s) => Ok(Value::StringObj(s.clone())),
1362        Expr::Range(start_expr, step_expr, stop_expr) => {
1363            let start = match eval_inner(start_expr, env, io.as_deref_mut())? {
1364                Value::Scalar(n) => n,
1365                _ => return Err("Range bounds must be real scalars".to_string()),
1366            };
1367            let stop = match eval_inner(stop_expr, env, io.as_deref_mut())? {
1368                Value::Scalar(n) => n,
1369                _ => return Err("Range bounds must be real scalars".to_string()),
1370            };
1371            let step = match step_expr {
1372                None => 1.0,
1373                Some(s) => match eval_inner(s, env, io)? {
1374                    Value::Scalar(n) => n,
1375                    _ => return Err("Range step must be a real scalar".to_string()),
1376                },
1377            };
1378            if step == 0.0 {
1379                return Err("Range step cannot be zero".to_string());
1380            }
1381            let n_float = (stop - start) / step;
1382            if n_float < -1e-10 {
1383                // Empty range: step points in the wrong direction
1384                return Ok(Value::Matrix(Array2::zeros((1, 0))));
1385            }
1386            let n = (n_float + 1e-10).floor() as usize + 1;
1387            let vals: Vec<f64> = (0..n).map(|i| start + i as f64 * step).collect();
1388            let m =
1389                Array2::from_shape_vec((1, n), vals).map_err(|e| format!("Range error: {e}"))?;
1390            Ok(Value::Matrix(m))
1391        }
1392    }
1393}
1394
1395fn eval_binop(l: Value, op: &Op, r: Value) -> Result<Value, String> {
1396    match (l, r) {
1397        (Value::Void, _) | (_, Value::Void) => {
1398            Err("Cannot apply operator to void value".to_string())
1399        }
1400        // --- String object operations ---
1401        (Value::StringObj(a), Value::StringObj(b)) => match op {
1402            Op::Add => Ok(Value::StringObj(a + &b)),
1403            Op::Eq => Ok(Value::Scalar(bool_to_f64(a == b))),
1404            Op::NotEq => Ok(Value::Scalar(bool_to_f64(a != b))),
1405            _ => Err("Operator not supported on string objects".to_string()),
1406        },
1407        // Char array: convert to numeric, re-dispatch
1408        (Value::Str(s), r) => eval_binop(str_to_numeric(&s), op, r),
1409        (l, Value::Str(s)) => eval_binop(l, op, str_to_numeric(&s)),
1410        // String object mixed with other types: error
1411        (Value::StringObj(_), _) | (_, Value::StringObj(_)) => {
1412            Err("String object cannot be combined with non-string values".to_string())
1413        }
1414        // Functions, tuples, cell arrays, structs, and struct arrays are not numeric
1415        (Value::Lambda(_), _)
1416        | (_, Value::Lambda(_))
1417        | (Value::Function { .. }, _)
1418        | (_, Value::Function { .. })
1419        | (Value::Tuple(_), _)
1420        | (_, Value::Tuple(_))
1421        | (Value::Cell(_), _)
1422        | (_, Value::Cell(_))
1423        | (Value::Struct(_), _)
1424        | (_, Value::Struct(_))
1425        | (Value::StructArray(_), _)
1426        | (_, Value::StructArray(_)) => Err("Cannot apply operator to a struct value".to_string()),
1427        // --- DateTime / Duration arithmetic ---
1428        // datetime + duration → datetime
1429        (Value::DateTime(t), Value::Duration(d)) => match op {
1430            Op::Add => Ok(Value::DateTime(t + d)),
1431            Op::Sub => Ok(Value::DateTime(t - d)),
1432            _ => Err("Unsupported operator between datetime and duration".to_string()),
1433        },
1434        // duration + datetime → datetime (commutative add only)
1435        (Value::Duration(d), Value::DateTime(t)) => match op {
1436            Op::Add => Ok(Value::DateTime(t + d)),
1437            _ => Err("Unsupported operator between duration and datetime".to_string()),
1438        },
1439        // datetime - datetime → duration
1440        (Value::DateTime(t1), Value::DateTime(t2)) => match op {
1441            Op::Sub => Ok(Value::Duration(t1 - t2)),
1442            Op::Eq => Ok(Value::Scalar(bool_to_f64(
1443                (t1 - t2).abs() < 1e-9 || (t1.is_nan() && t2.is_nan()),
1444            ))),
1445            Op::NotEq => Ok(Value::Scalar(bool_to_f64(
1446                (t1 - t2).abs() >= 1e-9 && !(t1.is_nan() && t2.is_nan()),
1447            ))),
1448            Op::Lt => Ok(Value::Scalar(bool_to_f64(t1 < t2))),
1449            Op::Gt => Ok(Value::Scalar(bool_to_f64(t1 > t2))),
1450            Op::LtEq => Ok(Value::Scalar(bool_to_f64(t1 <= t2))),
1451            Op::GtEq => Ok(Value::Scalar(bool_to_f64(t1 >= t2))),
1452            _ => Err("Unsupported operator between two datetimes".to_string()),
1453        },
1454        // duration ± duration → duration; duration */ scalar → duration; duration / duration → scalar
1455        (Value::Duration(d1), Value::Duration(d2)) => match op {
1456            Op::Add => Ok(Value::Duration(d1 + d2)),
1457            Op::Sub => Ok(Value::Duration(d1 - d2)),
1458            Op::Div | Op::ElemDiv => Ok(Value::Scalar(d1 / d2)),
1459            Op::Eq => Ok(Value::Scalar(bool_to_f64((d1 - d2).abs() < 1e-9))),
1460            Op::NotEq => Ok(Value::Scalar(bool_to_f64((d1 - d2).abs() >= 1e-9))),
1461            Op::Lt => Ok(Value::Scalar(bool_to_f64(d1 < d2))),
1462            Op::Gt => Ok(Value::Scalar(bool_to_f64(d1 > d2))),
1463            Op::LtEq => Ok(Value::Scalar(bool_to_f64(d1 <= d2))),
1464            Op::GtEq => Ok(Value::Scalar(bool_to_f64(d1 >= d2))),
1465            _ => Err("Unsupported operator between two durations".to_string()),
1466        },
1467        (Value::Duration(d), Value::Scalar(s)) => match op {
1468            Op::Mul | Op::ElemMul => Ok(Value::Duration(d * s)),
1469            Op::Div | Op::ElemDiv => Ok(Value::Duration(d / s)),
1470            _ => Err("Unsupported operator between duration and scalar".to_string()),
1471        },
1472        (Value::Scalar(s), Value::Duration(d)) => match op {
1473            Op::Mul | Op::ElemMul => Ok(Value::Duration(s * d)),
1474            _ => Err("Unsupported operator between scalar and duration".to_string()),
1475        },
1476        // DateTime/Duration + arrays
1477        (Value::DateTime(t), Value::DurationArray(dv)) => match op {
1478            Op::Add => Ok(Value::DateTimeArray(dv.iter().map(|d| t + d).collect())),
1479            Op::Sub => Ok(Value::DateTimeArray(dv.iter().map(|d| t - d).collect())),
1480            _ => Err("Unsupported operator between datetime and duration array".to_string()),
1481        },
1482        (Value::DurationArray(dv), Value::DateTime(t)) => match op {
1483            Op::Add => Ok(Value::DateTimeArray(dv.iter().map(|d| t + d).collect())),
1484            _ => Err("Unsupported operator between duration array and datetime".to_string()),
1485        },
1486        (Value::DateTimeArray(tv), Value::Duration(d)) => match op {
1487            Op::Add => Ok(Value::DateTimeArray(tv.iter().map(|t| t + d).collect())),
1488            Op::Sub => Ok(Value::DateTimeArray(tv.iter().map(|t| t - d).collect())),
1489            _ => Err("Unsupported operator between datetime array and duration".to_string()),
1490        },
1491        (Value::DateTimeArray(tv), Value::DurationArray(dv)) => match op {
1492            Op::Add if tv.len() == dv.len() => Ok(Value::DateTimeArray(
1493                tv.iter().zip(&dv).map(|(t, d)| t + d).collect(),
1494            )),
1495            Op::Sub if tv.len() == dv.len() => Ok(Value::DateTimeArray(
1496                tv.iter().zip(&dv).map(|(t, d)| t - d).collect(),
1497            )),
1498            _ => Err("Unsupported or mismatched datetime/duration array operation".to_string()),
1499        },
1500        (Value::DateTimeArray(tv1), Value::DateTimeArray(tv2)) => match op {
1501            Op::Sub if tv1.len() == tv2.len() => Ok(Value::DurationArray(
1502                tv1.iter().zip(&tv2).map(|(a, b)| a - b).collect(),
1503            )),
1504            _ => Err("Unsupported operator between two datetime arrays".to_string()),
1505        },
1506        (Value::DurationArray(dv), Value::Scalar(s)) => match op {
1507            Op::Mul | Op::ElemMul => Ok(Value::DurationArray(dv.iter().map(|d| d * s).collect())),
1508            Op::Div | Op::ElemDiv => Ok(Value::DurationArray(dv.iter().map(|d| d / s).collect())),
1509            _ => Err("Unsupported operator between duration array and scalar".to_string()),
1510        },
1511        (Value::Scalar(s), Value::DurationArray(dv)) => match op {
1512            Op::Mul | Op::ElemMul => Ok(Value::DurationArray(dv.iter().map(|d| s * d).collect())),
1513            _ => Err("Unsupported operator between scalar and duration array".to_string()),
1514        },
1515        // Catch-all: DateTime/Duration mixed with unsupported types
1516        (Value::DateTime(_), _)
1517        | (_, Value::DateTime(_))
1518        | (Value::Duration(_), _)
1519        | (_, Value::Duration(_))
1520        | (Value::DateTimeArray(_), _)
1521        | (_, Value::DateTimeArray(_))
1522        | (Value::DurationArray(_), _)
1523        | (_, Value::DurationArray(_)) => {
1524            Err("Unsupported operation on datetime or duration value".to_string())
1525        }
1526        // --- Complex arithmetic ---
1527        (Value::Complex(re1, im1), Value::Complex(re2, im2)) => {
1528            complex_binop(re1, im1, op, re2, im2)
1529        }
1530        (Value::Complex(re, im), Value::Scalar(s)) => complex_binop(re, im, op, s, 0.0),
1531        (Value::Scalar(s), Value::Complex(re, im)) => complex_binop(s, 0.0, op, re, im),
1532        // Complex scalar × real matrix → upcast matrix to ComplexMatrix, broadcast scalar
1533        (Value::Complex(re, im), Value::Matrix(m)) => {
1534            complex_binop_cm(re, im, op, cm_from_real(&m))
1535        }
1536        (Value::Matrix(m), Value::Complex(re, im)) => {
1537            cm_binop_complex(cm_from_real(&m), op, re, im)
1538        }
1539        // ComplexMatrix combinations
1540        (Value::ComplexMatrix(a), Value::ComplexMatrix(b)) => complex_matrix_binop(a, op, b),
1541        (Value::ComplexMatrix(cm), Value::Matrix(m)) => {
1542            complex_matrix_binop(cm, op, cm_from_real(&m))
1543        }
1544        (Value::Matrix(m), Value::ComplexMatrix(cm)) => {
1545            complex_matrix_binop(cm_from_real(&m), op, cm)
1546        }
1547        (Value::ComplexMatrix(cm), Value::Scalar(s)) => cm_binop_scalar(cm, op, s),
1548        (Value::Scalar(s), Value::ComplexMatrix(cm)) => scalar_binop_cm(s, op, cm),
1549        (Value::ComplexMatrix(cm), Value::Complex(re, im)) => cm_binop_complex(cm, op, re, im),
1550        (Value::Complex(re, im), Value::ComplexMatrix(cm)) => complex_binop_cm(re, im, op, cm),
1551        (Value::Scalar(lv), Value::Scalar(rv)) => {
1552            let result = match op {
1553                Op::Add => lv + rv,
1554                Op::Sub => lv - rv,
1555                Op::Mul | Op::ElemMul => lv * rv,
1556                Op::Div | Op::ElemDiv => {
1557                    if rv == 0.0 {
1558                        return Err("Division by zero".to_string());
1559                    }
1560                    lv / rv
1561                }
1562                Op::LDiv => {
1563                    if lv == 0.0 {
1564                        return Err("Left division by zero (a \\ b requires a ≠ 0)".to_string());
1565                    }
1566                    rv / lv
1567                }
1568                Op::Pow | Op::ElemPow => lv.powf(rv),
1569                Op::Eq => bool_to_f64(lv == rv),
1570                Op::NotEq => bool_to_f64(lv != rv),
1571                Op::Lt => bool_to_f64(lv < rv),
1572                Op::Gt => bool_to_f64(lv > rv),
1573                Op::LtEq => bool_to_f64(lv <= rv),
1574                Op::GtEq => bool_to_f64(lv >= rv),
1575                Op::And | Op::ElemAnd => bool_to_f64(lv != 0.0 && rv != 0.0),
1576                Op::Or | Op::ElemOr => bool_to_f64(lv != 0.0 || rv != 0.0),
1577            };
1578            Ok(Value::Scalar(result))
1579        }
1580        (Value::Matrix(lm), Value::Matrix(rm)) => match op {
1581            Op::Add => {
1582                check_same_shape(&lm, &rm)?;
1583                Ok(Value::Matrix(&lm + &rm))
1584            }
1585            Op::Sub => {
1586                check_same_shape(&lm, &rm)?;
1587                Ok(Value::Matrix(&lm - &rm))
1588            }
1589            Op::Mul => {
1590                if lm.ncols() != rm.nrows() {
1591                    return Err(format!(
1592                        "Inner dimensions must agree: {}x{} * {}x{}",
1593                        lm.nrows(),
1594                        lm.ncols(),
1595                        rm.nrows(),
1596                        rm.ncols()
1597                    ));
1598                }
1599                Ok(Value::Matrix(lm.dot(&rm)))
1600            }
1601            Op::ElemMul => {
1602                check_same_shape(&lm, &rm)?;
1603                Ok(Value::Matrix(&lm * &rm))
1604            }
1605            Op::ElemDiv => {
1606                check_same_shape(&lm, &rm)?;
1607                Ok(Value::Matrix(&lm / &rm))
1608            }
1609            Op::ElemPow => {
1610                check_same_shape(&lm, &rm)?;
1611                Ok(Value::Matrix(
1612                    ndarray::Zip::from(&lm)
1613                        .and(&rm)
1614                        .map_collect(|a, b| a.powf(*b)),
1615                ))
1616            }
1617            Op::Eq | Op::NotEq | Op::Lt | Op::Gt | Op::LtEq | Op::GtEq => {
1618                check_same_shape(&lm, &rm)?;
1619                Ok(Value::Matrix(
1620                    ndarray::Zip::from(&lm)
1621                        .and(&rm)
1622                        .map_collect(|a, b| bool_to_f64(cmp_op(op, *a, *b))),
1623                ))
1624            }
1625            Op::And | Op::Or | Op::ElemAnd | Op::ElemOr => {
1626                check_same_shape(&lm, &rm)?;
1627                Ok(Value::Matrix(
1628                    ndarray::Zip::from(&lm)
1629                        .and(&rm)
1630                        .map_collect(|a, b| bool_to_f64(cmp_op(op, *a, *b))),
1631                ))
1632            }
1633            Op::Div => Err("Matrix / Matrix: use inv(B)*A or A*inv(B)".to_string()),
1634            Op::LDiv => Ok(Value::Matrix(solve_linear(&lm, &rm)?)),
1635            Op::Pow => Err("Matrix ^ Matrix: not supported".to_string()),
1636        },
1637        (Value::Scalar(s), Value::Matrix(m)) => match op {
1638            Op::Add => Ok(Value::Matrix(s + &m)),
1639            Op::Sub => Ok(Value::Matrix(m.mapv(|x| s - x))),
1640            Op::Mul | Op::ElemMul => Ok(Value::Matrix(s * &m)),
1641            Op::Div => Err("Scalar / Matrix: not supported".to_string()),
1642            Op::ElemDiv => Err("Scalar ./ Matrix: not supported".to_string()),
1643            Op::LDiv => {
1644                if s == 0.0 {
1645                    return Err("Left division by zero (a \\ B requires a ≠ 0)".to_string());
1646                }
1647                Ok(Value::Matrix(m.mapv(|x| x / s)))
1648            }
1649            Op::Pow | Op::ElemPow => Ok(Value::Matrix(m.mapv(|x| s.powf(x)))),
1650            Op::Eq
1651            | Op::NotEq
1652            | Op::Lt
1653            | Op::Gt
1654            | Op::LtEq
1655            | Op::GtEq
1656            | Op::And
1657            | Op::Or
1658            | Op::ElemAnd
1659            | Op::ElemOr => Ok(Value::Matrix(m.mapv(|x| bool_to_f64(cmp_op(op, s, x))))),
1660        },
1661        (Value::Matrix(m), Value::Scalar(s)) => match op {
1662            Op::Add => Ok(Value::Matrix(&m + s)),
1663            Op::Sub => Ok(Value::Matrix(&m - s)),
1664            Op::Mul | Op::ElemMul => Ok(Value::Matrix(&m * s)),
1665            Op::Div | Op::ElemDiv => Ok(Value::Matrix(m.mapv(|x| x / s))),
1666            Op::LDiv => {
1667                let b = Array2::from_elem((m.nrows(), 1), s);
1668                Ok(Value::Matrix(solve_linear(&m, &b)?))
1669            }
1670            Op::Pow | Op::ElemPow => Ok(Value::Matrix(m.mapv(|x| x.powf(s)))),
1671            Op::Eq
1672            | Op::NotEq
1673            | Op::Lt
1674            | Op::Gt
1675            | Op::LtEq
1676            | Op::GtEq
1677            | Op::And
1678            | Op::Or
1679            | Op::ElemAnd
1680            | Op::ElemOr => Ok(Value::Matrix(m.mapv(|x| bool_to_f64(cmp_op(op, x, s))))),
1681        },
1682    }
1683}
1684
1685#[inline]
1686fn bool_to_f64(b: bool) -> f64 {
1687    if b { 1.0 } else { 0.0 }
1688}
1689
1690/// Applies a comparison or logical op to two scalar values.
1691fn cmp_op(op: &Op, a: f64, b: f64) -> bool {
1692    match op {
1693        Op::Eq => a == b,
1694        Op::NotEq => a != b,
1695        Op::Lt => a < b,
1696        Op::Gt => a > b,
1697        Op::LtEq => a <= b,
1698        Op::GtEq => a >= b,
1699        Op::And | Op::ElemAnd => a != 0.0 && b != 0.0,
1700        Op::Or | Op::ElemOr => a != 0.0 || b != 0.0,
1701        _ => unreachable!(),
1702    }
1703}
1704
1705/// Performs binary operations on two complex numbers `(re1+im1*i) OP (re2+im2*i)`.
1706fn complex_binop(re1: f64, im1: f64, op: &Op, re2: f64, im2: f64) -> Result<Value, String> {
1707    match op {
1708        Op::Add => Ok(make_complex(re1 + re2, im1 + im2)),
1709        Op::Sub => Ok(make_complex(re1 - re2, im1 - im2)),
1710        Op::Mul | Op::ElemMul => {
1711            // (a+bi)(c+di) = (ac-bd) + (ad+bc)i
1712            Ok(make_complex(re1 * re2 - im1 * im2, re1 * im2 + im1 * re2))
1713        }
1714        Op::Div | Op::ElemDiv => {
1715            // (a+bi)/(c+di) = ((ac+bd) + (bc-ad)i) / (c²+d²)
1716            let denom = re2 * re2 + im2 * im2;
1717            if denom == 0.0 {
1718                return Err("Division by zero (complex)".to_string());
1719            }
1720            Ok(make_complex(
1721                (re1 * re2 + im1 * im2) / denom,
1722                (im1 * re2 - re1 * im2) / denom,
1723            ))
1724        }
1725        Op::Pow | Op::ElemPow => {
1726            let r1 = (re1 * re1 + im1 * im1).sqrt();
1727            if r1 == 0.0 {
1728                if re2 > 0.0 {
1729                    return Ok(Value::Scalar(0.0));
1730                }
1731                return Ok(Value::Complex(f64::NAN, f64::NAN));
1732            }
1733            // For integer exponents with zero imaginary part, use repeated multiplication
1734            // to avoid polar-form floating-point error (e.g. i^2 = -1 exactly).
1735            if im2 == 0.0 && re2.fract() == 0.0 && re2.abs() < 1_000_000.0 {
1736                let n = re2 as i64;
1737                if n == 0 {
1738                    return Ok(Value::Scalar(1.0));
1739                }
1740                // positive power: repeated squaring
1741                let abs_n = n.unsigned_abs();
1742                let (mut rr, mut ri) = (1.0_f64, 0.0_f64);
1743                let (mut br, mut bi) = (re1, im1);
1744                let mut exp = abs_n;
1745                while exp > 0 {
1746                    if exp & 1 == 1 {
1747                        let nr = rr * br - ri * bi;
1748                        let ni = rr * bi + ri * br;
1749                        rr = nr;
1750                        ri = ni;
1751                    }
1752                    let nr = br * br - bi * bi;
1753                    let ni = 2.0 * br * bi;
1754                    br = nr;
1755                    bi = ni;
1756                    exp >>= 1;
1757                }
1758                if n < 0 {
1759                    // invert: 1/(rr+ri*i)
1760                    let denom = rr * rr + ri * ri;
1761                    return Ok(make_complex(rr / denom, -ri / denom));
1762                }
1763                return Ok(make_complex(rr, ri));
1764            }
1765            // General case: via polar form exp((c+di) * ln(a+bi))
1766            let theta1 = im1.atan2(re1);
1767            let ln_r1 = r1.ln();
1768            let exp_re = re2 * ln_r1 - im2 * theta1;
1769            let exp_im = im2 * ln_r1 + re2 * theta1;
1770            let mag = exp_re.exp();
1771            Ok(make_complex(mag * exp_im.cos(), mag * exp_im.sin()))
1772        }
1773        Op::Eq => Ok(Value::Scalar(bool_to_f64(re1 == re2 && im1 == im2))),
1774        Op::NotEq => Ok(Value::Scalar(bool_to_f64(re1 != re2 || im1 != im2))),
1775        Op::Lt | Op::Gt | Op::LtEq | Op::GtEq => {
1776            Err("Ordering is not defined for complex numbers".to_string())
1777        }
1778        Op::And | Op::ElemAnd => Ok(Value::Scalar(bool_to_f64(
1779            (re1 != 0.0 || im1 != 0.0) && (re2 != 0.0 || im2 != 0.0),
1780        ))),
1781        Op::Or | Op::ElemOr => Ok(Value::Scalar(bool_to_f64(
1782            re1 != 0.0 || im1 != 0.0 || re2 != 0.0 || im2 != 0.0,
1783        ))),
1784        Op::LDiv => Err("Left division (\\) is not supported for complex numbers".to_string()),
1785    }
1786}
1787
1788/// Constructs a `Value::Complex` or collapses to `Value::Scalar` when `im` is exactly zero.
1789#[inline]
1790fn make_complex(re: f64, im: f64) -> Value {
1791    if im == 0.0 {
1792        Value::Scalar(re)
1793    } else {
1794        Value::Complex(re, im)
1795    }
1796}
1797
1798/// Upcasts a real matrix to a complex matrix by setting all imaginary parts to zero.
1799#[inline]
1800fn cm_from_real(m: &Array2<f64>) -> Array2<Complex<f64>> {
1801    m.mapv(|x| Complex::new(x, 0.0))
1802}
1803
1804/// Performs binary operations between two `Array2<Complex<f64>>` matrices.
1805fn complex_matrix_binop(
1806    a: Array2<Complex<f64>>,
1807    op: &Op,
1808    b: Array2<Complex<f64>>,
1809) -> Result<Value, String> {
1810    let same_shape = || {
1811        if a.shape() != b.shape() {
1812            Err(format!(
1813                "Matrix dimensions must agree: {}×{} vs {}×{}",
1814                a.nrows(),
1815                a.ncols(),
1816                b.nrows(),
1817                b.ncols()
1818            ))
1819        } else {
1820            Ok(())
1821        }
1822    };
1823    match op {
1824        Op::Add => {
1825            same_shape()?;
1826            Ok(Value::ComplexMatrix(a + b))
1827        }
1828        Op::Sub => {
1829            same_shape()?;
1830            Ok(Value::ComplexMatrix(a - b))
1831        }
1832        Op::Mul => {
1833            if a.ncols() != b.nrows() {
1834                return Err(format!(
1835                    "Inner dimensions must agree: {}×{} * {}×{}",
1836                    a.nrows(),
1837                    a.ncols(),
1838                    b.nrows(),
1839                    b.ncols()
1840                ));
1841            }
1842            Ok(Value::ComplexMatrix(a.dot(&b)))
1843        }
1844        Op::ElemMul => {
1845            same_shape()?;
1846            Ok(Value::ComplexMatrix(a * b))
1847        }
1848        Op::ElemDiv => {
1849            same_shape()?;
1850            Ok(Value::ComplexMatrix(a / b))
1851        }
1852        Op::ElemPow => {
1853            same_shape()?;
1854            Ok(Value::ComplexMatrix(
1855                ndarray::Zip::from(&a)
1856                    .and(&b)
1857                    .map_collect(|x, y| x.powc(*y)),
1858            ))
1859        }
1860        Op::Pow => Err(
1861            "ComplexMatrix ^ ComplexMatrix: not supported; use .^ for element-wise power"
1862                .to_string(),
1863        ),
1864        Op::Div | Op::LDiv => {
1865            Err("Complex matrix / and \\ not supported; use inv(A)*B".to_string())
1866        }
1867        Op::Eq => {
1868            same_shape()?;
1869            Ok(Value::Matrix(
1870                ndarray::Zip::from(&a)
1871                    .and(&b)
1872                    .map_collect(|x, y| bool_to_f64(x == y)),
1873            ))
1874        }
1875        Op::NotEq => {
1876            same_shape()?;
1877            Ok(Value::Matrix(
1878                ndarray::Zip::from(&a)
1879                    .and(&b)
1880                    .map_collect(|x, y| bool_to_f64(x != y)),
1881            ))
1882        }
1883        Op::Lt | Op::Gt | Op::LtEq | Op::GtEq => {
1884            Err("Ordering comparison not defined for complex matrices".to_string())
1885        }
1886        Op::And | Op::ElemAnd => {
1887            same_shape()?;
1888            Ok(Value::Matrix(ndarray::Zip::from(&a).and(&b).map_collect(
1889                |x, y| bool_to_f64((x.re != 0.0 || x.im != 0.0) && (y.re != 0.0 || y.im != 0.0)),
1890            )))
1891        }
1892        Op::Or | Op::ElemOr => {
1893            same_shape()?;
1894            Ok(Value::Matrix(ndarray::Zip::from(&a).and(&b).map_collect(
1895                |x, y| bool_to_f64(x.re != 0.0 || x.im != 0.0 || y.re != 0.0 || y.im != 0.0),
1896            )))
1897        }
1898    }
1899}
1900
1901/// Broadcasts a scalar to every element of a complex matrix.
1902fn cm_binop_scalar(cm: Array2<Complex<f64>>, op: &Op, s: f64) -> Result<Value, String> {
1903    let c = Complex::new(s, 0.0);
1904    match op {
1905        Op::Add => Ok(Value::ComplexMatrix(cm.mapv(|x| x + c))),
1906        Op::Sub => Ok(Value::ComplexMatrix(cm.mapv(|x| x - c))),
1907        Op::Mul | Op::ElemMul => Ok(Value::ComplexMatrix(cm.mapv(|x| x * c))),
1908        Op::Div | Op::ElemDiv => Ok(Value::ComplexMatrix(cm.mapv(|x| x / c))),
1909        Op::Pow | Op::ElemPow => Ok(Value::ComplexMatrix(cm.mapv(|x| x.powf(s)))),
1910        Op::Eq => Ok(Value::Matrix(cm.mapv(|x| bool_to_f64(x == c)))),
1911        Op::NotEq => Ok(Value::Matrix(cm.mapv(|x| bool_to_f64(x != c)))),
1912        _ => Err("Unsupported operator between complex matrix and scalar".to_string()),
1913    }
1914}
1915
1916/// Broadcasts a scalar to every element of a complex matrix (scalar on the left).
1917fn scalar_binop_cm(s: f64, op: &Op, cm: Array2<Complex<f64>>) -> Result<Value, String> {
1918    let c = Complex::new(s, 0.0);
1919    match op {
1920        Op::Add => Ok(Value::ComplexMatrix(cm.mapv(|x| c + x))),
1921        Op::Sub => Ok(Value::ComplexMatrix(cm.mapv(|x| c - x))),
1922        Op::Mul | Op::ElemMul => Ok(Value::ComplexMatrix(cm.mapv(|x| c * x))),
1923        Op::Pow | Op::ElemPow => Ok(Value::ComplexMatrix(cm.mapv(|x| c.powc(x)))),
1924        Op::Eq => Ok(Value::Matrix(cm.mapv(|x| bool_to_f64(c == x)))),
1925        Op::NotEq => Ok(Value::Matrix(cm.mapv(|x| bool_to_f64(c != x)))),
1926        _ => Err("Unsupported operator between scalar and complex matrix".to_string()),
1927    }
1928}
1929
1930/// Broadcasts a complex scalar to every element of a complex matrix.
1931fn cm_binop_complex(cm: Array2<Complex<f64>>, op: &Op, re: f64, im: f64) -> Result<Value, String> {
1932    let c = Complex::new(re, im);
1933    match op {
1934        Op::Add => Ok(Value::ComplexMatrix(cm.mapv(|x| x + c))),
1935        Op::Sub => Ok(Value::ComplexMatrix(cm.mapv(|x| x - c))),
1936        Op::Mul | Op::ElemMul => Ok(Value::ComplexMatrix(cm.mapv(|x| x * c))),
1937        Op::Div | Op::ElemDiv => Ok(Value::ComplexMatrix(cm.mapv(|x| x / c))),
1938        Op::Pow | Op::ElemPow => Ok(Value::ComplexMatrix(cm.mapv(|x| x.powc(c)))),
1939        Op::Eq => Ok(Value::Matrix(cm.mapv(|x| bool_to_f64(x == c)))),
1940        Op::NotEq => Ok(Value::Matrix(cm.mapv(|x| bool_to_f64(x != c)))),
1941        _ => Err("Unsupported operator between complex matrix and complex scalar".to_string()),
1942    }
1943}
1944
1945/// Broadcasts a complex scalar (on the left) to every element of a complex matrix.
1946fn complex_binop_cm(re: f64, im: f64, op: &Op, cm: Array2<Complex<f64>>) -> Result<Value, String> {
1947    let c = Complex::new(re, im);
1948    match op {
1949        Op::Add => Ok(Value::ComplexMatrix(cm.mapv(|x| c + x))),
1950        Op::Sub => Ok(Value::ComplexMatrix(cm.mapv(|x| c - x))),
1951        Op::Mul | Op::ElemMul => Ok(Value::ComplexMatrix(cm.mapv(|x| c * x))),
1952        Op::Pow | Op::ElemPow => Ok(Value::ComplexMatrix(cm.mapv(|x| c.powc(x)))),
1953        Op::Eq => Ok(Value::Matrix(cm.mapv(|x| bool_to_f64(c == x)))),
1954        Op::NotEq => Ok(Value::Matrix(cm.mapv(|x| bool_to_f64(c != x)))),
1955        _ => Err("Unsupported operator between complex scalar and complex matrix".to_string()),
1956    }
1957}
1958
1959/// Converts a char array string to its numeric representation.
1960/// Single char → Scalar(code), multi-char → 1×N Matrix, empty → 1×0 Matrix.
1961fn str_to_numeric(s: &str) -> Value {
1962    let codes: Vec<f64> = s.chars().map(|c| c as u32 as f64).collect();
1963    match codes.len() {
1964        0 => Value::Matrix(Array2::zeros((1, 0))),
1965        1 => Value::Scalar(codes[0]),
1966        n => Value::Matrix(Array2::from_shape_vec((1, n), codes).unwrap()),
1967    }
1968}
1969
1970/// Extracts a string slice from a Str or StringObj value.
1971fn string_arg<'a>(v: &'a Value, fname: &str, pos: usize) -> Result<&'a str, String> {
1972    match v {
1973        Value::Str(s) | Value::StringObj(s) => Ok(s.as_str()),
1974        _ => Err(format!(
1975            "Function '{fname}' argument {pos} must be a string"
1976        )),
1977    }
1978}
1979
1980fn check_same_shape(lm: &Array2<f64>, rm: &Array2<f64>) -> Result<(), String> {
1981    if lm.shape() != rm.shape() {
1982        return Err(format!(
1983            "Matrix size mismatch: {}x{} vs {}x{}",
1984            lm.nrows(),
1985            lm.ncols(),
1986            rm.nrows(),
1987            rm.ncols()
1988        ));
1989    }
1990    Ok(())
1991}
1992
1993fn scalar_arg(v: &Value, fname: &str, pos: usize) -> Result<f64, String> {
1994    match v {
1995        Value::Void => Err(format!(
1996            "Function '{fname}' argument {pos} must be a scalar, got void"
1997        )),
1998        Value::Scalar(n) => Ok(*n),
1999        Value::Complex(re, im) if *im == 0.0 => Ok(*re),
2000        Value::Complex(_, _) => Err(format!(
2001            "Function '{fname}' argument {pos} must be real, got a complex number"
2002        )),
2003        Value::Matrix(_) => Err(format!(
2004            "Function '{fname}' argument {pos} must be a scalar, got a matrix"
2005        )),
2006        Value::ComplexMatrix(_) => Err(format!(
2007            "Function '{fname}' argument {pos} must be a scalar, got a complex matrix"
2008        )),
2009        Value::Str(s) if s.chars().count() == 1 => Ok(s.chars().next().unwrap() as u32 as f64),
2010        Value::Str(_) | Value::StringObj(_) => Err(format!(
2011            "Function '{fname}' argument {pos} must be a scalar, got a string"
2012        )),
2013        Value::Lambda(_)
2014        | Value::Function { .. }
2015        | Value::Tuple(_)
2016        | Value::Cell(_)
2017        | Value::Struct(_)
2018        | Value::StructArray(_)
2019        | Value::DateTime(_)
2020        | Value::Duration(_)
2021        | Value::DateTimeArray(_)
2022        | Value::DurationArray(_) => Err(format!(
2023            "Function '{fname}' argument {pos} must be a scalar, got a non-numeric value"
2024        )),
2025    }
2026}
2027
2028/// Applies a scalar function element-wise to a scalar or matrix.
2029/// Parses the first argument of `randi` into an inclusive `[lo, hi]` integer range.
2030///
2031/// Accepts either a scalar `max` (→ `[1, max]`) or a 1×2 / 2×1 vector `[min, max]`.
2032fn randi_range(v: &Value) -> Result<(i64, i64), String> {
2033    match v {
2034        Value::Scalar(n) => {
2035            let hi = *n as i64;
2036            if hi < 1 {
2037                return Err("randi: max must be a positive integer".to_string());
2038            }
2039            Ok((1, hi))
2040        }
2041        Value::Matrix(m) if m.len() == 2 => {
2042            let vals: Vec<f64> = m.iter().copied().collect();
2043            let lo = vals[0] as i64;
2044            let hi = vals[1] as i64;
2045            if lo > hi {
2046                return Err("randi: [min, max] range is empty".to_string());
2047            }
2048            Ok((lo, hi))
2049        }
2050        _ => Err("randi: first argument must be a scalar max or a [min, max] vector".to_string()),
2051    }
2052}
2053
2054// ── Descriptive statistics helpers ───────────────────────────────────────────
2055
2056/// Extracts a flat `Vec<f64>` from a `Scalar` or `Matrix` value.
2057fn numeric_vec(v: &Value, fname: &str) -> Result<Vec<f64>, String> {
2058    match v {
2059        Value::Scalar(n) => Ok(vec![*n]),
2060        Value::Matrix(m) => Ok(m.iter().copied().collect()),
2061        _ => Err(format!("{fname}: argument must be numeric")),
2062    }
2063}
2064
2065/// Computes the variance of a slice.  Returns `NaN` for empty, `0.0` for singletons.
2066/// `population = true` divides by N; `false` divides by N-1.
2067fn stat_var_vec(vals: &[f64], population: bool) -> f64 {
2068    let n = vals.len();
2069    if n == 0 {
2070        return f64::NAN;
2071    }
2072    if n == 1 {
2073        return 0.0;
2074    }
2075    let mean = vals.iter().sum::<f64>() / n as f64;
2076    let ss: f64 = vals.iter().map(|&x| (x - mean).powi(2)).sum();
2077    let denom = if population { n as f64 } else { (n - 1) as f64 };
2078    ss / denom
2079}
2080
2081/// Applies a column-wise statistical closure returning one scalar per column.
2082///
2083/// - Scalar → passes `[n]` to `f`.
2084/// - Vector (1×N or N×1) → scalar.
2085/// - M×N matrix (M>1, N>1) → 1×N row vector.
2086fn apply_stat<F>(v: &Value, mut f: F, fname: &str) -> Result<Value, String>
2087where
2088    F: FnMut(&[f64]) -> f64,
2089{
2090    match v {
2091        Value::Scalar(n) => Ok(Value::Scalar(f(&[*n]))),
2092        Value::Matrix(m) => {
2093            if m.nrows() == 1 || m.ncols() == 1 {
2094                let vals: Vec<f64> = m.iter().copied().collect();
2095                Ok(Value::Scalar(f(&vals)))
2096            } else {
2097                let ncols = m.ncols();
2098                let result: Vec<f64> = (0..ncols)
2099                    .map(|c| {
2100                        let col: Vec<f64> = m.column(c).iter().copied().collect();
2101                        f(&col)
2102                    })
2103                    .collect();
2104                Ok(Value::Matrix(
2105                    Array2::from_shape_vec((1, ncols), result).unwrap(),
2106                ))
2107            }
2108        }
2109        _ => Err(format!("{fname}: argument must be numeric")),
2110    }
2111}
2112
2113/// Computes the p-th percentile (0–100) of a pre-sorted slice via linear interpolation.
2114fn percentile_sorted(sorted: &[f64], p: f64) -> f64 {
2115    let n = sorted.len();
2116    if n == 0 {
2117        return f64::NAN;
2118    }
2119    if n == 1 {
2120        return sorted[0];
2121    }
2122    let p = p.clamp(0.0, 100.0);
2123    // Octave/MATLAB Type 5 (Hazen): r = n*p/100 + 0.5 (1-indexed), clamped to [1,n].
2124    let idx = (p / 100.0 * n as f64 - 0.5).max(0.0).min((n - 1) as f64);
2125    let lo = idx.floor() as usize;
2126    let hi = idx.ceil() as usize;
2127    let frac = idx - lo as f64;
2128    sorted[lo] * (1.0 - frac) + sorted[hi] * frac
2129}
2130
2131fn apply_elem<F: Fn(f64) -> f64>(v: &Value, f: F) -> Result<Value, String> {
2132    match v {
2133        Value::Void => Err("Element-wise function not applicable to void".to_string()),
2134        Value::Scalar(n) => Ok(Value::Scalar(f(*n))),
2135        Value::Matrix(m) => Ok(Value::Matrix(m.mapv(f))),
2136        Value::Complex(re, im) if *im == 0.0 => Ok(Value::Scalar(f(*re))),
2137        Value::Complex(_, _) => {
2138            Err("Element-wise real function not applicable to complex values".to_string())
2139        }
2140        Value::ComplexMatrix(_) => {
2141            Err("Element-wise real function not applicable to complex matrices".to_string())
2142        }
2143        Value::Str(_) | Value::StringObj(_) => {
2144            Err("Element-wise function not applicable to strings".to_string())
2145        }
2146        Value::Lambda(_)
2147        | Value::Function { .. }
2148        | Value::Tuple(_)
2149        | Value::Cell(_)
2150        | Value::Struct(_)
2151        | Value::StructArray(_)
2152        | Value::DateTime(_)
2153        | Value::Duration(_)
2154        | Value::DateTimeArray(_)
2155        | Value::DurationArray(_) => {
2156            Err("Element-wise function not applicable to this type".to_string())
2157        }
2158    }
2159}
2160
2161/// Reduces a scalar or matrix to a scalar (for vectors) or 1×N row vector (for M×N matrices).
2162///
2163/// - Scalar → apply `f` to `[n]`.
2164/// - Vector (1×N or N×1) → apply `f` to all elements, return scalar.
2165/// - M×N matrix (M>1, N>1) → apply `f` column-wise, return 1×N row vector.
2166fn apply_reduction<F>(v: &Value, f: F) -> Result<Value, String>
2167where
2168    F: Fn(&[f64]) -> f64,
2169{
2170    match v {
2171        Value::Void => Err("Reduction not applicable to void".to_string()),
2172        Value::Scalar(n) => Ok(Value::Scalar(f(&[*n]))),
2173        Value::Complex(_, _) => Err("Reduction not applicable to complex values".to_string()),
2174        Value::ComplexMatrix(_) => Err("Reduction not applicable to complex matrices".to_string()),
2175        Value::Str(_) | Value::StringObj(_) => {
2176            Err("Reduction not applicable to strings".to_string())
2177        }
2178        Value::Lambda(_)
2179        | Value::Function { .. }
2180        | Value::Tuple(_)
2181        | Value::Cell(_)
2182        | Value::Struct(_)
2183        | Value::StructArray(_)
2184        | Value::DateTime(_)
2185        | Value::Duration(_)
2186        | Value::DateTimeArray(_)
2187        | Value::DurationArray(_) => Err("Reduction not applicable to this type".to_string()),
2188        Value::Matrix(m) => {
2189            if m.nrows() == 1 || m.ncols() == 1 {
2190                let vals: Vec<f64> = m.iter().copied().collect();
2191                Ok(Value::Scalar(f(&vals)))
2192            } else {
2193                let ncols = m.ncols();
2194                let result: Vec<f64> = (0..ncols)
2195                    .map(|c| {
2196                        let col: Vec<f64> = m.column(c).iter().copied().collect();
2197                        f(&col)
2198                    })
2199                    .collect();
2200                Ok(Value::Matrix(
2201                    Array2::from_shape_vec((1, ncols), result).unwrap(),
2202                ))
2203            }
2204        }
2205    }
2206}
2207
2208/// Column-wise reduction over a `ComplexMatrix` (or any numeric value treated as complex).
2209///
2210/// For vectors (1×N or N×1) returns a `Complex`/`Scalar`; for M×N matrices returns a
2211/// `ComplexMatrix` 1×N row vector.
2212fn apply_cm_reduction<F>(v: &Value, f: F) -> Result<Value, String>
2213where
2214    F: Fn(&[Complex<f64>]) -> Complex<f64>,
2215{
2216    let make_scalar = |c: Complex<f64>| -> Value {
2217        if c.im == 0.0 {
2218            Value::Scalar(c.re)
2219        } else {
2220            Value::Complex(c.re, c.im)
2221        }
2222    };
2223    match v {
2224        Value::Scalar(n) => Ok(make_scalar(f(&[Complex::new(*n, 0.0)]))),
2225        Value::Complex(re, im) => Ok(make_scalar(f(&[Complex::new(*re, *im)]))),
2226        Value::Matrix(m) => {
2227            if m.nrows() == 1 || m.ncols() == 1 {
2228                let vals: Vec<Complex<f64>> = m.iter().map(|&x| Complex::new(x, 0.0)).collect();
2229                Ok(make_scalar(f(&vals)))
2230            } else {
2231                let ncols = m.ncols();
2232                let result: Vec<Complex<f64>> = (0..ncols)
2233                    .map(|c| {
2234                        let col: Vec<Complex<f64>> =
2235                            m.column(c).iter().map(|&x| Complex::new(x, 0.0)).collect();
2236                        f(&col)
2237                    })
2238                    .collect();
2239                if result.iter().all(|c| c.im == 0.0) {
2240                    let reals: Vec<f64> = result.iter().map(|c| c.re).collect();
2241                    Ok(Value::Matrix(
2242                        Array2::from_shape_vec((1, ncols), reals).unwrap(),
2243                    ))
2244                } else {
2245                    Ok(Value::ComplexMatrix(
2246                        Array2::from_shape_vec((1, ncols), result).unwrap(),
2247                    ))
2248                }
2249            }
2250        }
2251        Value::ComplexMatrix(m) => {
2252            if m.nrows() == 1 || m.ncols() == 1 {
2253                let vals: Vec<Complex<f64>> = m.iter().copied().collect();
2254                Ok(make_scalar(f(&vals)))
2255            } else {
2256                let ncols = m.ncols();
2257                let result: Vec<Complex<f64>> = (0..ncols)
2258                    .map(|c| {
2259                        let col: Vec<Complex<f64>> = m.column(c).iter().copied().collect();
2260                        f(&col)
2261                    })
2262                    .collect();
2263                if result.iter().all(|c| c.im == 0.0) {
2264                    let reals: Vec<f64> = result.iter().map(|c| c.re).collect();
2265                    Ok(Value::Matrix(
2266                        Array2::from_shape_vec((1, ncols), reals).unwrap(),
2267                    ))
2268                } else {
2269                    Ok(Value::ComplexMatrix(
2270                        Array2::from_shape_vec((1, ncols), result).unwrap(),
2271                    ))
2272                }
2273            }
2274        }
2275        _ => Err("Reduction not applicable to this type".to_string()),
2276    }
2277}
2278
2279/// Computes a cumulative scan (cumsum / cumprod) along a vector or column-wise on a matrix.
2280///
2281/// `combine(accumulator, element) -> new_accumulator` — e.g. `|a, x| a + x` for cumsum.
2282fn apply_cumulative<F>(v: &Value, combine: F) -> Result<Value, String>
2283where
2284    F: Fn(f64, f64) -> f64,
2285{
2286    match v {
2287        Value::Void => Err("Cumulative reduction not applicable to void".to_string()),
2288        Value::Scalar(n) => Ok(Value::Scalar(*n)),
2289        Value::Complex(_, _) => {
2290            Err("Cumulative reduction not applicable to complex values".to_string())
2291        }
2292        Value::ComplexMatrix(_) => {
2293            Err("Cumulative reduction not applicable to complex matrices".to_string())
2294        }
2295        Value::Str(_) | Value::StringObj(_) => {
2296            Err("Cumulative reduction not applicable to strings".to_string())
2297        }
2298        Value::Lambda(_)
2299        | Value::Function { .. }
2300        | Value::Tuple(_)
2301        | Value::Cell(_)
2302        | Value::Struct(_)
2303        | Value::StructArray(_)
2304        | Value::DateTime(_)
2305        | Value::Duration(_)
2306        | Value::DateTimeArray(_)
2307        | Value::DurationArray(_) => {
2308            Err("Cumulative reduction not applicable to this type".to_string())
2309        }
2310        Value::Matrix(m) => {
2311            let initial = combine(0.0, 0.0); // detect identity: 0+0=0 or 0*0=0
2312            // Use 0.0 as additive identity, 1.0 as multiplicative identity.
2313            // We detect the identity from f(1.0, 1.0) vs f(0.0, 0.0).
2314            let identity = if (combine(1.0, 1.0) - 1.0).abs() < 1e-15 && initial == 0.0 {
2315                1.0 // product
2316            } else {
2317                0.0 // sum
2318            };
2319            let (nrows, ncols) = (m.nrows(), m.ncols());
2320            let mut result = m.clone();
2321            if nrows == 1 || ncols == 1 {
2322                // Vector: scan along all elements in order
2323                let mut acc = identity;
2324                for v in result.iter_mut() {
2325                    acc = combine(acc, *v);
2326                    *v = acc;
2327                }
2328            } else {
2329                // Matrix: scan each column independently
2330                for c in 0..ncols {
2331                    let mut acc = identity;
2332                    for r in 0..nrows {
2333                        acc = combine(acc, result[[r, c]]);
2334                        result[[r, c]] = acc;
2335                    }
2336                }
2337            }
2338            Ok(Value::Matrix(result))
2339        }
2340    }
2341}
2342
2343/// Returns column-major 1-based indices of non-zero elements, up to `max_k`.
2344fn find_nonzero(v: &Value, max_k: usize) -> Result<Value, String> {
2345    match v {
2346        Value::Void => Err("find: not applicable to void".to_string()),
2347        Value::ComplexMatrix(_) => Err("find: not applicable to complex matrices".to_string()),
2348        Value::Str(_) | Value::StringObj(_) => Err("find: not applicable to strings".to_string()),
2349        Value::Lambda(_)
2350        | Value::Function { .. }
2351        | Value::Tuple(_)
2352        | Value::Cell(_)
2353        | Value::Struct(_)
2354        | Value::StructArray(_)
2355        | Value::DateTime(_)
2356        | Value::Duration(_)
2357        | Value::DateTimeArray(_)
2358        | Value::DurationArray(_) => Err("find: not applicable to this type".to_string()),
2359        Value::Complex(re, im) => {
2360            if (*re != 0.0 || *im != 0.0) && max_k >= 1 {
2361                Ok(Value::Matrix(
2362                    Array2::from_shape_vec((1, 1), vec![1.0]).unwrap(),
2363                ))
2364            } else {
2365                Ok(Value::Matrix(Array2::zeros((1, 0))))
2366            }
2367        }
2368        Value::Scalar(n) => {
2369            if *n != 0.0 && max_k >= 1 {
2370                Ok(Value::Matrix(
2371                    Array2::from_shape_vec((1, 1), vec![1.0]).unwrap(),
2372                ))
2373            } else {
2374                Ok(Value::Matrix(Array2::zeros((1, 0))))
2375            }
2376        }
2377        Value::Matrix(m) => {
2378            let nrows = m.nrows();
2379            let total = m.len();
2380            let mut idxs: Vec<f64> = Vec::new();
2381            for i in 0..total {
2382                if idxs.len() >= max_k {
2383                    break;
2384                }
2385                let row = i % nrows;
2386                let col = i / nrows;
2387                if m[[row, col]] != 0.0 {
2388                    idxs.push((i + 1) as f64);
2389                }
2390            }
2391            let n = idxs.len();
2392            if n == 0 {
2393                Ok(Value::Matrix(Array2::zeros((1, 0))))
2394            } else {
2395                Ok(Value::Matrix(Array2::from_shape_vec((1, n), idxs).unwrap()))
2396            }
2397        }
2398    }
2399}
2400
2401// ---------------------------------------------------------------------------
2402// C-style printf format engine
2403// ---------------------------------------------------------------------------
2404
2405/// Formats `args` using a C-style `fmt` string.
2406///
2407/// Supported specifiers: `%d` `%i` `%f` `%e` `%g` `%s` `%%`.
2408/// Flags: `-` (left-align), `+` (force sign), `0` (zero-pad), ` ` (space sign).
2409/// Width and `.precision` follow standard C `printf` conventions.
2410/// Escape sequences `\n` `\t` `\\` are also processed.
2411///
2412/// Octave behaviour: if `args` is longer than the number of specifiers the
2413/// format string is repeated until all args are consumed.
2414pub fn format_printf(fmt: &str, args: &[Value]) -> Result<String, String> {
2415    let mut result = String::new();
2416    let mut arg_idx = 0;
2417
2418    loop {
2419        let consumed_before = arg_idx;
2420        let mut chars = fmt.chars().peekable();
2421
2422        while let Some(c) = chars.next() {
2423            if c == '\\' {
2424                match chars.next() {
2425                    Some('n') => result.push('\n'),
2426                    Some('t') => result.push('\t'),
2427                    Some('\\') => result.push('\\'),
2428                    Some('\'') => result.push('\''),
2429                    Some('"') => result.push('"'),
2430                    Some(other) => {
2431                        result.push('\\');
2432                        result.push(other);
2433                    }
2434                    None => result.push('\\'),
2435                }
2436                continue;
2437            }
2438
2439            if c != '%' {
2440                result.push(c);
2441                continue;
2442            }
2443
2444            // `%%` → literal `%`
2445            if chars.peek() == Some(&'%') {
2446                chars.next();
2447                result.push('%');
2448                continue;
2449            }
2450
2451            // Parse flags
2452            let mut flag_minus = false;
2453            let mut flag_plus = false;
2454            let mut flag_zero = false;
2455            let mut flag_space = false;
2456            loop {
2457                match chars.peek() {
2458                    Some('-') => {
2459                        flag_minus = true;
2460                        chars.next();
2461                    }
2462                    Some('+') => {
2463                        flag_plus = true;
2464                        chars.next();
2465                    }
2466                    Some('0') => {
2467                        flag_zero = true;
2468                        chars.next();
2469                    }
2470                    Some(' ') => {
2471                        flag_space = true;
2472                        chars.next();
2473                    }
2474                    _ => break,
2475                }
2476            }
2477
2478            // Parse width
2479            let mut width_str = String::new();
2480            while let Some(&d) = chars.peek() {
2481                if d.is_ascii_digit() {
2482                    width_str.push(d);
2483                    chars.next();
2484                } else {
2485                    break;
2486                }
2487            }
2488            let width: usize = width_str.parse().unwrap_or(0);
2489
2490            // Parse precision
2491            let mut precision: Option<usize> = None;
2492            if chars.peek() == Some(&'.') {
2493                chars.next();
2494                let mut p = String::new();
2495                while let Some(&d) = chars.peek() {
2496                    if d.is_ascii_digit() {
2497                        p.push(d);
2498                        chars.next();
2499                    } else {
2500                        break;
2501                    }
2502                }
2503                precision = Some(p.parse().unwrap_or(0));
2504            }
2505
2506            // Specifier character
2507            let spec = match chars.next() {
2508                Some(s) => s,
2509                None => {
2510                    return Err("fprintf: incomplete format specifier at end of string".to_string());
2511                }
2512            };
2513
2514            // No more args — silently skip remaining specifiers
2515            if arg_idx >= args.len() {
2516                continue;
2517            }
2518
2519            let arg = &args[arg_idx];
2520            arg_idx += 1;
2521
2522            let formatted = match spec {
2523                'd' | 'i' => {
2524                    let n = printf_scalar(arg, spec)?;
2525                    let i = n.trunc() as i64;
2526                    let s = printf_sign_str(i >= 0, flag_plus, flag_space, format!("{}", i.abs()));
2527                    printf_pad(s, width, flag_minus, flag_zero)
2528                }
2529                'f' => {
2530                    let n = printf_scalar(arg, spec)?;
2531                    let prec = precision.unwrap_or(6);
2532                    let s = printf_sign_str(
2533                        n >= 0.0,
2534                        flag_plus,
2535                        flag_space,
2536                        format!("{:.prec$}", n.abs(), prec = prec),
2537                    );
2538                    printf_pad(s, width, flag_minus, flag_zero)
2539                }
2540                'e' | 'E' => {
2541                    let n = printf_scalar(arg, spec)?;
2542                    let prec = precision.unwrap_or(6);
2543                    let s = printf_format_sci(n, prec, flag_plus, flag_space, spec == 'E');
2544                    printf_pad(s, width, flag_minus, flag_zero)
2545                }
2546                'g' | 'G' => {
2547                    let n = printf_scalar(arg, spec)?;
2548                    let prec = precision.unwrap_or(6).max(1);
2549                    let s = printf_format_g(n, prec, flag_plus, flag_space, spec == 'G');
2550                    printf_pad(s, width, flag_minus, flag_zero)
2551                }
2552                's' => {
2553                    let s = printf_string(arg)?;
2554                    let s = if let Some(max_len) = precision {
2555                        s.chars().take(max_len).collect::<String>()
2556                    } else {
2557                        s
2558                    };
2559                    printf_pad(s, width, flag_minus, false)
2560                }
2561                other => return Err(format!("fprintf: unknown format specifier '%{other}'")),
2562            };
2563
2564            result.push_str(&formatted);
2565        }
2566
2567        // Stop if all args consumed or no specifiers were found (infinite loop guard)
2568        if arg_idx >= args.len() || arg_idx == consumed_before {
2569            break;
2570        }
2571    }
2572
2573    Ok(result)
2574}
2575
2576/// Extracts a scalar f64 from a Value for use in numeric printf specifiers.
2577fn printf_scalar(v: &Value, spec: char) -> Result<f64, String> {
2578    match v {
2579        Value::Scalar(n) => Ok(*n),
2580        Value::Complex(re, im) if *im == 0.0 => Ok(*re),
2581        Value::Str(s) if s.chars().count() == 1 => Ok(s.chars().next().unwrap() as u32 as f64),
2582        _ => Err(format!(
2583            "fprintf: expected numeric argument for '%{spec}', got {:?}",
2584            std::mem::discriminant(v)
2585        )),
2586    }
2587}
2588
2589/// Extracts a string from a Value for use in `%s`.
2590fn printf_string(v: &Value) -> Result<String, String> {
2591    match v {
2592        Value::Str(s) | Value::StringObj(s) => Ok(s.clone()),
2593        Value::Scalar(n) => Ok(format_number(*n)),
2594        Value::Complex(re, im) => Ok(format_complex(*re, *im, &FormatMode::Custom(6))),
2595        Value::Void => Err("fprintf: cannot format void as string".to_string()),
2596        Value::Matrix(_) => Err("fprintf: cannot format matrix as string".to_string()),
2597        Value::ComplexMatrix(_) => {
2598            Err("fprintf: cannot format complex matrix as string".to_string())
2599        }
2600        Value::DateTime(ts) => Ok(crate::datetime::format_datetime(*ts)),
2601        Value::Duration(s) => Ok(crate::datetime::format_duration(*s)),
2602        Value::Lambda(_)
2603        | Value::Function { .. }
2604        | Value::Tuple(_)
2605        | Value::Cell(_)
2606        | Value::Struct(_)
2607        | Value::StructArray(_)
2608        | Value::DateTimeArray(_)
2609        | Value::DurationArray(_) => Err("fprintf: cannot format this type as string".to_string()),
2610    }
2611}
2612
2613/// Builds a sign-prefixed string: `+n`, ` n`, `-n`, or bare `n`.
2614fn printf_sign_str(positive: bool, flag_plus: bool, flag_space: bool, digits: String) -> String {
2615    if positive {
2616        if flag_plus {
2617            format!("+{digits}")
2618        } else if flag_space {
2619            format!(" {digits}")
2620        } else {
2621            digits
2622        }
2623    } else {
2624        format!("-{digits}")
2625    }
2626}
2627
2628/// Right- or left-pads `s` to at least `width` chars, optionally zero-pads.
2629fn printf_pad(s: String, width: usize, left_align: bool, zero_pad: bool) -> String {
2630    if s.len() >= width {
2631        return s;
2632    }
2633    let pad_len = width - s.len();
2634    if left_align {
2635        format!("{s}{}", " ".repeat(pad_len))
2636    } else if zero_pad {
2637        // Insert zeros after optional sign
2638        let (prefix, rest) = if s.starts_with(['+', '-', ' ']) {
2639            s.split_at(1)
2640        } else {
2641            ("", s.as_str())
2642        };
2643        format!("{prefix}{}{rest}", "0".repeat(pad_len))
2644    } else {
2645        format!("{}{s}", " ".repeat(pad_len))
2646    }
2647}
2648
2649/// Formats `n` in scientific notation matching C `%e` / `%E`.
2650/// Always produces at least 2 exponent digits with an explicit sign: `1.23e+04`.
2651fn printf_format_sci(
2652    n: f64,
2653    prec: usize,
2654    flag_plus: bool,
2655    flag_space: bool,
2656    upper: bool,
2657) -> String {
2658    if n == 0.0 {
2659        let zeros = "0".repeat(prec);
2660        let sep = if prec > 0 {
2661            format!(".{zeros}")
2662        } else {
2663            String::new()
2664        };
2665        let e_char = if upper { 'E' } else { 'e' };
2666        let sign = if flag_plus {
2667            "+"
2668        } else if flag_space {
2669            " "
2670        } else {
2671            ""
2672        };
2673        return format!("{sign}0{sep}{e_char}+00");
2674    }
2675
2676    let neg = n < 0.0;
2677    let abs_n = n.abs();
2678    let exp = abs_n.log10().floor() as i32;
2679    let mantissa = abs_n / 10f64.powi(exp);
2680    let man_str = format!("{:.prec$}", mantissa, prec = prec);
2681
2682    let e_char = if upper { 'E' } else { 'e' };
2683    let exp_sign = if exp >= 0 { '+' } else { '-' };
2684    let exp_abs = exp.unsigned_abs();
2685    let exp_str = if exp_abs < 10 {
2686        format!("{e_char}{exp_sign}0{exp_abs}")
2687    } else {
2688        format!("{e_char}{exp_sign}{exp_abs}")
2689    };
2690
2691    let sign_str = if neg {
2692        "-"
2693    } else if flag_plus {
2694        "+"
2695    } else if flag_space {
2696        " "
2697    } else {
2698        ""
2699    };
2700    format!("{sign_str}{man_str}{exp_str}")
2701}
2702
2703/// Formats `n` using `%g` / `%G` rules:
2704/// uses `%e` if exponent < -4 or >= prec, otherwise `%f`; trims trailing zeros.
2705fn printf_format_g(n: f64, prec: usize, flag_plus: bool, flag_space: bool, upper: bool) -> String {
2706    if n == 0.0 {
2707        let sign = if flag_plus {
2708            "+"
2709        } else if flag_space {
2710            " "
2711        } else {
2712            ""
2713        };
2714        return format!("{sign}0");
2715    }
2716    let abs_n = n.abs();
2717    let exp = abs_n.log10().floor() as i32;
2718    if exp < -4 || exp >= prec as i32 {
2719        let s = printf_format_sci(n, prec.saturating_sub(1), flag_plus, flag_space, upper);
2720        trim_g_sci(s, upper)
2721    } else {
2722        let decimal_places = (prec as i32 - 1 - exp).max(0) as usize;
2723        let neg = n < 0.0;
2724        let s = format!("{:.prec$}", abs_n, prec = decimal_places);
2725        let s = if s.contains('.') {
2726            s.trim_end_matches('0').trim_end_matches('.').to_string()
2727        } else {
2728            s
2729        };
2730        let sign = if neg {
2731            "-"
2732        } else if flag_plus {
2733            "+"
2734        } else if flag_space {
2735            " "
2736        } else {
2737            ""
2738        };
2739        format!("{sign}{s}")
2740    }
2741}
2742
2743/// Trims trailing zeros from the mantissa of a scientific-notation string `1.230e+04` → `1.23e+04`.
2744fn trim_g_sci(s: String, upper: bool) -> String {
2745    let e_char = if upper { 'E' } else { 'e' };
2746    if let Some(e_pos) = s.find(e_char) {
2747        let mantissa = &s[..e_pos];
2748        let exp_part = &s[e_pos..];
2749        let trimmed = if mantissa.contains('.') {
2750            mantissa.trim_end_matches('0').trim_end_matches('.')
2751        } else {
2752            mantissa
2753        };
2754        format!("{trimmed}{exp_part}")
2755    } else {
2756        s
2757    }
2758}
2759
2760/// Calls a `Lambda` or `Function` value with the given arguments.
2761///
2762/// Used by `cellfun` and `arrayfun` to apply a function to each element
2763/// without going through the name-lookup path.
2764fn call_function_value(
2765    f: &Value,
2766    args: &[Value],
2767    io: Option<&mut IoContext>,
2768) -> Result<Value, String> {
2769    match f {
2770        Value::Lambda(lf) => {
2771            let lf = lf.clone();
2772            lf.0(args, io)
2773        }
2774        Value::Function { .. } => {
2775            // Named function called via cellfun/arrayfun — name is unknown at this point.
2776            // Use a minimal env that doesn't export any user variables to avoid
2777            // polluting the caller's scope. Functions see their own scope via exec.
2778            let empty_env = Env::new();
2779            match io {
2780                Some(io_ref) => FN_CALL_HOOK.with(|c| match c.get() {
2781                    Some(hook) => hook("<anonymous>", f, args, &empty_env, io_ref),
2782                    None => Err("User function execution not initialized".to_string()),
2783                }),
2784                None => {
2785                    let mut tmp_io = IoContext::new();
2786                    FN_CALL_HOOK.with(|c| match c.get() {
2787                        Some(hook) => hook("<anonymous>", f, args, &empty_env, &mut tmp_io),
2788                        None => Err("User function execution not initialized".to_string()),
2789                    })
2790                }
2791            }
2792        }
2793        _ => Err("cellfun/arrayfun: first argument must be a function or lambda (@fn)".to_string()),
2794    }
2795}
2796
2797/// Names of all built-in functions recognized by [`call_builtin`].
2798///
2799/// Used for REPL tab completion and "did you mean?" suggestions.
2800pub fn builtin_names() -> &'static [&'static str] {
2801    &[
2802        "abs",
2803        "acos",
2804        "all",
2805        "angle",
2806        "any",
2807        "arrayfun",
2808        "asin",
2809        "assert",
2810        "atan",
2811        "atan2",
2812        "bitand",
2813        "bitnot",
2814        "bitor",
2815        "bitshift",
2816        "bitxor",
2817        "ceil",
2818        "cell",
2819        "cellfun",
2820        "chol",
2821        "complex",
2822        "cond",
2823        "cross",
2824        "conj",
2825        "contains",
2826        "conv",
2827        "cos",
2828        "cov",
2829        "cumprod",
2830        "cumsum",
2831        "datenum",
2832        "datestr",
2833        "datevec",
2834        "datetime",
2835        "day",
2836        "days",
2837        "deconv",
2838        "det",
2839        "diag",
2840        "diff",
2841        "dot",
2842        "disp",
2843        "dlmread",
2844        "dlmwrite",
2845        "eig",
2846        "endsWith",
2847        "erf",
2848        "eval",
2849        "erfc",
2850        "exist",
2851        "exp",
2852        "eye",
2853        "fclose",
2854        "fft",
2855        "fftfreq",
2856        "fftshift",
2857        "fgetl",
2858        "fgets",
2859        "fieldnames",
2860        "ifft",
2861        "ifftshift",
2862        "find",
2863        "fliplr",
2864        "flipud",
2865        "floor",
2866        "fopen",
2867        "fprintf",
2868        "genpath",
2869        "histc",
2870        "hour",
2871        "hours",
2872        "hypot",
2873        "imag",
2874        "ind2sub",
2875        "int2str",
2876        "interp1",
2877        "intersect",
2878        "inv",
2879        "iqr",
2880        "iscell",
2881        "ismember",
2882        "ischar",
2883        "isdatetime",
2884        "isduration",
2885        "isempty",
2886        "isfield",
2887        "isfile",
2888        "isfinite",
2889        "isfolder",
2890        "isinf",
2891        "isnan",
2892        "isnat",
2893        "isreal",
2894        "isstring",
2895        "isstruct",
2896        "jsonencode",
2897        "jsondecode",
2898        "kron",
2899        "kurtosis",
2900        "lasterr",
2901        "length",
2902        "linspace",
2903        "load",
2904        "log",
2905        "log10",
2906        "log2",
2907        "lower",
2908        "lu",
2909        "mat2str",
2910        "max",
2911        "mean",
2912        "median",
2913        "milliseconds",
2914        "min",
2915        "minute",
2916        "minutes",
2917        "mod",
2918        "mode",
2919        "month",
2920        "nan",
2921        "norm",
2922        "normcdf",
2923        "normpdf",
2924        "not",
2925        "null",
2926        "num2str",
2927        "numel",
2928        "ones",
2929        "orth",
2930        "pinv",
2931        "poly",
2932        "polyfit",
2933        "polyval",
2934        "posixtime",
2935        "prctile",
2936        "prod",
2937        "qr",
2938        "rand",
2939        "randi",
2940        "randn",
2941        "rank",
2942        "readmatrix",
2943        "readtable",
2944        "real",
2945        "regexp",
2946        "regexpi",
2947        "regexprep",
2948        "rem",
2949        "repelem",
2950        "repmat",
2951        "reshape",
2952        "rmfield",
2953        "rng",
2954        "roots",
2955        "round",
2956        "setdiff",
2957        "second",
2958        "seconds",
2959        "sign",
2960        "sin",
2961        "size",
2962        "skewness",
2963        "sort",
2964        "sprintf",
2965        "sqrt",
2966        "startsWith",
2967        "std",
2968        "str2double",
2969        "str2num",
2970        "strcmp",
2971        "strcmpi",
2972        "strjoin",
2973        "strrep",
2974        "strsplit",
2975        "strtrim",
2976        "sub2ind",
2977        "sum",
2978        "svd",
2979        "tan",
2980        "tic",
2981        "toc",
2982        "trace",
2983        "tril",
2984        "triu",
2985        "union",
2986        "unique",
2987        "upper",
2988        "var",
2989        "writetable",
2990        "xor",
2991        "year",
2992        "years",
2993        "zeros",
2994        "zscore",
2995    ]
2996}
2997
2998/// Computes the Levenshtein edit distance between two strings.
2999fn levenshtein(a: &str, b: &str) -> usize {
3000    let a: Vec<char> = a.chars().collect();
3001    let b: Vec<char> = b.chars().collect();
3002    let (m, n) = (a.len(), b.len());
3003    let mut row: Vec<usize> = (0..=n).collect();
3004    for i in 1..=m {
3005        let mut prev = row[0];
3006        row[0] = i;
3007        for j in 1..=n {
3008            let next = if a[i - 1] == b[j - 1] {
3009                prev
3010            } else {
3011                1 + prev.min(row[j]).min(row[j - 1])
3012            };
3013            prev = row[j];
3014            row[j] = next;
3015        }
3016    }
3017    row[n]
3018}
3019
3020/// Finds the closest name in `env` keys and built-in names within Levenshtein distance 2.
3021fn suggest_similar(name: &str, env: &Env) -> Option<String> {
3022    const MAX_DIST: usize = 2;
3023    let mut best: Option<(String, usize)> = None;
3024    let mut update = |candidate: &str| {
3025        let d = levenshtein(name, candidate);
3026        if d <= MAX_DIST && best.as_ref().is_none_or(|(_, bd)| d < *bd) {
3027            best = Some((candidate.to_string(), d));
3028        }
3029    };
3030    for key in env.keys() {
3031        update(key);
3032    }
3033    for &bname in builtin_names() {
3034        update(bname);
3035    }
3036    best.map(|(s, _)| s)
3037}
3038
3039/// Checks equality of two values for `assert(a, b[, tol])`.
3040fn assert_values_equal(a: &Value, b: &Value, tol: Option<f64>) -> Result<Value, String> {
3041    match (a, b) {
3042        (Value::Scalar(x), Value::Scalar(y)) => {
3043            let ok = match tol {
3044                None => x == y,
3045                Some(t) => (x - y).abs() <= t,
3046            };
3047            if ok {
3048                Ok(Value::Void)
3049            } else if let Some(t) = tol {
3050                Err(format!(
3051                    "assert: |{x} - {y}| = {} exceeds tolerance {t}",
3052                    (x - y).abs()
3053                ))
3054            } else {
3055                Err(format!("assert: {x} ~= {y}"))
3056            }
3057        }
3058        (Value::Matrix(ma), Value::Matrix(mb)) => {
3059            if ma.shape() != mb.shape() {
3060                return Err(format!(
3061                    "assert: size mismatch [{}×{}] vs [{}×{}]",
3062                    ma.nrows(),
3063                    ma.ncols(),
3064                    mb.nrows(),
3065                    mb.ncols()
3066                ));
3067            }
3068            for (x, y) in ma.iter().zip(mb.iter()) {
3069                let ok = match tol {
3070                    None => x == y,
3071                    Some(t) => (x - y).abs() <= t,
3072                };
3073                if !ok {
3074                    if let Some(t) = tol {
3075                        return Err(format!(
3076                            "assert: difference {} exceeds tolerance {t}",
3077                            (x - y).abs()
3078                        ));
3079                    } else {
3080                        return Err(format!("assert: {x} ~= {y}"));
3081                    }
3082                }
3083            }
3084            Ok(Value::Void)
3085        }
3086        _ => {
3087            if tol.is_some() {
3088                return Err("assert: tolerance requires numeric arguments".to_string());
3089            }
3090            if a == b {
3091                Ok(Value::Void)
3092            } else {
3093                Err("assert: values not equal".to_string())
3094            }
3095        }
3096    }
3097}
3098
3099fn call_builtin(
3100    name: &str,
3101    args: &[Value],
3102    env: &Env,
3103    mut io: Option<&mut IoContext>,
3104) -> Result<Value, String> {
3105    match (name, args.len()) {
3106        // --- 1-argument scalar functions ---
3107        ("sqrt", 1) => apply_elem(&args[0], |x| x.sqrt()),
3108        ("floor", 1) => apply_elem(&args[0], |x| x.floor()),
3109        ("ceil", 1) => apply_elem(&args[0], |x| x.ceil()),
3110        ("round", 1) => apply_elem(&args[0], |x| x.round()),
3111        ("sign", 1) => apply_elem(&args[0], |x| x.signum()),
3112        ("log", 1) => apply_elem(&args[0], |x| x.ln()),
3113        ("log2", 1) => apply_elem(&args[0], |x| x.log2()),
3114        ("log10", 1) => apply_elem(&args[0], |x| x.log10()),
3115        ("exp", 1) => apply_elem(&args[0], |x| x.exp()),
3116        ("sin", 1) => apply_elem(&args[0], |x| x.sin()),
3117        ("cos", 1) => apply_elem(&args[0], |x| x.cos()),
3118        ("tan", 1) => apply_elem(&args[0], |x| x.tan()),
3119        ("asin", 1) => apply_elem(&args[0], |x| x.asin()),
3120        ("acos", 1) => apply_elem(&args[0], |x| x.acos()),
3121        ("atan", 1) => apply_elem(&args[0], |x| x.atan()),
3122        // --- Special functions (erf, normal distribution) ---
3123        ("erf", 1) => apply_elem(&args[0], libm::erf),
3124        ("erfc", 1) => apply_elem(&args[0], libm::erfc),
3125        ("normcdf", 1) => apply_elem(&args[0], |x| {
3126            0.5 * (1.0 + libm::erf(x / std::f64::consts::SQRT_2))
3127        }),
3128        ("normcdf", 3) => {
3129            let mu = scalar_arg(&args[1], name, 2)?;
3130            let s = scalar_arg(&args[2], name, 3)?;
3131            if s <= 0.0 {
3132                return Err("normcdf: sigma must be positive".to_string());
3133            }
3134            apply_elem(&args[0], move |x| {
3135                0.5 * (1.0 + libm::erf((x - mu) / (s * std::f64::consts::SQRT_2)))
3136            })
3137        }
3138        ("normpdf", 1) => apply_elem(&args[0], |x| {
3139            (-0.5 * x * x).exp() / (2.0 * std::f64::consts::PI).sqrt()
3140        }),
3141        ("normpdf", 3) => {
3142            let mu = scalar_arg(&args[1], name, 2)?;
3143            let s = scalar_arg(&args[2], name, 3)?;
3144            if s <= 0.0 {
3145                return Err("normpdf: sigma must be positive".to_string());
3146            }
3147            apply_elem(&args[0], move |x| {
3148                let z = (x - mu) / s;
3149                (-0.5 * z * z).exp() / (s * (2.0 * std::f64::consts::PI).sqrt())
3150            })
3151        }
3152        // --- 2-argument scalar functions ---
3153        ("atan2", 2) => Ok(Value::Scalar(
3154            scalar_arg(&args[0], name, 1)?.atan2(scalar_arg(&args[1], name, 2)?),
3155        )),
3156        ("mod", 2) => {
3157            let a = scalar_arg(&args[0], name, 1)?;
3158            let b = scalar_arg(&args[1], name, 2)?;
3159            Ok(Value::Scalar(a - b * (a / b).floor()))
3160        }
3161        ("rem", 2) => {
3162            let a = scalar_arg(&args[0], name, 1)?;
3163            let b = scalar_arg(&args[1], name, 2)?;
3164            Ok(Value::Scalar(a - b * (a / b).trunc()))
3165        }
3166        ("max", 2) => Ok(Value::Scalar(
3167            scalar_arg(&args[0], name, 1)?.max(scalar_arg(&args[1], name, 2)?),
3168        )),
3169        ("min", 2) => Ok(Value::Scalar(
3170            scalar_arg(&args[0], name, 1)?.min(scalar_arg(&args[1], name, 2)?),
3171        )),
3172        ("hypot", 2) => Ok(Value::Scalar(
3173            scalar_arg(&args[0], name, 1)?.hypot(scalar_arg(&args[1], name, 2)?),
3174        )),
3175        ("log", 2) => Ok(Value::Scalar(
3176            scalar_arg(&args[0], name, 1)?.log(scalar_arg(&args[1], name, 2)?),
3177        )),
3178        // --- Matrix constructors ---
3179        ("zeros", 1) => {
3180            let n = scalar_arg(&args[0], name, 1)? as usize;
3181            Ok(Value::Matrix(Array2::zeros((n, n))))
3182        }
3183        ("zeros", 2) => {
3184            let r = scalar_arg(&args[0], name, 1)? as usize;
3185            let c = scalar_arg(&args[1], name, 2)? as usize;
3186            Ok(Value::Matrix(Array2::zeros((r, c))))
3187        }
3188        ("ones", 1) => {
3189            let n = scalar_arg(&args[0], name, 1)? as usize;
3190            Ok(Value::Matrix(Array2::ones((n, n))))
3191        }
3192        ("ones", 2) => {
3193            let r = scalar_arg(&args[0], name, 1)? as usize;
3194            let c = scalar_arg(&args[1], name, 2)? as usize;
3195            Ok(Value::Matrix(Array2::ones((r, c))))
3196        }
3197        ("eye", 1) => {
3198            let n = scalar_arg(&args[0], name, 1)? as usize;
3199            let mut m = Array2::<f64>::zeros((n, n));
3200            for i in 0..n {
3201                m[[i, i]] = 1.0;
3202            }
3203            Ok(Value::Matrix(m))
3204        }
3205        // --- Matrix properties ---
3206        ("size", 1) => match &args[0] {
3207            Value::Void => Err("size: not applicable to void".to_string()),
3208            Value::Scalar(_) | Value::Complex(_, _) | Value::Struct(_) => Ok(Value::Matrix(
3209                Array2::from_shape_vec((1, 2), vec![1.0, 1.0]).unwrap(),
3210            )),
3211            Value::Matrix(m) => Ok(Value::Matrix(
3212                Array2::from_shape_vec((1, 2), vec![m.nrows() as f64, m.ncols() as f64]).unwrap(),
3213            )),
3214            Value::ComplexMatrix(m) => Ok(Value::Matrix(
3215                Array2::from_shape_vec((1, 2), vec![m.nrows() as f64, m.ncols() as f64]).unwrap(),
3216            )),
3217            Value::Str(s) => Ok(Value::Matrix(
3218                Array2::from_shape_vec((1, 2), vec![1.0, s.chars().count() as f64]).unwrap(),
3219            )),
3220            Value::StringObj(_) => Ok(Value::Matrix(
3221                Array2::from_shape_vec((1, 2), vec![1.0, 1.0]).unwrap(),
3222            )),
3223            Value::Cell(v) => Ok(Value::Matrix(
3224                Array2::from_shape_vec((1, 2), vec![1.0, v.len() as f64]).unwrap(),
3225            )),
3226            Value::StructArray(arr) => Ok(Value::Matrix(
3227                Array2::from_shape_vec((1, 2), vec![1.0, arr.len() as f64]).unwrap(),
3228            )),
3229            Value::Lambda(_)
3230            | Value::Function { .. }
3231            | Value::Tuple(_)
3232            | Value::DateTime(_)
3233            | Value::Duration(_)
3234            | Value::DateTimeArray(_)
3235            | Value::DurationArray(_) => Err("size: not applicable to this type".to_string()),
3236        },
3237        ("size", 2) => {
3238            let dim = scalar_arg(&args[1], name, 2)? as usize;
3239            match &args[0] {
3240                Value::Void => Err("size: not applicable to void".to_string()),
3241                Value::Scalar(_) | Value::Complex(_, _) | Value::Struct(_) => {
3242                    Ok(Value::Scalar(1.0))
3243                }
3244                Value::Matrix(m) => match dim {
3245                    1 => Ok(Value::Scalar(m.nrows() as f64)),
3246                    2 => Ok(Value::Scalar(m.ncols() as f64)),
3247                    _ => Err(format!("size: invalid dimension {dim}, must be 1 or 2")),
3248                },
3249                Value::ComplexMatrix(m) => match dim {
3250                    1 => Ok(Value::Scalar(m.nrows() as f64)),
3251                    2 => Ok(Value::Scalar(m.ncols() as f64)),
3252                    _ => Err(format!("size: invalid dimension {dim}, must be 1 or 2")),
3253                },
3254                Value::Str(s) => match dim {
3255                    1 => Ok(Value::Scalar(1.0)),
3256                    2 => Ok(Value::Scalar(s.chars().count() as f64)),
3257                    _ => Err(format!("size: invalid dimension {dim}")),
3258                },
3259                Value::StringObj(_) => Ok(Value::Scalar(1.0)),
3260                Value::Cell(v) => match dim {
3261                    1 => Ok(Value::Scalar(1.0)),
3262                    2 => Ok(Value::Scalar(v.len() as f64)),
3263                    _ => Err(format!("size: invalid dimension {dim}")),
3264                },
3265                Value::StructArray(arr) => match dim {
3266                    1 => Ok(Value::Scalar(1.0)),
3267                    2 => Ok(Value::Scalar(arr.len() as f64)),
3268                    _ => Err(format!("size: invalid dimension {dim}")),
3269                },
3270                Value::Lambda(_)
3271                | Value::Function { .. }
3272                | Value::Tuple(_)
3273                | Value::DateTime(_)
3274                | Value::Duration(_)
3275                | Value::DateTimeArray(_)
3276                | Value::DurationArray(_) => Err("size: not applicable to this type".to_string()),
3277            }
3278        }
3279        ("length", 1) => match &args[0] {
3280            Value::Void => Err("length: not applicable to void".to_string()),
3281            Value::Scalar(_) | Value::Complex(_, _) | Value::Struct(_) => Ok(Value::Scalar(1.0)),
3282            Value::Matrix(m) => Ok(Value::Scalar(m.nrows().max(m.ncols()) as f64)),
3283            Value::ComplexMatrix(m) => Ok(Value::Scalar(m.nrows().max(m.ncols()) as f64)),
3284            Value::Str(s) => Ok(Value::Scalar(s.chars().count() as f64)),
3285            Value::StringObj(_) => Ok(Value::Scalar(1.0)),
3286            Value::Cell(v) => Ok(Value::Scalar(v.len() as f64)),
3287            Value::StructArray(arr) => Ok(Value::Scalar(arr.len() as f64)),
3288            Value::DateTimeArray(v) | Value::DurationArray(v) => Ok(Value::Scalar(v.len() as f64)),
3289            Value::DateTime(_) | Value::Duration(_) => Ok(Value::Scalar(1.0)),
3290            Value::Lambda(_) | Value::Function { .. } | Value::Tuple(_) => {
3291                Err("length: not applicable to function values".to_string())
3292            }
3293        },
3294        ("numel", 1) => match &args[0] {
3295            Value::Void => Err("numel: not applicable to void".to_string()),
3296            Value::Scalar(_) | Value::Complex(_, _) | Value::Struct(_) => Ok(Value::Scalar(1.0)),
3297            Value::Matrix(m) => Ok(Value::Scalar(m.len() as f64)),
3298            Value::ComplexMatrix(m) => Ok(Value::Scalar(m.len() as f64)),
3299            Value::Str(s) => Ok(Value::Scalar(s.chars().count() as f64)),
3300            Value::StringObj(_) => Ok(Value::Scalar(1.0)),
3301            Value::Cell(v) => Ok(Value::Scalar(v.len() as f64)),
3302            Value::StructArray(arr) => Ok(Value::Scalar(arr.len() as f64)),
3303            Value::DateTimeArray(v) | Value::DurationArray(v) => Ok(Value::Scalar(v.len() as f64)),
3304            Value::DateTime(_) | Value::Duration(_) => Ok(Value::Scalar(1.0)),
3305            Value::Lambda(_) | Value::Function { .. } | Value::Tuple(_) => {
3306                Err("numel: not applicable to function values".to_string())
3307            }
3308        },
3309        ("trace", 1) => match &args[0] {
3310            Value::Void => Err("trace: not applicable to void".to_string()),
3311            Value::Scalar(n) => Ok(Value::Scalar(*n)),
3312            Value::Complex(re, _) => Ok(Value::Scalar(*re)),
3313            Value::Matrix(m) => {
3314                let n = m.nrows().min(m.ncols());
3315                Ok(Value::Scalar((0..n).map(|i| m[[i, i]]).sum()))
3316            }
3317            Value::ComplexMatrix(m) => {
3318                let n = m.nrows().min(m.ncols());
3319                let s: Complex<f64> = (0..n).map(|i| m[[i, i]]).sum();
3320                Ok(if s.im == 0.0 {
3321                    Value::Scalar(s.re)
3322                } else {
3323                    Value::Complex(s.re, s.im)
3324                })
3325            }
3326            Value::Str(_)
3327            | Value::StringObj(_)
3328            | Value::Lambda(_)
3329            | Value::Function { .. }
3330            | Value::Tuple(_)
3331            | Value::Cell(_)
3332            | Value::Struct(_)
3333            | Value::StructArray(_)
3334            | Value::DateTime(_)
3335            | Value::Duration(_)
3336            | Value::DateTimeArray(_)
3337            | Value::DurationArray(_) => {
3338                Err("trace: not applicable to non-numeric values".to_string())
3339            }
3340        },
3341        ("det", 1) => match &args[0] {
3342            Value::Void => Err("det: not applicable to void".to_string()),
3343            Value::Scalar(n) => Ok(Value::Scalar(*n)),
3344            Value::Complex(_, _) => Err("det: not applicable to complex scalars".to_string()),
3345            Value::ComplexMatrix(_) => Err("det: not supported for complex matrices".to_string()),
3346            Value::Matrix(m) => Ok(Value::Scalar(det_matrix(m)?)),
3347            Value::Str(_)
3348            | Value::StringObj(_)
3349            | Value::Lambda(_)
3350            | Value::Function { .. }
3351            | Value::Tuple(_)
3352            | Value::Cell(_)
3353            | Value::Struct(_)
3354            | Value::StructArray(_)
3355            | Value::DateTime(_)
3356            | Value::Duration(_)
3357            | Value::DateTimeArray(_)
3358            | Value::DurationArray(_) => {
3359                Err("det: not applicable to non-numeric values".to_string())
3360            }
3361        },
3362        ("inv", 1) => match &args[0] {
3363            Value::Void => Err("inv: not applicable to void".to_string()),
3364            Value::Scalar(n) => {
3365                if *n == 0.0 {
3366                    Err("inv: singular (zero scalar)".to_string())
3367                } else {
3368                    Ok(Value::Scalar(1.0 / n))
3369                }
3370            }
3371            Value::Complex(re, im) => {
3372                // 1/(a+bi) = (a-bi)/(a²+b²)
3373                let denom = re * re + im * im;
3374                if denom == 0.0 {
3375                    Err("inv: singular (zero complex)".to_string())
3376                } else {
3377                    Ok(make_complex(re / denom, -im / denom))
3378                }
3379            }
3380            Value::Matrix(m) => Ok(Value::Matrix(inv_matrix(m)?)),
3381            Value::ComplexMatrix(_) => Err("inv: not supported for complex matrices".to_string()),
3382            Value::Str(_)
3383            | Value::StringObj(_)
3384            | Value::Lambda(_)
3385            | Value::Function { .. }
3386            | Value::Tuple(_)
3387            | Value::Cell(_)
3388            | Value::Struct(_)
3389            | Value::StructArray(_)
3390            | Value::DateTime(_)
3391            | Value::Duration(_)
3392            | Value::DateTimeArray(_)
3393            | Value::DurationArray(_) => {
3394                Err("inv: not applicable to non-numeric values".to_string())
3395            }
3396        },
3397        // --- Range / linspace ---
3398        ("linspace", 3) => {
3399            let a = scalar_arg(&args[0], name, 1)?;
3400            let b = scalar_arg(&args[1], name, 2)?;
3401            let n = scalar_arg(&args[2], name, 3)? as usize;
3402            if n == 0 {
3403                return Ok(Value::Matrix(Array2::zeros((1, 0))));
3404            }
3405            if n == 1 {
3406                return Ok(Value::Matrix(
3407                    Array2::from_shape_vec((1, 1), vec![b]).unwrap(),
3408                ));
3409            }
3410            let vals: Vec<f64> = (0..n)
3411                .map(|i| a + (b - a) * i as f64 / (n - 1) as f64)
3412                .collect();
3413            Ok(Value::Matrix(Array2::from_shape_vec((1, n), vals).unwrap()))
3414        }
3415        // --- Bitwise functions ---
3416        // All operands are truncated to i64. Results are non-negative integers
3417        // returned as f64.  For bitnot the bit-width defines the mask.
3418        ("bitand", 2) => {
3419            let a = to_bits(scalar_arg(&args[0], name, 1)?, name, 1)?;
3420            let b = to_bits(scalar_arg(&args[1], name, 2)?, name, 2)?;
3421            Ok(Value::Scalar((a & b) as f64))
3422        }
3423        ("bitor", 2) => {
3424            let a = to_bits(scalar_arg(&args[0], name, 1)?, name, 1)?;
3425            let b = to_bits(scalar_arg(&args[1], name, 2)?, name, 2)?;
3426            Ok(Value::Scalar((a | b) as f64))
3427        }
3428        ("bitxor", 2) => {
3429            let a = to_bits(scalar_arg(&args[0], name, 1)?, name, 1)?;
3430            let b = to_bits(scalar_arg(&args[1], name, 2)?, name, 2)?;
3431            Ok(Value::Scalar((a ^ b) as f64))
3432        }
3433        // bitshift(a, n): n > 0 → left shift; n < 0 → logical right shift.
3434        // Shifts of 64 or more return 0.
3435        ("bitshift", 2) => {
3436            let a = to_bits(scalar_arg(&args[0], name, 1)?, name, 1)?;
3437            let n = scalar_arg(&args[1], name, 2)?;
3438            if n.fract() != 0.0 {
3439                return Err("bitshift: shift amount must be an integer".to_string());
3440            }
3441            let n = n as i64;
3442            let result: u64 = if n >= 64 || n <= -64 {
3443                0
3444            } else if n >= 0 {
3445                a.wrapping_shl(n as u32)
3446            } else {
3447                a.wrapping_shr((-n) as u32)
3448            };
3449            Ok(Value::Scalar(result as f64))
3450        }
3451        // bitnot(a)        — NOT within 32-bit window (Octave uint32 default)
3452        // bitnot(a, bits)  — NOT within explicit bit-width window (1–53)
3453        ("bitnot", 1) => {
3454            let a = to_bits(scalar_arg(&args[0], name, 1)?, name, 1)?;
3455            let mask: u64 = 0xFFFF_FFFF;
3456            Ok(Value::Scalar(((a ^ mask) & mask) as f64))
3457        }
3458        ("bitnot", 2) => {
3459            let a = to_bits(scalar_arg(&args[0], name, 1)?, name, 1)?;
3460            let bits = scalar_arg(&args[1], name, 2)?;
3461            if bits.fract() != 0.0 || !(1.0..=53.0).contains(&bits) {
3462                return Err(format!(
3463                    "bitnot: bit-width must be an integer in [1, 53], got {bits}"
3464                ));
3465            }
3466            let mask: u64 = (1u64 << bits as u32) - 1;
3467            Ok(Value::Scalar(((a ^ mask) & mask) as f64))
3468        }
3469        // --- Special constant predicates (element-wise) ---
3470        ("isnan", 1) => apply_elem(&args[0], |x| if x.is_nan() { 1.0 } else { 0.0 }),
3471        ("isinf", 1) => apply_elem(&args[0], |x| if x.is_infinite() { 1.0 } else { 0.0 }),
3472        ("isfinite", 1) => apply_elem(&args[0], |x| if x.is_finite() { 1.0 } else { 0.0 }),
3473        // --- NaN matrix constructors ---
3474        ("nan", 1) => {
3475            let n = scalar_arg(&args[0], name, 1)? as usize;
3476            Ok(Value::Matrix(Array2::from_elem((n, n), f64::NAN)))
3477        }
3478        ("nan", 2) => {
3479            let r = scalar_arg(&args[0], name, 1)? as usize;
3480            let c = scalar_arg(&args[1], name, 2)? as usize;
3481            Ok(Value::Matrix(Array2::from_elem((r, c), f64::NAN)))
3482        }
3483        // --- Random number generation ---
3484        ("rand", 0) => Ok(Value::Scalar(rand_uniform())),
3485        ("rand", 1) => {
3486            let n = scalar_arg(&args[0], name, 1)? as usize;
3487            let data: Vec<f64> = (0..n * n).map(|_| rand_uniform()).collect();
3488            Ok(Value::Matrix(Array2::from_shape_vec((n, n), data).unwrap()))
3489        }
3490        ("rand", 2) => {
3491            let r = scalar_arg(&args[0], name, 1)? as usize;
3492            let c = scalar_arg(&args[1], name, 2)? as usize;
3493            let data: Vec<f64> = (0..r * c).map(|_| rand_uniform()).collect();
3494            Ok(Value::Matrix(Array2::from_shape_vec((r, c), data).unwrap()))
3495        }
3496        ("randn", 0) => Ok(Value::Scalar(rand_normal())),
3497        ("randn", 1) => {
3498            let n = scalar_arg(&args[0], name, 1)? as usize;
3499            let data: Vec<f64> = (0..n * n).map(|_| rand_normal()).collect();
3500            Ok(Value::Matrix(Array2::from_shape_vec((n, n), data).unwrap()))
3501        }
3502        ("randn", 2) => {
3503            let r = scalar_arg(&args[0], name, 1)? as usize;
3504            let c = scalar_arg(&args[1], name, 2)? as usize;
3505            let data: Vec<f64> = (0..r * c).map(|_| rand_normal()).collect();
3506            Ok(Value::Matrix(Array2::from_shape_vec((r, c), data).unwrap()))
3507        }
3508        ("randi", 1) => {
3509            let (lo, hi) = randi_range(&args[0])?;
3510            let v = RNG.with(|r| r.borrow_mut().gen_range(lo..=hi)) as f64;
3511            Ok(Value::Scalar(v))
3512        }
3513        ("randi", 2) => {
3514            let (lo, hi) = randi_range(&args[0])?;
3515            let n = scalar_arg(&args[1], name, 2)? as usize;
3516            let data: Vec<f64> = (0..n * n)
3517                .map(|_| RNG.with(|r| r.borrow_mut().gen_range(lo..=hi)) as f64)
3518                .collect();
3519            Ok(Value::Matrix(Array2::from_shape_vec((n, n), data).unwrap()))
3520        }
3521        ("randi", 3) => {
3522            let (lo, hi) = randi_range(&args[0])?;
3523            let r = scalar_arg(&args[1], name, 2)? as usize;
3524            let c = scalar_arg(&args[2], name, 3)? as usize;
3525            let data: Vec<f64> = (0..r * c)
3526                .map(|_| RNG.with(|rng| rng.borrow_mut().gen_range(lo..=hi)) as f64)
3527                .collect();
3528            Ok(Value::Matrix(Array2::from_shape_vec((r, c), data).unwrap()))
3529        }
3530        ("rng", 1) => match &args[0] {
3531            Value::Scalar(n) => {
3532                rng_seed(*n as u64);
3533                Ok(Value::Void)
3534            }
3535            Value::Str(s) | Value::StringObj(s) if s == "shuffle" => {
3536                rng_shuffle();
3537                Ok(Value::Void)
3538            }
3539            _ => Err("rng: argument must be a numeric seed or 'shuffle'".to_string()),
3540        },
3541        // --- Vector reductions ---
3542        // For vectors (1×N or N×1): reduce all elements to scalar.
3543        // For M×N matrices (M>1, N>1): reduce column-wise, return 1×N row vector.
3544        ("sum", 1) => {
3545            if matches!(&args[0], Value::Complex(_, _) | Value::ComplexMatrix(_)) {
3546                apply_cm_reduction(&args[0], |v| v.iter().copied().sum())
3547            } else {
3548                apply_reduction(&args[0], |v| v.iter().copied().sum())
3549            }
3550        }
3551        ("prod", 1) => {
3552            if matches!(&args[0], Value::Complex(_, _) | Value::ComplexMatrix(_)) {
3553                apply_cm_reduction(&args[0], |v| v.iter().copied().product())
3554            } else {
3555                apply_reduction(&args[0], |v| v.iter().copied().product())
3556            }
3557        }
3558        ("any", 1) => apply_reduction(&args[0], |v| {
3559            if v.iter().any(|&x| x != 0.0) {
3560                1.0
3561            } else {
3562                0.0
3563            }
3564        }),
3565        ("all", 1) => apply_reduction(&args[0], |v| {
3566            if v.iter().all(|&x| x != 0.0) {
3567                1.0
3568            } else {
3569                0.0
3570            }
3571        }),
3572        ("mean", 1) => {
3573            if matches!(&args[0], Value::Complex(_, _) | Value::ComplexMatrix(_)) {
3574                apply_cm_reduction(&args[0], |v| {
3575                    if v.is_empty() {
3576                        Complex::new(f64::NAN, 0.0)
3577                    } else {
3578                        v.iter().copied().sum::<Complex<f64>>() / v.len() as f64
3579                    }
3580                })
3581            } else {
3582                apply_reduction(&args[0], |v| {
3583                    if v.is_empty() {
3584                        f64::NAN
3585                    } else {
3586                        v.iter().copied().sum::<f64>() / v.len() as f64
3587                    }
3588                })
3589            }
3590        }
3591        // 1-arg min/max: reduce to scalar for vectors, column-wise for matrices.
3592        // 2-arg forms (element-wise scalar min/max) are already handled above.
3593        ("min", 1) => apply_reduction(&args[0], |v| {
3594            v.iter().copied().fold(f64::INFINITY, f64::min)
3595        }),
3596        ("max", 1) => apply_reduction(&args[0], |v| {
3597            v.iter().copied().fold(f64::NEG_INFINITY, f64::max)
3598        }),
3599        // --- Norms ---
3600        ("norm", 1) => match &args[0] {
3601            Value::Void => Err("norm: not applicable to void".to_string()),
3602            Value::Scalar(n) => Ok(Value::Scalar(n.abs())),
3603            Value::Complex(re, im) => Ok(Value::Scalar((re * re + im * im).sqrt())),
3604            Value::Matrix(m) => {
3605                if m.nrows() <= 1 || m.ncols() <= 1 {
3606                    // Vector: L2 norm.
3607                    Ok(Value::Scalar(m.iter().map(|x| x * x).sum::<f64>().sqrt()))
3608                } else {
3609                    // Matrix: 2-norm = largest singular value.
3610                    let (_, s, _) = svd_compute(m)?;
3611                    Ok(Value::Scalar(s.first().copied().unwrap_or(0.0)))
3612                }
3613            }
3614            Value::ComplexMatrix(m) => Ok(Value::Scalar(
3615                m.iter().map(|c| c.norm_sqr()).sum::<f64>().sqrt(),
3616            )),
3617            Value::Str(_)
3618            | Value::StringObj(_)
3619            | Value::Lambda(_)
3620            | Value::Function { .. }
3621            | Value::Tuple(_)
3622            | Value::Cell(_)
3623            | Value::Struct(_)
3624            | Value::StructArray(_)
3625            | Value::DateTime(_)
3626            | Value::Duration(_)
3627            | Value::DateTimeArray(_)
3628            | Value::DurationArray(_) => {
3629                Err("norm: not applicable to non-numeric values".to_string())
3630            }
3631        },
3632        ("norm", 2) => match &args[1] {
3633            Value::Str(s) | Value::StringObj(s) => match s.as_str() {
3634                "fro" => match &args[0] {
3635                    Value::Scalar(n) => Ok(Value::Scalar(n.abs())),
3636                    Value::Matrix(m) => {
3637                        Ok(Value::Scalar(m.iter().map(|x| x * x).sum::<f64>().sqrt()))
3638                    }
3639                    _ => Err("norm: first argument must be numeric".to_string()),
3640                },
3641                other => Err(format!("norm: unknown norm type '{other}'")),
3642            },
3643            _ => {
3644                let p = scalar_arg(&args[1], name, 2)?;
3645                match &args[0] {
3646                    Value::Void => Err("norm: not applicable to void".to_string()),
3647                    Value::Scalar(n) => Ok(Value::Scalar(n.abs())),
3648                    Value::Complex(re, im) => Ok(Value::Scalar((re * re + im * im).sqrt().powf(p))),
3649                    Value::Matrix(m) => {
3650                        if m.nrows() > 1 && m.ncols() > 1 {
3651                            // Matrix norms.
3652                            if (p - 2.0).abs() < 1e-15 {
3653                                let (_, s, _) = svd_compute(m)?;
3654                                return Ok(Value::Scalar(s.first().copied().unwrap_or(0.0)));
3655                            } else if (p - 1.0).abs() < 1e-15 {
3656                                // Maximum absolute column sum.
3657                                let v = (0..m.ncols())
3658                                    .map(|j| m.column(j).iter().map(|&x| x.abs()).sum::<f64>())
3659                                    .fold(0.0_f64, f64::max);
3660                                return Ok(Value::Scalar(v));
3661                            } else if p == f64::INFINITY {
3662                                // Maximum absolute row sum.
3663                                let v = (0..m.nrows())
3664                                    .map(|i| m.row(i).iter().map(|&x| x.abs()).sum::<f64>())
3665                                    .fold(0.0_f64, f64::max);
3666                                return Ok(Value::Scalar(v));
3667                            }
3668                        }
3669                        // Vector (or general Lp).
3670                        if p == f64::INFINITY {
3671                            Ok(Value::Scalar(
3672                                m.iter().copied().fold(0.0_f64, |acc, x| acc.max(x.abs())),
3673                            ))
3674                        } else {
3675                            Ok(Value::Scalar(
3676                                m.iter().map(|x| x.abs().powf(p)).sum::<f64>().powf(1.0 / p),
3677                            ))
3678                        }
3679                    }
3680                    Value::ComplexMatrix(m) => Ok(Value::Scalar(
3681                        m.iter().map(|c| c.norm_sqr()).sum::<f64>().sqrt().powf(p),
3682                    )),
3683                    Value::Str(_)
3684                    | Value::StringObj(_)
3685                    | Value::Lambda(_)
3686                    | Value::Function { .. }
3687                    | Value::Tuple(_)
3688                    | Value::Cell(_)
3689                    | Value::Struct(_)
3690                    | Value::StructArray(_)
3691                    | Value::DateTime(_)
3692                    | Value::Duration(_)
3693                    | Value::DateTimeArray(_)
3694                    | Value::DurationArray(_) => {
3695                        Err("norm: not applicable to non-numeric values".to_string())
3696                    }
3697                }
3698            }
3699        },
3700        // --- Cumulative reductions ---
3701        ("cumsum", 1) => apply_cumulative(&args[0], |acc, x| acc + x),
3702        ("cumprod", 1) => apply_cumulative(&args[0], |acc, x| acc * x),
3703        // --- Sort ---
3704        ("sort", 1) => match &args[0] {
3705            Value::Void => Err("sort: not applicable to void".to_string()),
3706            Value::Scalar(n) => Ok(Value::Scalar(*n)),
3707            Value::Complex(_, _) => Err("sort: not applicable to complex values".to_string()),
3708            Value::ComplexMatrix(_) => Err("sort: not applicable to complex values".to_string()),
3709            Value::Str(_)
3710            | Value::StringObj(_)
3711            | Value::Lambda(_)
3712            | Value::Function { .. }
3713            | Value::Tuple(_)
3714            | Value::Cell(_)
3715            | Value::Struct(_)
3716            | Value::StructArray(_)
3717            | Value::DateTime(_)
3718            | Value::Duration(_)
3719            | Value::DateTimeArray(_)
3720            | Value::DurationArray(_) => {
3721                Err("sort: not applicable to non-numeric values".to_string())
3722            }
3723            Value::Matrix(m) => {
3724                if m.nrows() > 1 && m.ncols() > 1 {
3725                    return Err("sort: input must be a vector".to_string());
3726                }
3727                let mut vals: Vec<f64> = m.iter().copied().collect();
3728                vals.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
3729                Ok(Value::Matrix(
3730                    Array2::from_shape_vec(m.raw_dim(), vals).unwrap(),
3731                ))
3732            }
3733        },
3734        // --- Reshape ---
3735        ("reshape", 3) => {
3736            let r = scalar_arg(&args[1], name, 2)? as usize;
3737            let c = scalar_arg(&args[2], name, 3)? as usize;
3738            match &args[0] {
3739                Value::Void => Err("reshape: not applicable to void".to_string()),
3740                Value::Scalar(n) => {
3741                    if r * c != 1 {
3742                        return Err(format!("reshape: cannot reshape 1 element into {r}x{c}"));
3743                    }
3744                    Ok(Value::Matrix(
3745                        Array2::from_shape_vec((1, 1), vec![*n]).unwrap(),
3746                    ))
3747                }
3748                Value::Complex(_, _) => {
3749                    Err("reshape: not applicable to complex values".to_string())
3750                }
3751                Value::ComplexMatrix(_) => {
3752                    Err("reshape: not supported for complex matrices".to_string())
3753                }
3754                Value::Str(_)
3755                | Value::StringObj(_)
3756                | Value::Lambda(_)
3757                | Value::Function { .. }
3758                | Value::Tuple(_)
3759                | Value::Cell(_)
3760                | Value::Struct(_)
3761                | Value::StructArray(_)
3762                | Value::DateTime(_)
3763                | Value::Duration(_)
3764                | Value::DateTimeArray(_)
3765                | Value::DurationArray(_) => {
3766                    Err("reshape: not applicable to non-numeric values".to_string())
3767                }
3768                Value::Matrix(m) => {
3769                    let total = m.len();
3770                    if r * c != total {
3771                        return Err(format!(
3772                            "reshape: cannot reshape {total} elements into {r}x{c}"
3773                        ));
3774                    }
3775                    // Column-major order (MATLAB convention)
3776                    let flat: Vec<f64> = (0..m.ncols())
3777                        .flat_map(|col| (0..m.nrows()).map(move |row| m[[row, col]]))
3778                        .collect();
3779                    let mut result = Array2::<f64>::zeros((r, c));
3780                    for (i, &v) in flat.iter().enumerate() {
3781                        result[[i % r, i / r]] = v;
3782                    }
3783                    Ok(Value::Matrix(result))
3784                }
3785            }
3786        }
3787        // --- Flip ---
3788        ("fliplr", 1) => match &args[0] {
3789            Value::Void => Err(format!("{name}: not applicable to void")),
3790            Value::Scalar(n) => Ok(Value::Scalar(*n)),
3791            Value::Complex(re, im) => Ok(Value::Complex(*re, *im)),
3792            Value::ComplexMatrix(_) => Err(format!("{name}: not supported for complex matrices")),
3793            Value::Str(_)
3794            | Value::StringObj(_)
3795            | Value::Lambda(_)
3796            | Value::Function { .. }
3797            | Value::Tuple(_)
3798            | Value::Cell(_)
3799            | Value::Struct(_)
3800            | Value::StructArray(_)
3801            | Value::DateTime(_)
3802            | Value::Duration(_)
3803            | Value::DateTimeArray(_)
3804            | Value::DurationArray(_) => {
3805                Err(format!("{name}: not applicable to non-numeric values"))
3806            }
3807            Value::Matrix(m) => {
3808                let (nrows, ncols) = (m.nrows(), m.ncols());
3809                let mut result = m.clone();
3810                for r in 0..nrows {
3811                    for c in 0..ncols / 2 {
3812                        let tmp = result[[r, c]];
3813                        result[[r, c]] = result[[r, ncols - 1 - c]];
3814                        result[[r, ncols - 1 - c]] = tmp;
3815                    }
3816                }
3817                Ok(Value::Matrix(result))
3818            }
3819        },
3820        ("flipud", 1) => match &args[0] {
3821            Value::Void => Err(format!("{name}: not applicable to void")),
3822            Value::Scalar(n) => Ok(Value::Scalar(*n)),
3823            Value::Complex(re, im) => Ok(Value::Complex(*re, *im)),
3824            Value::ComplexMatrix(_) => Err(format!("{name}: not supported for complex matrices")),
3825            Value::Str(_)
3826            | Value::StringObj(_)
3827            | Value::Lambda(_)
3828            | Value::Function { .. }
3829            | Value::Tuple(_)
3830            | Value::Cell(_)
3831            | Value::Struct(_)
3832            | Value::StructArray(_)
3833            | Value::DateTime(_)
3834            | Value::Duration(_)
3835            | Value::DateTimeArray(_)
3836            | Value::DurationArray(_) => {
3837                Err(format!("{name}: not applicable to non-numeric values"))
3838            }
3839            Value::Matrix(m) => {
3840                let (nrows, ncols) = (m.nrows(), m.ncols());
3841                let mut result = m.clone();
3842                for c in 0..ncols {
3843                    for r in 0..nrows / 2 {
3844                        let tmp = result[[r, c]];
3845                        result[[r, c]] = result[[nrows - 1 - r, c]];
3846                        result[[nrows - 1 - r, c]] = tmp;
3847                    }
3848                }
3849                Ok(Value::Matrix(result))
3850            }
3851        },
3852        // --- Find ---
3853        ("find", 1) => find_nonzero(&args[0], usize::MAX),
3854        ("find", 2) => {
3855            let k = scalar_arg(&args[1], name, 2)?;
3856            if k < 0.0 {
3857                return Err("find: k must be non-negative".to_string());
3858            }
3859            find_nonzero(&args[0], k as usize)
3860        }
3861        // --- Unique ---
3862        ("unique", 1) => match &args[0] {
3863            Value::Void => Err("unique: not applicable to void".to_string()),
3864            Value::Scalar(n) => Ok(Value::Scalar(*n)),
3865            Value::Matrix(m) => {
3866                let mut vals: Vec<f64> = m.iter().copied().collect();
3867                vals.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
3868                let mut unique: Vec<f64> = Vec::new();
3869                for v in vals {
3870                    if unique.last().is_none_or(|&last| last != v) {
3871                        unique.push(v);
3872                    }
3873                }
3874                let n = unique.len();
3875                Ok(Value::Matrix(
3876                    Array2::from_shape_vec((1, n), unique).unwrap(),
3877                ))
3878            }
3879            Value::Complex(_, _) => Err("unique: not applicable to complex values".to_string()),
3880            Value::ComplexMatrix(_) => Err("unique: not applicable to complex values".to_string()),
3881            Value::Str(_)
3882            | Value::StringObj(_)
3883            | Value::Lambda(_)
3884            | Value::Function { .. }
3885            | Value::Tuple(_)
3886            | Value::Cell(_)
3887            | Value::Struct(_)
3888            | Value::StructArray(_)
3889            | Value::DateTime(_)
3890            | Value::Duration(_)
3891            | Value::DateTimeArray(_)
3892            | Value::DurationArray(_) => {
3893                Err("unique: not applicable to non-numeric values".to_string())
3894            }
3895        },
3896        // --- Descriptive statistics ---
3897        ("std", 1) => apply_stat(&args[0], |s| stat_var_vec(s, false).sqrt(), "std"),
3898        ("std", 2) => {
3899            let w = scalar_arg(&args[1], name, 2)?;
3900            let population = w != 0.0;
3901            apply_stat(&args[0], |s| stat_var_vec(s, population).sqrt(), "std")
3902        }
3903        ("var", 1) => apply_stat(&args[0], |s| stat_var_vec(s, false), "var"),
3904        ("var", 2) => {
3905            let w = scalar_arg(&args[1], name, 2)?;
3906            let population = w != 0.0;
3907            apply_stat(&args[0], |s| stat_var_vec(s, population), "var")
3908        }
3909        ("cov", 1) => match &args[0] {
3910            Value::Scalar(_) => Ok(Value::Scalar(0.0)),
3911            Value::Matrix(m) => {
3912                if m.nrows() == 1 || m.ncols() == 1 {
3913                    let vals: Vec<f64> = m.iter().copied().collect();
3914                    Ok(Value::Scalar(stat_var_vec(&vals, false)))
3915                } else {
3916                    let (nobs, nvars) = (m.nrows(), m.ncols());
3917                    if nobs < 2 {
3918                        return Err("cov: need at least 2 observations".to_string());
3919                    }
3920                    let mut centered = m.clone();
3921                    for c in 0..nvars {
3922                        let col_mean: f64 = m.column(c).iter().sum::<f64>() / nobs as f64;
3923                        for r in 0..nobs {
3924                            centered[[r, c]] -= col_mean;
3925                        }
3926                    }
3927                    let denom = (nobs - 1) as f64;
3928                    let mut cov_mat = Array2::<f64>::zeros((nvars, nvars));
3929                    for i in 0..nvars {
3930                        for j in 0..nvars {
3931                            let dot: f64 =
3932                                (0..nobs).map(|r| centered[[r, i]] * centered[[r, j]]).sum();
3933                            cov_mat[[i, j]] = dot / denom;
3934                        }
3935                    }
3936                    Ok(Value::Matrix(cov_mat))
3937                }
3938            }
3939            _ => Err("cov: argument must be numeric".to_string()),
3940        },
3941        ("median", 1) => apply_stat(
3942            &args[0],
3943            |s| {
3944                if s.is_empty() {
3945                    return f64::NAN;
3946                }
3947                let mut v = s.to_vec();
3948                v.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
3949                let n = v.len();
3950                if n % 2 == 0 {
3951                    (v[n / 2 - 1] + v[n / 2]) / 2.0
3952                } else {
3953                    v[n / 2]
3954                }
3955            },
3956            "median",
3957        ),
3958        ("mode", 1) => apply_stat(
3959            &args[0],
3960            |s| {
3961                if s.is_empty() {
3962                    return f64::NAN;
3963                }
3964                let mut counts: std::collections::HashMap<u64, usize> =
3965                    std::collections::HashMap::new();
3966                for &x in s {
3967                    *counts.entry(x.to_bits()).or_insert(0) += 1;
3968                }
3969                let max_count = counts.values().copied().max().unwrap_or(0);
3970                let mut candidates: Vec<f64> = counts
3971                    .iter()
3972                    .filter(|&(_, &c)| c == max_count)
3973                    .map(|(&bits, _)| f64::from_bits(bits))
3974                    .collect();
3975                candidates.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
3976                candidates[0]
3977            },
3978            "mode",
3979        ),
3980        ("skewness", 1) => apply_stat(
3981            &args[0],
3982            |s| {
3983                let n = s.len();
3984                if n == 0 {
3985                    return f64::NAN;
3986                }
3987                if n == 1 {
3988                    return 0.0;
3989                }
3990                let mean = s.iter().sum::<f64>() / n as f64;
3991                let m2 = s.iter().map(|&x| (x - mean).powi(2)).sum::<f64>() / n as f64;
3992                if m2 == 0.0 {
3993                    return f64::NAN;
3994                }
3995                let m3 = s.iter().map(|&x| (x - mean).powi(3)).sum::<f64>() / n as f64;
3996                m3 / m2.powf(1.5)
3997            },
3998            "skewness",
3999        ),
4000        ("kurtosis", 1) => apply_stat(
4001            &args[0],
4002            |s| {
4003                let n = s.len();
4004                if n < 2 {
4005                    return f64::NAN;
4006                }
4007                let mean = s.iter().sum::<f64>() / n as f64;
4008                let m2 = s.iter().map(|&x| (x - mean).powi(2)).sum::<f64>() / n as f64;
4009                if m2 == 0.0 {
4010                    return f64::NAN;
4011                }
4012                let m4 = s.iter().map(|&x| (x - mean).powi(4)).sum::<f64>() / n as f64;
4013                m4 / m2.powi(2)
4014            },
4015            "kurtosis",
4016        ),
4017        ("hist", n) if n == 1 || n == 2 => {
4018            let n_bins = if args.len() == 2 {
4019                scalar_arg(&args[1], name, 2)? as usize
4020            } else {
4021                10
4022            };
4023            if n_bins == 0 {
4024                return Err("hist: number of bins must be positive".to_string());
4025            }
4026            let vals = numeric_vec(&args[0], name)?;
4027            if vals.is_empty() {
4028                return Ok(Value::Void);
4029            }
4030            let min_v = vals.iter().cloned().fold(f64::INFINITY, f64::min);
4031            let max_v = vals.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
4032            let range = if max_v > min_v { max_v - min_v } else { 1.0 };
4033            let mut counts = vec![0usize; n_bins];
4034            for &v in &vals {
4035                let b = ((v - min_v) / range * n_bins as f64) as usize;
4036                counts[b.min(n_bins - 1)] += 1;
4037            }
4038            let bar_cols: usize = std::env::var("COLUMNS")
4039                .ok()
4040                .and_then(|s| s.parse::<usize>().ok())
4041                .unwrap_or(80)
4042                .saturating_sub(26)
4043                .max(10);
4044            let max_count = *counts.iter().max().unwrap_or(&1).max(&1);
4045            let mut output = String::new();
4046            for (i, &c) in counts.iter().enumerate() {
4047                let lo = min_v + range * (i as f64 / n_bins as f64);
4048                let hi = min_v + range * ((i + 1) as f64 / n_bins as f64);
4049                let bar_len = c * bar_cols / max_count;
4050                output.push_str(&format!(
4051                    "{lo:8.4} {hi:8.4} |{bar:<bar_cols$}| {c}\n",
4052                    bar = "#".repeat(bar_len),
4053                ));
4054            }
4055            match io.as_deref_mut() {
4056                Some(ctx) => ctx.write_to_fd(1, &output)?,
4057                None => {
4058                    print!("{output}");
4059                    if output.contains('\n') {
4060                        std::io::stdout().flush().ok();
4061                    }
4062                }
4063            }
4064            Ok(Value::Void)
4065        }
4066        ("histc", 2) => {
4067            let vals = numeric_vec(&args[0], name)?;
4068            let edges = numeric_vec(&args[1], name)?;
4069            if edges.is_empty() {
4070                return Err("histc: edges must not be empty".to_string());
4071            }
4072            let n_edges = edges.len();
4073            let mut counts = vec![0.0f64; n_edges];
4074            for &v in &vals {
4075                // Linear scan — fine for typical edge counts
4076                let last = n_edges - 1;
4077                if v == edges[last] {
4078                    counts[last] += 1.0;
4079                } else {
4080                    for i in 0..last {
4081                        if v >= edges[i] && v < edges[i + 1] {
4082                            counts[i] += 1.0;
4083                            break;
4084                        }
4085                    }
4086                }
4087            }
4088            Ok(Value::Matrix(
4089                Array2::from_shape_vec((1, n_edges), counts).unwrap(),
4090            ))
4091        }
4092        // --- Percentiles and distributions ---
4093        ("prctile", 2) => {
4094            let p_vals = numeric_vec(&args[1], name)?;
4095            let n_p = p_vals.len();
4096
4097            // Sort one column of floats and compute all requested percentiles.
4098            let compute_col = |vals: &[f64]| -> Vec<f64> {
4099                let mut s = vals.to_vec();
4100                s.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
4101                p_vals.iter().map(|&p| percentile_sorted(&s, p)).collect()
4102            };
4103
4104            match &args[0] {
4105                Value::Scalar(n) => {
4106                    let pr = compute_col(&[*n]);
4107                    if n_p == 1 {
4108                        Ok(Value::Scalar(pr[0]))
4109                    } else {
4110                        Ok(Value::Matrix(Array2::from_shape_vec((1, n_p), pr).unwrap()))
4111                    }
4112                }
4113                Value::Matrix(m) if m.nrows() == 1 || m.ncols() == 1 => {
4114                    let vals: Vec<f64> = m.iter().copied().collect();
4115                    let pr = compute_col(&vals);
4116                    if n_p == 1 {
4117                        Ok(Value::Scalar(pr[0]))
4118                    } else {
4119                        Ok(Value::Matrix(Array2::from_shape_vec((1, n_p), pr).unwrap()))
4120                    }
4121                }
4122                Value::Matrix(m) => {
4123                    // M×N matrix: column-wise → n_p × ncols result
4124                    let ncols = m.ncols();
4125                    let mut result = Array2::<f64>::zeros((n_p, ncols));
4126                    for j in 0..ncols {
4127                        let col: Vec<f64> = m.column(j).iter().copied().collect();
4128                        let pr = compute_col(&col);
4129                        for (i, &v) in pr.iter().enumerate() {
4130                            result[[i, j]] = v;
4131                        }
4132                    }
4133                    if n_p == 1 {
4134                        let row: Vec<f64> = result.row(0).iter().copied().collect();
4135                        Ok(Value::Matrix(
4136                            Array2::from_shape_vec((1, ncols), row).unwrap(),
4137                        ))
4138                    } else {
4139                        Ok(Value::Matrix(result))
4140                    }
4141                }
4142                _ => Err("prctile: first argument must be numeric".to_string()),
4143            }
4144        }
4145        ("iqr", 1) => apply_stat(
4146            &args[0],
4147            |s| {
4148                let mut sorted = s.to_vec();
4149                sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
4150                percentile_sorted(&sorted, 75.0) - percentile_sorted(&sorted, 25.0)
4151            },
4152            "iqr",
4153        ),
4154        ("zscore", 1) => match &args[0] {
4155            Value::Scalar(_) => Ok(Value::Scalar(0.0)),
4156            Value::Matrix(m) => {
4157                if m.nrows() == 1 || m.ncols() == 1 {
4158                    let vals: Vec<f64> = m.iter().copied().collect();
4159                    let n = vals.len() as f64;
4160                    let mean = vals.iter().sum::<f64>() / n;
4161                    let s = stat_var_vec(&vals, false).sqrt();
4162                    let result: Vec<f64> = vals
4163                        .iter()
4164                        .map(|&x| if s == 0.0 { 0.0 } else { (x - mean) / s })
4165                        .collect();
4166                    Ok(Value::Matrix(
4167                        Array2::from_shape_vec(m.raw_dim(), result).unwrap(),
4168                    ))
4169                } else {
4170                    let (nrows, ncols) = (m.nrows(), m.ncols());
4171                    let mut result = m.clone();
4172                    for j in 0..ncols {
4173                        let col: Vec<f64> = m.column(j).iter().copied().collect();
4174                        let mean = col.iter().sum::<f64>() / col.len() as f64;
4175                        let s = stat_var_vec(&col, false).sqrt();
4176                        for i in 0..nrows {
4177                            result[[i, j]] = if s == 0.0 {
4178                                0.0
4179                            } else {
4180                                (m[[i, j]] - mean) / s
4181                            };
4182                        }
4183                    }
4184                    Ok(Value::Matrix(result))
4185                }
4186            }
4187            _ => Err("zscore: argument must be numeric".to_string()),
4188        },
4189        // diag(v) — vector → diagonal matrix; diag(A) → column vector of main diagonal.
4190        ("diag", 1) => match &args[0] {
4191            Value::Scalar(n) => Ok(Value::Matrix(Array2::from_elem((1, 1), *n))),
4192            Value::Matrix(m) => {
4193                let (rows, cols) = (m.nrows(), m.ncols());
4194                if rows == 1 || cols == 1 {
4195                    // vector → N×N diagonal matrix
4196                    let v: Vec<f64> = m.iter().copied().collect();
4197                    let n = v.len();
4198                    let mut result = Array2::<f64>::zeros((n, n));
4199                    for (i, &val) in v.iter().enumerate() {
4200                        result[[i, i]] = val;
4201                    }
4202                    Ok(Value::Matrix(result))
4203                } else {
4204                    // matrix → extract main diagonal as N×1 column vector
4205                    let n = rows.min(cols);
4206                    let d: Vec<f64> = (0..n).map(|i| m[[i, i]]).collect();
4207                    Ok(Value::Matrix(Array2::from_shape_vec((n, 1), d).unwrap()))
4208                }
4209            }
4210            Value::Void => Err("diag: not applicable to void".to_string()),
4211            Value::Complex(re, im) => {
4212                let mut result = Array2::<Complex<f64>>::zeros((1, 1));
4213                result[[0, 0]] = Complex::new(*re, *im);
4214                Ok(Value::ComplexMatrix(result))
4215            }
4216            Value::ComplexMatrix(m) => {
4217                let (rows, cols) = (m.nrows(), m.ncols());
4218                if rows == 1 || cols == 1 {
4219                    let v: Vec<Complex<f64>> = m.iter().copied().collect();
4220                    let n = v.len();
4221                    let mut result = Array2::<Complex<f64>>::zeros((n, n));
4222                    for (i, &val) in v.iter().enumerate() {
4223                        result[[i, i]] = val;
4224                    }
4225                    Ok(Value::ComplexMatrix(result))
4226                } else {
4227                    let n = rows.min(cols);
4228                    let d: Vec<Complex<f64>> = (0..n).map(|i| m[[i, i]]).collect();
4229                    Ok(Value::ComplexMatrix(
4230                        Array2::from_shape_vec((n, 1), d).unwrap(),
4231                    ))
4232                }
4233            }
4234            Value::Str(_)
4235            | Value::StringObj(_)
4236            | Value::Lambda(_)
4237            | Value::Function { .. }
4238            | Value::Tuple(_)
4239            | Value::Cell(_)
4240            | Value::Struct(_)
4241            | Value::StructArray(_)
4242            | Value::DateTime(_)
4243            | Value::Duration(_)
4244            | Value::DateTimeArray(_)
4245            | Value::DurationArray(_) => {
4246                Err("diag: not applicable to non-numeric values".to_string())
4247            }
4248        },
4249
4250        // --- Complex built-ins ---
4251        // real(z) — real part; works on scalars, matrices, and complex matrices.
4252        ("real", 1) => match &args[0] {
4253            Value::Void => Err("real: not applicable to void".to_string()),
4254            Value::Scalar(n) => Ok(Value::Scalar(*n)),
4255            Value::Complex(re, _) => Ok(Value::Scalar(*re)),
4256            Value::Matrix(m) => Ok(Value::Matrix(m.clone())),
4257            Value::ComplexMatrix(m) => Ok(Value::Matrix(m.mapv(|c| c.re))),
4258            Value::Str(_)
4259            | Value::StringObj(_)
4260            | Value::Lambda(_)
4261            | Value::Function { .. }
4262            | Value::Tuple(_)
4263            | Value::Cell(_)
4264            | Value::Struct(_)
4265            | Value::StructArray(_)
4266            | Value::DateTime(_)
4267            | Value::Duration(_)
4268            | Value::DateTimeArray(_)
4269            | Value::DurationArray(_) => {
4270                Err("real: not applicable to non-numeric values".to_string())
4271            }
4272        },
4273        // imag(z) — imaginary part; returns 0.0 for real scalars and real matrices.
4274        ("imag", 1) => match &args[0] {
4275            Value::Void => Err("imag: not applicable to void".to_string()),
4276            Value::Scalar(_) => Ok(Value::Scalar(0.0)),
4277            Value::Complex(_, im) => Ok(Value::Scalar(*im)),
4278            Value::Matrix(m) => Ok(Value::Matrix(Array2::zeros(m.raw_dim()))),
4279            Value::ComplexMatrix(m) => Ok(Value::Matrix(m.mapv(|c| c.im))),
4280            Value::Str(_)
4281            | Value::StringObj(_)
4282            | Value::Lambda(_)
4283            | Value::Function { .. }
4284            | Value::Tuple(_)
4285            | Value::Cell(_)
4286            | Value::Struct(_)
4287            | Value::StructArray(_)
4288            | Value::DateTime(_)
4289            | Value::Duration(_)
4290            | Value::DateTimeArray(_)
4291            | Value::DurationArray(_) => {
4292                Err("imag: not applicable to non-numeric values".to_string())
4293            }
4294        },
4295        // abs(z) — modulus; overloads scalar abs; element-wise on matrices.
4296        ("abs", 1) => match &args[0] {
4297            Value::Void => Err("abs: not applicable to void".to_string()),
4298            Value::Scalar(n) => Ok(Value::Scalar(n.abs())),
4299            Value::Complex(re, im) => Ok(Value::Scalar((re * re + im * im).sqrt())),
4300            Value::Matrix(m) => Ok(Value::Matrix(m.mapv(|x| x.abs()))),
4301            Value::ComplexMatrix(m) => Ok(Value::Matrix(m.mapv(|c| c.norm()))),
4302            Value::Str(_)
4303            | Value::StringObj(_)
4304            | Value::Lambda(_)
4305            | Value::Function { .. }
4306            | Value::Tuple(_)
4307            | Value::Cell(_)
4308            | Value::Struct(_)
4309            | Value::StructArray(_)
4310            | Value::DateTime(_)
4311            | Value::Duration(_)
4312            | Value::DateTimeArray(_)
4313            | Value::DurationArray(_) => {
4314                Err("abs: not applicable to non-numeric values".to_string())
4315            }
4316        },
4317        // angle(z) — argument in radians; returns 0 for non-negative reals.
4318        ("angle", 1) => match &args[0] {
4319            Value::Void => Err("angle: not applicable to void".to_string()),
4320            Value::Scalar(n) => Ok(Value::Scalar(if *n >= 0.0 {
4321                0.0
4322            } else {
4323                std::f64::consts::PI
4324            })),
4325            Value::Complex(re, im) => Ok(Value::Scalar(im.atan2(*re))),
4326            Value::Matrix(m) => {
4327                Ok(Value::Matrix(m.mapv(|x| {
4328                    if x >= 0.0 { 0.0 } else { std::f64::consts::PI }
4329                })))
4330            }
4331            Value::ComplexMatrix(m) => Ok(Value::Matrix(m.mapv(|c| c.im.atan2(c.re)))),
4332            Value::Str(_)
4333            | Value::StringObj(_)
4334            | Value::Lambda(_)
4335            | Value::Function { .. }
4336            | Value::Tuple(_)
4337            | Value::Cell(_)
4338            | Value::Struct(_)
4339            | Value::StructArray(_)
4340            | Value::DateTime(_)
4341            | Value::Duration(_)
4342            | Value::DateTimeArray(_)
4343            | Value::DurationArray(_) => {
4344                Err("angle: not applicable to non-numeric values".to_string())
4345            }
4346        },
4347        // conj(z) — complex conjugate; element-wise over complex matrices.
4348        ("conj", 1) => match &args[0] {
4349            Value::Void => Err("conj: not applicable to void".to_string()),
4350            Value::Scalar(n) => Ok(Value::Scalar(*n)),
4351            Value::Complex(re, im) => Ok(make_complex(*re, -*im)),
4352            Value::Matrix(m) => Ok(Value::Matrix(m.clone())),
4353            Value::ComplexMatrix(m) => Ok(Value::ComplexMatrix(m.mapv(|c| c.conj()))),
4354            Value::Str(_)
4355            | Value::StringObj(_)
4356            | Value::Lambda(_)
4357            | Value::Function { .. }
4358            | Value::Tuple(_)
4359            | Value::Cell(_)
4360            | Value::Struct(_)
4361            | Value::StructArray(_)
4362            | Value::DateTime(_)
4363            | Value::Duration(_)
4364            | Value::DateTimeArray(_)
4365            | Value::DurationArray(_) => {
4366                Err("conj: not applicable to non-numeric values".to_string())
4367            }
4368        },
4369        // complex(re, im) — construct complex from two reals.
4370        ("complex", 2) => {
4371            let re = scalar_arg(&args[0], name, 1)?;
4372            let im = scalar_arg(&args[1], name, 2)?;
4373            Ok(make_complex(re, im))
4374        }
4375        // isreal(z) — 1.0 if imaginary part is zero, 0.0 otherwise.
4376        ("isreal", 1) => match &args[0] {
4377            Value::Void => Ok(Value::Scalar(0.0)),
4378            Value::Scalar(_) => Ok(Value::Scalar(1.0)),
4379            Value::Complex(_, im) => Ok(Value::Scalar(if *im == 0.0 { 1.0 } else { 0.0 })),
4380            Value::Matrix(_) => Ok(Value::Scalar(1.0)),
4381            Value::ComplexMatrix(_) => Ok(Value::Scalar(0.0)),
4382            // Strings are not real numbers; functions are not numbers
4383            Value::Str(_) | Value::StringObj(_) => Ok(Value::Scalar(0.0)),
4384            Value::Lambda(_)
4385            | Value::Function { .. }
4386            | Value::Tuple(_)
4387            | Value::Cell(_)
4388            | Value::Struct(_)
4389            | Value::StructArray(_)
4390            | Value::DateTime(_)
4391            | Value::Duration(_)
4392            | Value::DateTimeArray(_)
4393            | Value::DurationArray(_) => Ok(Value::Scalar(0.0)),
4394        },
4395        // --- String built-ins ---
4396        // num2str(x) — convert number to char array string
4397        ("num2str", 1) => match &args[0] {
4398            Value::Void => Err("num2str: not applicable to void".to_string()),
4399            Value::Str(s) => Ok(Value::Str(s.clone())),
4400            Value::StringObj(s) => Ok(Value::Str(s.clone())),
4401            Value::Scalar(n) => Ok(Value::Str(fmt_auto_sig(*n, 5))),
4402            Value::Complex(re, im) => Ok(Value::Str(format_complex(*re, *im, &FormatMode::Short))),
4403            Value::Matrix(m) => {
4404                let s = m
4405                    .iter()
4406                    .map(|x| fmt_auto_sig(*x, 5))
4407                    .collect::<Vec<_>>()
4408                    .join("  ");
4409                Ok(Value::Str(s))
4410            }
4411            Value::ComplexMatrix(_) => {
4412                Err("num2str: not supported for complex matrices".to_string())
4413            }
4414            Value::Lambda(_)
4415            | Value::Function { .. }
4416            | Value::Tuple(_)
4417            | Value::Cell(_)
4418            | Value::Struct(_)
4419            | Value::StructArray(_)
4420            | Value::DateTime(_)
4421            | Value::Duration(_)
4422            | Value::DateTimeArray(_)
4423            | Value::DurationArray(_) => Err("num2str: not applicable to this type".to_string()),
4424        },
4425        // num2str(x, N) — N significant digits
4426        ("num2str", 2) => {
4427            let n = scalar_arg(&args[1], name, 2)? as usize;
4428            match &args[0] {
4429                Value::Void => Err("num2str: not applicable to void".to_string()),
4430                Value::Str(s) => Ok(Value::Str(s.clone())),
4431                Value::StringObj(s) => Ok(Value::Str(s.clone())),
4432                Value::Scalar(v) => Ok(Value::Str(fmt_auto_sig(*v, n))),
4433                Value::Complex(re, im) => {
4434                    Ok(Value::Str(format_complex(*re, *im, &FormatMode::Custom(n))))
4435                }
4436                Value::Matrix(m) => {
4437                    let s = m
4438                        .iter()
4439                        .map(|x| fmt_auto_sig(*x, n))
4440                        .collect::<Vec<_>>()
4441                        .join("  ");
4442                    Ok(Value::Str(s))
4443                }
4444                Value::ComplexMatrix(_) => {
4445                    Err("num2str: not supported for complex matrices".to_string())
4446                }
4447                Value::Lambda(_)
4448                | Value::Function { .. }
4449                | Value::Tuple(_)
4450                | Value::Cell(_)
4451                | Value::Struct(_)
4452                | Value::StructArray(_)
4453                | Value::DateTime(_)
4454                | Value::Duration(_)
4455                | Value::DateTimeArray(_)
4456                | Value::DurationArray(_) => {
4457                    Err("num2str: not applicable to this type".to_string())
4458                }
4459            }
4460        }
4461        // str2double(s) — parse string as f64; return NaN on failure
4462        ("str2double", 1) => {
4463            let s = string_arg(&args[0], name, 1)?;
4464            match s.trim().parse::<f64>() {
4465                Ok(n) => Ok(Value::Scalar(n)),
4466                Err(_) => Ok(Value::Scalar(f64::NAN)),
4467            }
4468        }
4469        // str2num(s) — parse string as f64; return error on failure
4470        ("str2num", 1) => {
4471            let s = string_arg(&args[0], name, 1)?;
4472            s.trim()
4473                .parse::<f64>()
4474                .map(Value::Scalar)
4475                .map_err(|_| format!("str2num: cannot convert '{}' to number", s.trim()))
4476        }
4477        // strcat(a, b, ...) — concatenate strings
4478        ("strcat", n) if n >= 2 => {
4479            let mut result = String::new();
4480            let mut any_obj = false;
4481            for (i, arg) in args.iter().enumerate() {
4482                match arg {
4483                    Value::Str(s) => result.push_str(s.trim_end()),
4484                    Value::StringObj(s) => {
4485                        result.push_str(s);
4486                        any_obj = true;
4487                    }
4488                    _ => return Err(format!("strcat: argument {} must be a string", i + 1)),
4489                }
4490            }
4491            if any_obj {
4492                Ok(Value::StringObj(result))
4493            } else {
4494                Ok(Value::Str(result))
4495            }
4496        }
4497        // ischar(s) — 1.0 if char array, 0.0 otherwise
4498        ("ischar", 1) => Ok(Value::Scalar(if matches!(&args[0], Value::Str(_)) {
4499            1.0
4500        } else {
4501            0.0
4502        })),
4503        // isstring(s) — 1.0 if string object, 0.0 otherwise
4504        ("isstring", 1) => Ok(Value::Scalar(if matches!(&args[0], Value::StringObj(_)) {
4505            1.0
4506        } else {
4507            0.0
4508        })),
4509        // --- Struct built-ins ---
4510        // struct('k1',v1,'k2',v2,...) — construct a scalar struct from name-value pairs
4511        ("struct", _) => {
4512            if !args.len().is_multiple_of(2) {
4513                return Err(
4514                    "struct: requires an even number of arguments (name, value, ...)".to_string(),
4515                );
4516            }
4517            let mut map = IndexMap::new();
4518            for pair in args.chunks(2) {
4519                let key = match &pair[0] {
4520                    Value::Str(s) | Value::StringObj(s) => s.clone(),
4521                    _ => return Err("struct: field names must be strings".to_string()),
4522                };
4523                map.insert(key, pair[1].clone());
4524            }
4525            Ok(Value::Struct(map))
4526        }
4527        // fieldnames(s) — cell array of field names in insertion order
4528        ("fieldnames", 1) => match &args[0] {
4529            Value::Struct(map) => {
4530                let names: Vec<Value> = map.keys().map(|k| Value::Str(k.clone())).collect();
4531                Ok(Value::Cell(names))
4532            }
4533            Value::StructArray(arr) => {
4534                // Use field names from first element
4535                let names: Vec<Value> = arr
4536                    .first()
4537                    .map(|m| m.keys().map(|k| Value::Str(k.clone())).collect())
4538                    .unwrap_or_default();
4539                Ok(Value::Cell(names))
4540            }
4541            _ => Err("fieldnames: argument must be a struct".to_string()),
4542        },
4543        // isfield(s, 'name') — 1.0 if field exists, 0.0 otherwise
4544        ("isfield", 2) => {
4545            let field = match &args[1] {
4546                Value::Str(s) | Value::StringObj(s) => s.clone(),
4547                _ => return Err("isfield: second argument must be a string".to_string()),
4548            };
4549            Ok(Value::Scalar(match &args[0] {
4550                Value::Struct(map) if map.contains_key(&field) => 1.0,
4551                Value::StructArray(arr) if arr.first().is_some_and(|m| m.contains_key(&field)) => {
4552                    1.0
4553                }
4554                _ => 0.0,
4555            }))
4556        }
4557        // rmfield(s, 'name') — copy of struct with field removed
4558        ("rmfield", 2) => {
4559            let field = match &args[1] {
4560                Value::Str(s) | Value::StringObj(s) => s.clone(),
4561                _ => return Err("rmfield: second argument must be a string".to_string()),
4562            };
4563            match &args[0] {
4564                Value::Struct(map) => {
4565                    if !map.contains_key(&field) {
4566                        return Err(format!("rmfield: field '{field}' does not exist"));
4567                    }
4568                    let mut updated = map.clone();
4569                    updated.shift_remove(&field);
4570                    Ok(Value::Struct(updated))
4571                }
4572                Value::StructArray(arr) => {
4573                    let updated: Result<Vec<_>, _> = arr
4574                        .iter()
4575                        .map(|m| {
4576                            if !m.contains_key(&field) {
4577                                return Err(format!("rmfield: field '{field}' does not exist"));
4578                            }
4579                            let mut m2 = m.clone();
4580                            m2.shift_remove(&field);
4581                            Ok(m2)
4582                        })
4583                        .collect();
4584                    Ok(Value::StructArray(updated?))
4585                }
4586                _ => Err("rmfield: first argument must be a struct".to_string()),
4587            }
4588        }
4589        // isstruct(v) — 1.0 if v is a struct or struct array, 0.0 otherwise
4590        ("isstruct", 1) => Ok(Value::Scalar(
4591            if matches!(&args[0], Value::Struct(_) | Value::StructArray(_)) {
4592                1.0
4593            } else {
4594                0.0
4595            },
4596        )),
4597        // --- Cell array built-ins ---
4598        // isempty(v) — 1.0 if v has no elements, 0.0 otherwise.
4599        // Matches MATLAB: empty matrix, empty string, empty cell, or Void are empty.
4600        ("isempty", 1) => {
4601            let empty = match &args[0] {
4602                Value::Matrix(m) => m.is_empty(),
4603                Value::Str(s) | Value::StringObj(s) => s.is_empty(),
4604                Value::Cell(v) => v.is_empty(),
4605                Value::Void => true,
4606                _ => false,
4607            };
4608            Ok(Value::Scalar(if empty { 1.0 } else { 0.0 }))
4609        }
4610        // iscell(v) — 1.0 if v is a cell array, 0.0 otherwise
4611        ("iscell", 1) => Ok(Value::Scalar(if matches!(&args[0], Value::Cell(_)) {
4612            1.0
4613        } else {
4614            0.0
4615        })),
4616        // cell(n) — create 1×n cell of Scalar(0.0) slots
4617        ("cell", 1) => {
4618            let n = scalar_arg(&args[0], name, 1)? as usize;
4619            Ok(Value::Cell(vec![Value::Scalar(0.0); n]))
4620        }
4621        // cell(m, n) — create 1×(m*n) cell (2-D layout deferred; stored flat)
4622        ("cell", 2) => {
4623            let m = scalar_arg(&args[0], name, 1)? as usize;
4624            let n = scalar_arg(&args[1], name, 2)? as usize;
4625            Ok(Value::Cell(vec![Value::Scalar(0.0); m * n]))
4626        }
4627        // cellfun(f, c) — apply f to each element of cell c.
4628        // Returns Value::Matrix when all results are scalars; otherwise Value::Cell.
4629        ("cellfun", 2) => {
4630            let f = args[0].clone();
4631            match &args[1] {
4632                Value::Cell(elems) => {
4633                    let elems = elems.clone();
4634                    let mut results = Vec::with_capacity(elems.len());
4635                    for elem in &elems {
4636                        let result =
4637                            call_function_value(&f, std::slice::from_ref(elem), io.as_deref_mut())?;
4638                        results.push(result);
4639                    }
4640                    // Try uniform output (all scalars)
4641                    let all_scalar = results.iter().all(|v| matches!(v, Value::Scalar(_)));
4642                    if all_scalar {
4643                        let vals: Vec<f64> = results
4644                            .iter()
4645                            .map(|v| {
4646                                if let Value::Scalar(n) = v {
4647                                    *n
4648                                } else {
4649                                    unreachable!()
4650                                }
4651                            })
4652                            .collect();
4653                        let n = vals.len();
4654                        if n == 0 {
4655                            Ok(Value::Matrix(Array2::zeros((1, 0))))
4656                        } else {
4657                            Ok(Value::Matrix(Array2::from_shape_vec((1, n), vals).unwrap()))
4658                        }
4659                    } else {
4660                        Ok(Value::Cell(results))
4661                    }
4662                }
4663                _ => Err("cellfun: second argument must be a cell array".to_string()),
4664            }
4665        }
4666        // arrayfun(f, v) — apply f element-wise to matrix v.
4667        // Returns same-shape Value::Matrix (scalar-returning f only).
4668        ("arrayfun", 2) => {
4669            let f = args[0].clone();
4670            match &args[1] {
4671                Value::Matrix(m) => {
4672                    let m = m.clone();
4673                    let mut flat = Vec::with_capacity(m.len());
4674                    // Iterate in column-major order
4675                    for col in 0..m.ncols() {
4676                        for row in 0..m.nrows() {
4677                            let elem = Value::Scalar(m[[row, col]]);
4678                            let result = call_function_value(&f, &[elem], io.as_deref_mut())?;
4679                            match result {
4680                                Value::Scalar(n) => flat.push(n),
4681                                _ => {
4682                                    return Err(
4683                                        "arrayfun: function must return a scalar".to_string()
4684                                    );
4685                                }
4686                            }
4687                        }
4688                    }
4689                    Ok(Value::Matrix(
4690                        Array2::from_shape_vec((m.nrows(), m.ncols()), flat).unwrap(),
4691                    ))
4692                }
4693                Value::Scalar(n) => {
4694                    let elem = Value::Scalar(*n);
4695                    let result = call_function_value(&f, &[elem], io.as_deref_mut())?;
4696                    Ok(result)
4697                }
4698                _ => {
4699                    Err("arrayfun: second argument must be a numeric matrix or scalar".to_string())
4700                }
4701            }
4702        }
4703        // lower(s) — convert to lowercase
4704        ("lower", 1) => match &args[0] {
4705            Value::Str(s) => Ok(Value::Str(s.to_lowercase())),
4706            Value::StringObj(s) => Ok(Value::StringObj(s.to_lowercase())),
4707            _ => Err("lower: argument must be a string".to_string()),
4708        },
4709        // upper(s) — convert to uppercase
4710        ("upper", 1) => match &args[0] {
4711            Value::Str(s) => Ok(Value::Str(s.to_uppercase())),
4712            Value::StringObj(s) => Ok(Value::StringObj(s.to_uppercase())),
4713            _ => Err("upper: argument must be a string".to_string()),
4714        },
4715        // strtrim(s) — trim leading/trailing whitespace
4716        ("strtrim", 1) => match &args[0] {
4717            Value::Str(s) => Ok(Value::Str(s.trim().to_string())),
4718            Value::StringObj(s) => Ok(Value::StringObj(s.trim().to_string())),
4719            _ => Err("strtrim: argument must be a string".to_string()),
4720        },
4721        // strrep(s, old, new) — replace all occurrences
4722        ("strrep", 3) => {
4723            let s = string_arg(&args[0], name, 1)?.to_string();
4724            let old = string_arg(&args[1], name, 2)?;
4725            let new = string_arg(&args[2], name, 3)?;
4726            let result = s.replace(old, new);
4727            match &args[0] {
4728                Value::StringObj(_) => Ok(Value::StringObj(result)),
4729                _ => Ok(Value::Str(result)),
4730            }
4731        }
4732        // strcmp(a, b) — case-sensitive string comparison
4733        ("strcmp", 2) => {
4734            let a = string_arg(&args[0], name, 1)?;
4735            let b = string_arg(&args[1], name, 2)?;
4736            Ok(Value::Scalar(bool_to_f64(a == b)))
4737        }
4738        // strcmpi(a, b) — case-insensitive comparison
4739        ("strcmpi", 2) => {
4740            let a = string_arg(&args[0], name, 1)?.to_lowercase();
4741            let b = string_arg(&args[1], name, 2)?.to_lowercase();
4742            Ok(Value::Scalar(bool_to_f64(a == b)))
4743        }
4744        // disp(x) — display value without variable name, like MATLAB disp()
4745        ("disp", 1) => {
4746            use std::io::Write;
4747            let mode = get_display_fmt();
4748            let output = match &args[0] {
4749                Value::Str(s) | Value::StringObj(s) => format!("{s}\n"),
4750                v => match format_value_full(v, &mode) {
4751                    Some(block) => format!("{block}\n\n"),
4752                    None => format!("{}\n", format_value(v, get_display_base(), &mode)),
4753                },
4754            };
4755            match io {
4756                Some(ctx) => ctx.write_to_fd(1, &output)?,
4757                None => {
4758                    print!("{output}");
4759                    if output.contains('\n') {
4760                        std::io::stdout().flush().ok();
4761                    }
4762                }
4763            }
4764            Ok(Value::Void)
4765        }
4766        // sprintf(fmt, ...) — format and return as char array
4767        ("sprintf", n) if n >= 1 => {
4768            let fmt = string_arg(&args[0], name, 1)?.to_string();
4769            let result = format_printf(&fmt, &args[1..])?;
4770            Ok(Value::Str(result))
4771        }
4772        // fprintf([fd,] fmt, ...) — format and print; fd defaults to 1 (stdout)
4773        ("fprintf", n) if n >= 1 => {
4774            // If first arg is a numeric scalar, treat it as a file descriptor.
4775            let (fd, fmt_idx) = match &args[0] {
4776                Value::Scalar(n) => (*n as i32, 1),
4777                _ => (1, 0),
4778            };
4779            if fmt_idx >= args.len() {
4780                return Err("fprintf: missing format string".to_string());
4781            }
4782            let fmt = string_arg(&args[fmt_idx], name, fmt_idx + 1)?.to_string();
4783            let output = format_printf(&fmt, &args[fmt_idx + 1..])?;
4784            match io {
4785                Some(ctx) => ctx.write_to_fd(fd, &output)?,
4786                None => {
4787                    // No I/O context: only stdout (fd 1) is allowed
4788                    if fd == 1 {
4789                        use std::io::Write;
4790                        print!("{output}");
4791                        if output.contains('\n') {
4792                            std::io::stdout().flush().ok();
4793                        }
4794                    } else {
4795                        return Err("fprintf: file I/O not available in this context".to_string());
4796                    }
4797                }
4798            }
4799            Ok(Value::Void)
4800        }
4801        // fopen(path, mode) — open a file; returns fd or -1 on failure
4802        ("fopen", 2) => {
4803            let path = string_arg(&args[0], name, 1)?;
4804            let mode = string_arg(&args[1], name, 2)?;
4805            match io {
4806                Some(ctx) => Ok(Value::Scalar(ctx.fopen(path, mode) as f64)),
4807                None => Err("fopen: file I/O not available in this context".to_string()),
4808            }
4809        }
4810        // fclose(fd) or fclose('all')
4811        ("fclose", 1) => match &args[0] {
4812            Value::Str(s) if s == "all" => {
4813                if let Some(ctx) = io {
4814                    ctx.fclose_all();
4815                }
4816                Ok(Value::Scalar(0.0))
4817            }
4818            _ => {
4819                let fd = scalar_arg(&args[0], name, 1)? as i32;
4820                match io {
4821                    Some(ctx) => Ok(Value::Scalar(ctx.fclose(fd) as f64)),
4822                    None => Err("fclose: file I/O not available in this context".to_string()),
4823                }
4824            }
4825        },
4826        // fgetl(fd) — read line, strip newline; returns Str or Scalar(-1) at EOF
4827        ("fgetl", 1) => {
4828            let fd = scalar_arg(&args[0], name, 1)? as i32;
4829            match io {
4830                Some(ctx) => match ctx.fgetl(fd) {
4831                    Some(line) => Ok(Value::Str(line)),
4832                    None => Ok(Value::Scalar(-1.0)),
4833                },
4834                None => Err("fgetl: file I/O not available in this context".to_string()),
4835            }
4836        }
4837        // fgets(fd) — read line, keep newline; returns Str or Scalar(-1) at EOF
4838        ("fgets", 1) => {
4839            let fd = scalar_arg(&args[0], name, 1)? as i32;
4840            match io {
4841                Some(ctx) => match ctx.fgets(fd) {
4842                    Some(line) => Ok(Value::Str(line)),
4843                    None => Ok(Value::Scalar(-1.0)),
4844                },
4845                None => Err("fgets: file I/O not available in this context".to_string()),
4846            }
4847        }
4848        // isfile(path) — 1.0 if path exists and is a regular file, else 0.0
4849        ("isfile", 1) => {
4850            let path = string_arg(&args[0], name, 1)?;
4851            let is_file = std::fs::metadata(path)
4852                .map(|m| m.is_file())
4853                .unwrap_or(false);
4854            Ok(Value::Scalar(bool_to_f64(is_file)))
4855        }
4856        // isfolder(path) — 1.0 if path exists and is a directory, else 0.0
4857        ("isfolder", 1) => {
4858            let path = string_arg(&args[0], name, 1)?;
4859            let is_dir = std::fs::metadata(path).map(|m| m.is_dir()).unwrap_or(false);
4860            Ok(Value::Scalar(bool_to_f64(is_dir)))
4861        }
4862        // genpath(dir) — return dir and all subdirectories as a path separator-delimited string
4863        ("genpath", 1) => {
4864            let root = string_arg(&args[0], name, 1)?;
4865            let sep = if cfg!(windows) { ';' } else { ':' };
4866            let mut dirs: Vec<String> = Vec::new();
4867            let mut stack = vec![std::path::PathBuf::from(root)];
4868            while let Some(dir) = stack.pop() {
4869                if !dir.is_dir() {
4870                    continue;
4871                }
4872                dirs.push(dir.to_string_lossy().into_owned());
4873                if let Ok(entries) = std::fs::read_dir(&dir) {
4874                    let mut children: Vec<std::path::PathBuf> = entries
4875                        .filter_map(|e| e.ok())
4876                        .map(|e| e.path())
4877                        .filter(|p| p.is_dir())
4878                        .collect();
4879                    children.sort();
4880                    children.reverse();
4881                    stack.extend(children);
4882                }
4883            }
4884            Ok(Value::Str(dirs.join(&sep.to_string())))
4885        }
4886        // pwd() — current working directory as a char array (parser sends ans as sole arg for empty calls)
4887        ("pwd", _) => {
4888            let cwd = std::env::current_dir()
4889                .map(|p| p.to_string_lossy().into_owned())
4890                .unwrap_or_default();
4891            Ok(Value::Str(cwd))
4892        }
4893        // exist(name) — check var (1), then file (2), else 0
4894        ("exist", 1) => {
4895            let name_arg = string_arg(&args[0], name, 1)?;
4896            if env.contains_key(name_arg) {
4897                Ok(Value::Scalar(1.0))
4898            } else if std::path::Path::new(name_arg).is_file() {
4899                Ok(Value::Scalar(2.0))
4900            } else {
4901                Ok(Value::Scalar(0.0))
4902            }
4903        }
4904        // exist(name, 'var') or exist(name, 'file')
4905        ("exist", 2) => {
4906            let name_arg = string_arg(&args[0], name, 1)?;
4907            let kind = string_arg(&args[1], name, 2)?;
4908            match kind {
4909                "var" => Ok(Value::Scalar(if env.contains_key(name_arg) {
4910                    1.0
4911                } else {
4912                    0.0
4913                })),
4914                "file" => Ok(Value::Scalar(if std::path::Path::new(name_arg).is_file() {
4915                    2.0
4916                } else {
4917                    0.0
4918                })),
4919                other => Err(format!(
4920                    "exist: unknown type '{other}', expected 'var' or 'file'"
4921                )),
4922            }
4923        }
4924        // dlmread(path) / dlmread(path, delim)
4925        ("dlmread", 1) => {
4926            let path = string_arg(&args[0], name, 1)?.to_string();
4927            dlmread_impl(&path, None)
4928        }
4929        ("dlmread", 2) => {
4930            let path = string_arg(&args[0], name, 1)?.to_string();
4931            let delim = interpret_delim(string_arg(&args[1], name, 2)?);
4932            dlmread_impl(&path, Some(delim))
4933        }
4934        // dlmwrite(path, A) / dlmwrite(path, A, delim)
4935        ("dlmwrite", 2) => {
4936            let path = string_arg(&args[0], name, 1)?.to_string();
4937            dlmwrite_impl(&path, &args[1], None)
4938        }
4939        ("dlmwrite", 3) => {
4940            let path = string_arg(&args[0], name, 1)?.to_string();
4941            let delim = interpret_delim(string_arg(&args[2], name, 3)?);
4942            dlmwrite_impl(&path, &args[1], Some(delim))
4943        }
4944        // readmatrix(path) / readmatrix(path, 'Delimiter', d)
4945        ("readmatrix", n) if n == 1 || n == 3 => {
4946            let path = string_arg(&args[0], name, 1)?.to_string();
4947            let delim = parse_delimiter_opt(name, args, 1)?;
4948            readmatrix_impl(&path, delim)
4949        }
4950        // readtable(path) / readtable(path, 'Delimiter', d)
4951        ("readtable", n) if n == 1 || n == 3 => {
4952            let path = string_arg(&args[0], name, 1)?.to_string();
4953            let delim = parse_delimiter_opt(name, args, 1)?;
4954            readtable_impl(&path, delim)
4955        }
4956        // writetable(T, path) / writetable(T, path, 'Delimiter', d)
4957        ("writetable", n) if n == 2 || n == 4 => {
4958            let path = string_arg(&args[1], name, 2)?.to_string();
4959            let delim = parse_delimiter_opt(name, args, 2)?;
4960            writetable_impl(&args[0], &path, delim)
4961        }
4962        // xor(a, b) — element-wise XOR: (a != 0) XOR (b != 0)
4963        ("xor", 2) => {
4964            let a = &args[0];
4965            let b = &args[1];
4966            match (a, b) {
4967                (Value::Scalar(x), Value::Scalar(y)) => {
4968                    Ok(Value::Scalar(bool_to_f64((*x != 0.0) ^ (*y != 0.0))))
4969                }
4970                (Value::Matrix(mx), Value::Matrix(my)) => {
4971                    if mx.shape() != my.shape() {
4972                        return Err("xor: matrices must have the same dimensions".to_string());
4973                    }
4974                    Ok(Value::Matrix(ndarray::Zip::from(mx).and(my).map_collect(
4975                        |a, b| bool_to_f64((*a != 0.0) ^ (*b != 0.0)),
4976                    )))
4977                }
4978                (Value::Scalar(s), Value::Matrix(m)) => {
4979                    let sv = *s != 0.0;
4980                    Ok(Value::Matrix(m.mapv(|x| bool_to_f64(sv ^ (x != 0.0)))))
4981                }
4982                (Value::Matrix(m), Value::Scalar(s)) => {
4983                    let sv = *s != 0.0;
4984                    Ok(Value::Matrix(m.mapv(|x| bool_to_f64((x != 0.0) ^ sv))))
4985                }
4986                _ => Err("xor: arguments must be numeric".to_string()),
4987            }
4988        }
4989        // not(a) — element-wise NOT (alias for ~a)
4990        ("not", 1) => apply_elem(&args[0], |x| if x == 0.0 { 1.0 } else { 0.0 }),
4991        // int2str(x) — round to nearest integer, return as char array
4992        ("int2str", 1) => match &args[0] {
4993            Value::Scalar(n) => Ok(Value::Str(format!("{}", n.round() as i64))),
4994            Value::Matrix(m) => {
4995                let parts: Vec<String> =
4996                    m.iter().map(|x| format!("{}", x.round() as i64)).collect();
4997                Ok(Value::Str(parts.join("  ")))
4998            }
4999            _ => Err("int2str: argument must be numeric".to_string()),
5000        },
5001        // mat2str(A) — matrix to MATLAB literal syntax string
5002        ("mat2str", 1) => match &args[0] {
5003            Value::Scalar(n) => Ok(Value::Str(format!("{n}"))),
5004            Value::Matrix(m) => {
5005                if m.nrows() == 0 || m.ncols() == 0 {
5006                    return Ok(Value::Str("[]".to_string()));
5007                }
5008                let mut s = String::from("[");
5009                for (r, row) in m.rows().into_iter().enumerate() {
5010                    if r > 0 {
5011                        s.push(';');
5012                    }
5013                    for (c, val) in row.iter().enumerate() {
5014                        if c > 0 {
5015                            s.push(' ');
5016                        }
5017                        s.push_str(&format!("{val}"));
5018                    }
5019                }
5020                s.push(']');
5021                Ok(Value::Str(s))
5022            }
5023            _ => Err("mat2str: argument must be numeric".to_string()),
5024        },
5025        // strsplit(s, delim) — split string by delimiter, return cell array
5026        ("strsplit", 2) => {
5027            let s = string_arg(&args[0], name, 1)?.to_string();
5028            let delim = string_arg(&args[1], name, 2)?.to_string();
5029            let parts: Vec<Value> = s
5030                .split(delim.as_str())
5031                .map(|p| Value::Str(p.to_string()))
5032                .collect();
5033            Ok(Value::Cell(parts))
5034        }
5035        // strsplit(s) — split on whitespace
5036        ("strsplit", 1) => {
5037            let s = string_arg(&args[0], name, 1)?.to_string();
5038            let parts: Vec<Value> = s
5039                .split_whitespace()
5040                .map(|p| Value::Str(p.to_string()))
5041                .collect();
5042            Ok(Value::Cell(parts))
5043        }
5044        // strjoin(c) / strjoin(c, delim) — join a cell array of strings
5045        ("strjoin", n) if n == 1 || n == 2 => {
5046            let cells = match &args[0] {
5047                Value::Cell(v) => v,
5048                _ => {
5049                    return Err(
5050                        "strjoin: first argument must be a cell array of strings".to_string()
5051                    );
5052                }
5053            };
5054            let delim = if n == 2 {
5055                string_arg(&args[1], name, 2)?.to_string()
5056            } else {
5057                " ".to_string()
5058            };
5059            let mut parts: Vec<String> = Vec::with_capacity(cells.len());
5060            for (i, v) in cells.iter().enumerate() {
5061                match v {
5062                    Value::Str(s) | Value::StringObj(s) => parts.push(s.clone()),
5063                    _ => return Err(format!("strjoin: element {} must be a string", i + 1)),
5064                }
5065            }
5066            Ok(Value::Str(parts.join(&delim)))
5067        }
5068        // contains(s, pat) / contains(s, pat, 'IgnoreCase', tf) — substring check
5069        ("contains", 2) => {
5070            let s = string_arg(&args[0], name, 1)?;
5071            let pat = string_arg(&args[1], name, 2)?;
5072            Ok(Value::Scalar(bool_to_f64(s.contains(pat))))
5073        }
5074        ("contains", 4) => {
5075            let s = string_arg(&args[0], name, 1)?;
5076            let pat = string_arg(&args[1], name, 2)?;
5077            let key = string_arg(&args[2], name, 3)?;
5078            if key != "IgnoreCase" {
5079                return Err(format!(
5080                    "contains: unknown option '{key}'; expected 'IgnoreCase'"
5081                ));
5082            }
5083            let ignore = match &args[3] {
5084                Value::Scalar(n) => *n != 0.0,
5085                _ => return Err("contains: 'IgnoreCase' value must be a scalar".to_string()),
5086            };
5087            if ignore {
5088                Ok(Value::Scalar(bool_to_f64(
5089                    s.to_lowercase().contains(&pat.to_lowercase()),
5090                )))
5091            } else {
5092                Ok(Value::Scalar(bool_to_f64(s.contains(pat))))
5093            }
5094        }
5095        // startsWith(s, pat) — prefix check
5096        ("startsWith", 2) => {
5097            let s = string_arg(&args[0], name, 1)?;
5098            let pat = string_arg(&args[1], name, 2)?;
5099            Ok(Value::Scalar(bool_to_f64(s.starts_with(pat))))
5100        }
5101        // endsWith(s, pat) — suffix check
5102        ("endsWith", 2) => {
5103            let s = string_arg(&args[0], name, 1)?;
5104            let pat = string_arg(&args[1], name, 2)?;
5105            Ok(Value::Scalar(bool_to_f64(s.ends_with(pat))))
5106        }
5107        // regexp(s, pat) / regexp(s, pat, 'match') — regular expression search
5108        ("regexp", 2) => {
5109            let s = string_arg(&args[0], name, 1)?.to_string();
5110            let pat = string_arg(&args[1], name, 2)?.to_string();
5111            regexp_impl("regexp", &s, &pat, false, false)
5112        }
5113        ("regexp", 3) => {
5114            let s = string_arg(&args[0], name, 1)?.to_string();
5115            let pat = string_arg(&args[1], name, 2)?.to_string();
5116            let opt = string_arg(&args[2], name, 3)?;
5117            if opt != "match" {
5118                return Err(format!("regexp: unknown option '{opt}'; expected 'match'"));
5119            }
5120            regexp_impl("regexp", &s, &pat, false, true)
5121        }
5122        // regexpi(s, pat) — case-insensitive regexp
5123        ("regexpi", 2) => {
5124            let s = string_arg(&args[0], name, 1)?.to_string();
5125            let pat = string_arg(&args[1], name, 2)?.to_string();
5126            regexp_impl("regexpi", &s, &pat, true, false)
5127        }
5128        ("regexpi", 3) => {
5129            let s = string_arg(&args[0], name, 1)?.to_string();
5130            let pat = string_arg(&args[1], name, 2)?.to_string();
5131            let opt = string_arg(&args[2], name, 3)?;
5132            if opt != "match" {
5133                return Err(format!("regexpi: unknown option '{opt}'; expected 'match'"));
5134            }
5135            regexp_impl("regexpi", &s, &pat, true, true)
5136        }
5137        // regexprep(s, pat, rep) — replace all matches with literal replacement
5138        ("regexprep", 3) => {
5139            let s = string_arg(&args[0], name, 1)?.to_string();
5140            let pat = string_arg(&args[1], name, 2)?.to_string();
5141            let rep = string_arg(&args[2], name, 3)?.to_string();
5142            regexprep_impl(&s, &pat, &rep)
5143        }
5144        // error(fmt, args...) — raise a runtime error with a formatted message
5145        ("error", _) if !args.is_empty() => {
5146            let fmt_str = match &args[0] {
5147                Value::Str(s) | Value::StringObj(s) => s.clone(),
5148                _ => return Err("error: first argument must be a format string".to_string()),
5149            };
5150            let msg = format_printf(&fmt_str, &args[1..])?;
5151            Err(msg)
5152        }
5153        // warning(fmt, args...) — print a warning to stderr, continue execution
5154        ("warning", _) if !args.is_empty() => {
5155            let fmt_str = match &args[0] {
5156                Value::Str(s) | Value::StringObj(s) => s.clone(),
5157                _ => return Err("warning: first argument must be a format string".to_string()),
5158            };
5159            let msg = format_printf(&fmt_str, &args[1..])?;
5160            eprintln!("warning: {msg}");
5161            Ok(Value::Void)
5162        }
5163        // lasterr() — return last error message; lasterr(msg) — set and return previous
5164        ("lasterr", 0) => Ok(Value::Str(get_last_err())),
5165        ("lasterr", 1) => {
5166            let prev = get_last_err();
5167            let new_msg = match &args[0] {
5168                Value::Str(s) | Value::StringObj(s) => s.clone(),
5169                _ => return Err("lasterr: argument must be a string".to_string()),
5170            };
5171            set_last_err(&new_msg);
5172            Ok(Value::Str(prev))
5173        }
5174        // pcall(@func, args...) — protected call; returns [ok, result_or_msg]
5175        ("pcall", _) if !args.is_empty() => {
5176            let callable = args[0].clone();
5177            let call_args = &args[1..];
5178            let result = match &callable {
5179                Value::Lambda(f) => {
5180                    let f = f.clone();
5181                    f.0(call_args, io)
5182                }
5183                Value::Function { .. } => match io {
5184                    Some(io_ref) => FN_CALL_HOOK.with(|c| match c.get() {
5185                        Some(hook) => hook("<pcall>", &callable, call_args, env, io_ref),
5186                        None => Err("pcall: function execution not initialized".to_string()),
5187                    }),
5188                    None => {
5189                        let mut tmp_io = IoContext::new();
5190                        FN_CALL_HOOK.with(|c| match c.get() {
5191                            Some(hook) => hook("<pcall>", &callable, call_args, env, &mut tmp_io),
5192                            None => Err("pcall: function execution not initialized".to_string()),
5193                        })
5194                    }
5195                },
5196                _ => {
5197                    return Err(
5198                        "pcall: first argument must be a function handle (@func)".to_string()
5199                    );
5200                }
5201            };
5202            match result {
5203                Ok(v) => Ok(Value::Tuple(vec![Value::Scalar(1.0), v])),
5204                Err(msg) => {
5205                    set_last_err(&msg);
5206                    Ok(Value::Tuple(vec![Value::Scalar(0.0), Value::Str(msg)]))
5207                }
5208            }
5209        }
5210        // ── Phase 18 — Advanced linear algebra ──────────────────────────────
5211
5212        // eig(A): d = eig(A) → eigenvalue column vector; [V,D] = eig(A) → tuple.
5213        ("eig", 1) => match &args[0] {
5214            Value::Scalar(n) => {
5215                if get_nargout() <= 1 {
5216                    Ok(Value::Matrix(
5217                        Array2::from_shape_vec((1, 1), vec![*n]).unwrap(),
5218                    ))
5219                } else {
5220                    Ok(Value::Tuple(vec![
5221                        Value::Matrix(Array2::eye(1)),
5222                        Value::Matrix(Array2::from_elem((1, 1), *n)),
5223                    ]))
5224                }
5225            }
5226            Value::Matrix(m) => {
5227                let (evals, evecs) = eig_compute(m)?;
5228                let nn = evals.len();
5229                let has_imag = evals.iter().any(|c| c.im.abs() > 1e-14);
5230                if get_nargout() <= 1 {
5231                    if has_imag {
5232                        Ok(Value::ComplexMatrix(
5233                            Array2::from_shape_vec((nn, 1), evals).unwrap(),
5234                        ))
5235                    } else {
5236                        let reals: Vec<f64> = evals.iter().map(|c| c.re).collect();
5237                        Ok(Value::Matrix(
5238                            Array2::from_shape_vec((nn, 1), reals).unwrap(),
5239                        ))
5240                    }
5241                } else if has_imag {
5242                    Err("eig: [V,D] form not supported when eigenvalues are complex".to_string())
5243                } else {
5244                    let reals: Vec<f64> = evals.iter().map(|c| c.re).collect();
5245                    let mut d = Array2::<f64>::zeros((nn, nn));
5246                    for (i, &e) in reals.iter().enumerate() {
5247                        d[[i, i]] = e;
5248                    }
5249                    Ok(Value::Tuple(vec![Value::Matrix(evecs), Value::Matrix(d)]))
5250                }
5251            }
5252            _ => Err("eig: argument must be a real numeric matrix".to_string()),
5253        },
5254
5255        // svd(A): s = svd(A) → singular values; [U,S,V] = svd(A) → full tuple.
5256        // svd(A, 'econ') → economy tuple.
5257        ("svd", 1) => match &args[0] {
5258            Value::Scalar(n) => {
5259                let sv = n.abs();
5260                if get_nargout() <= 1 {
5261                    Ok(Value::Matrix(
5262                        Array2::from_shape_vec((1, 1), vec![sv]).unwrap(),
5263                    ))
5264                } else {
5265                    Ok(Value::Tuple(vec![
5266                        Value::Matrix(Array2::eye(1)),
5267                        Value::Matrix(Array2::from_elem((1, 1), sv)),
5268                        Value::Matrix(Array2::eye(1)),
5269                    ]))
5270                }
5271            }
5272            Value::Matrix(m) => {
5273                let mm = m.nrows();
5274                let nn = m.ncols();
5275                let (u_c, s_v, v_c) = svd_compute(m)?;
5276                let k = s_v.len();
5277                if get_nargout() <= 1 {
5278                    let col: Vec<f64> = s_v;
5279                    Ok(Value::Matrix(Array2::from_shape_vec((k, 1), col).unwrap()))
5280                } else {
5281                    // Full SVD: extend U to m×m, S to m×n.
5282                    let u_full = complete_orthonormal_basis(&u_c);
5283                    let mut s_mat = Array2::<f64>::zeros((mm, nn));
5284                    for (i, &sv) in s_v.iter().enumerate() {
5285                        s_mat[[i, i]] = sv;
5286                    }
5287                    Ok(Value::Tuple(vec![
5288                        Value::Matrix(u_full),
5289                        Value::Matrix(s_mat),
5290                        Value::Matrix(v_c),
5291                    ]))
5292                }
5293            }
5294            _ => Err("svd: argument must be a real numeric matrix".to_string()),
5295        },
5296        ("svd", 2) => match (&args[0], &args[1]) {
5297            (Value::Matrix(m), Value::Str(opt) | Value::StringObj(opt)) if opt == "econ" => {
5298                let (u_c, s_v, v_c) = svd_compute(m)?;
5299                let k = s_v.len();
5300                let mut s_mat = Array2::<f64>::zeros((k, k));
5301                for (i, &sv) in s_v.iter().enumerate() {
5302                    s_mat[[i, i]] = sv;
5303                }
5304                Ok(Value::Tuple(vec![
5305                    Value::Matrix(u_c),
5306                    Value::Matrix(s_mat),
5307                    Value::Matrix(v_c),
5308                ]))
5309            }
5310            _ => Err("svd: expected svd(A, 'econ')".to_string()),
5311        },
5312
5313        // lu(A): R = lu(A) → U factor; [L,U,P] = lu(A) → full tuple (PA=LU).
5314        ("lu", 1) => match &args[0] {
5315            Value::Scalar(n) => {
5316                if get_nargout() <= 1 {
5317                    Ok(Value::Scalar(*n))
5318                } else {
5319                    Ok(Value::Tuple(vec![
5320                        Value::Matrix(Array2::eye(1)),
5321                        Value::Matrix(Array2::from_elem((1, 1), *n)),
5322                        Value::Matrix(Array2::eye(1)),
5323                    ]))
5324                }
5325            }
5326            Value::Matrix(m) => {
5327                let (l, u, p) = lu_decompose(m)?;
5328                if get_nargout() <= 1 {
5329                    Ok(Value::Matrix(u))
5330                } else {
5331                    Ok(Value::Tuple(vec![
5332                        Value::Matrix(l),
5333                        Value::Matrix(u),
5334                        Value::Matrix(p),
5335                    ]))
5336                }
5337            }
5338            _ => Err("lu: argument must be a real numeric matrix".to_string()),
5339        },
5340
5341        // qr(A): R = qr(A) → R factor; [Q,R] = qr(A) → full tuple.
5342        ("qr", 1) => match &args[0] {
5343            Value::Scalar(n) => {
5344                if get_nargout() <= 1 {
5345                    Ok(Value::Scalar(*n))
5346                } else {
5347                    Ok(Value::Tuple(vec![
5348                        Value::Matrix(Array2::from_elem(
5349                            (1, 1),
5350                            if *n >= 0.0 { 1.0 } else { -1.0 },
5351                        )),
5352                        Value::Matrix(Array2::from_elem((1, 1), n.abs())),
5353                    ]))
5354                }
5355            }
5356            Value::Matrix(m) => {
5357                let (q, r) = qr_decompose(m)?;
5358                if get_nargout() <= 1 {
5359                    Ok(Value::Matrix(r))
5360                } else {
5361                    Ok(Value::Tuple(vec![Value::Matrix(q), Value::Matrix(r)]))
5362                }
5363            }
5364            _ => Err("qr: argument must be a real numeric matrix".to_string()),
5365        },
5366
5367        // chol(A): always returns upper triangular R such that A = R'*R.
5368        ("chol", 1) => match &args[0] {
5369            Value::Scalar(n) => {
5370                if *n < 0.0 {
5371                    Err("chol: value is not positive definite".to_string())
5372                } else {
5373                    Ok(Value::Scalar(n.sqrt()))
5374                }
5375            }
5376            Value::Matrix(m) => Ok(Value::Matrix(chol_decompose(m)?)),
5377            _ => Err("chol: argument must be a real numeric matrix".to_string()),
5378        },
5379
5380        // rank(A): numerical rank via SVD threshold.
5381        ("rank", 1) => match &args[0] {
5382            Value::Scalar(x) => Ok(Value::Scalar(if x.abs() > 1e-15 { 1.0 } else { 0.0 })),
5383            Value::Matrix(m) => {
5384                let (_, s_v, _) = svd_compute(m)?;
5385                let tol = (m.nrows().max(m.ncols())) as f64
5386                    * s_v.first().copied().unwrap_or(0.0)
5387                    * f64::EPSILON
5388                    * 2.0;
5389                let r = s_v.iter().filter(|&&s| s > tol).count();
5390                Ok(Value::Scalar(r as f64))
5391            }
5392            _ => Err("rank: argument must be a real numeric matrix".to_string()),
5393        },
5394
5395        // null(A): orthonormal basis for null space of A (columns of V for ~0 singular values).
5396        ("null", 1) => match &args[0] {
5397            Value::Scalar(_) => Ok(Value::Matrix(Array2::zeros((1, 0)))),
5398            Value::Matrix(m) => {
5399                let nn = m.ncols();
5400                let (_, s_v, v_c) = svd_compute(m)?;
5401                let tol = (m.nrows().max(nn)) as f64
5402                    * s_v.first().copied().unwrap_or(0.0)
5403                    * f64::EPSILON
5404                    * 2.0;
5405                let r = s_v.iter().filter(|&&s| s > tol).count();
5406                let null_k = nn.saturating_sub(r);
5407                if null_k == 0 {
5408                    return Ok(Value::Matrix(Array2::zeros((nn, 0))));
5409                }
5410                let mut result = Array2::<f64>::zeros((nn, null_k));
5411                for j in 0..null_k {
5412                    let col_idx = r + j;
5413                    if col_idx < v_c.ncols() {
5414                        for i in 0..nn {
5415                            result[[i, j]] = v_c[[i, col_idx]];
5416                        }
5417                    }
5418                }
5419                Ok(Value::Matrix(result))
5420            }
5421            _ => Err("null: argument must be a real numeric matrix".to_string()),
5422        },
5423
5424        // orth(A): orthonormal basis for column space of A (columns of U for nonzero singular values).
5425        ("orth", 1) => match &args[0] {
5426            Value::Scalar(x) => {
5427                if x.abs() > 1e-15 {
5428                    Ok(Value::Matrix(Array2::from_elem((1, 1), 1.0)))
5429                } else {
5430                    Ok(Value::Matrix(Array2::zeros((1, 0))))
5431                }
5432            }
5433            Value::Matrix(m) => {
5434                let mm = m.nrows();
5435                let (u_c, s_v, _) = svd_compute(m)?;
5436                let tol = (mm.max(m.ncols())) as f64
5437                    * s_v.first().copied().unwrap_or(0.0)
5438                    * f64::EPSILON
5439                    * 2.0;
5440                let r = s_v.iter().filter(|&&s| s > tol).count();
5441                if r == 0 {
5442                    return Ok(Value::Matrix(Array2::zeros((mm, 0))));
5443                }
5444                let mut result = Array2::<f64>::zeros((mm, r));
5445                for j in 0..r {
5446                    if j < u_c.ncols() {
5447                        for i in 0..mm {
5448                            result[[i, j]] = u_c[[i, j]];
5449                        }
5450                    }
5451                }
5452                Ok(Value::Matrix(result))
5453            }
5454            _ => Err("orth: argument must be a real numeric matrix".to_string()),
5455        },
5456
5457        // cond(A): condition number = σ_max / σ_min (2-norm by default).
5458        ("cond", 1) => match &args[0] {
5459            Value::Scalar(x) => {
5460                if x.abs() < 1e-15 {
5461                    Ok(Value::Scalar(f64::INFINITY))
5462                } else {
5463                    Ok(Value::Scalar(1.0))
5464                }
5465            }
5466            Value::Matrix(m) => {
5467                let (_, s_v, _) = svd_compute(m)?;
5468                if s_v.is_empty() {
5469                    return Ok(Value::Scalar(1.0));
5470                }
5471                let s_max = s_v[0];
5472                let s_min = *s_v.last().unwrap();
5473                Ok(Value::Scalar(if s_min < 1e-15 {
5474                    f64::INFINITY
5475                } else {
5476                    s_max / s_min
5477                }))
5478            }
5479            _ => Err("cond: argument must be a real numeric matrix".to_string()),
5480        },
5481
5482        // pinv(A): Moore-Penrose pseudoinverse via SVD.
5483        ("pinv", 1) => match &args[0] {
5484            Value::Scalar(x) => Ok(Value::Scalar(if x.abs() < 1e-15 { 0.0 } else { 1.0 / x })),
5485            Value::Matrix(m) => {
5486                let mm = m.nrows();
5487                let nn = m.ncols();
5488                let (u_c, s_v, v_c) = svd_compute(m)?;
5489                let k = s_v.len();
5490                let tol =
5491                    (mm.max(nn)) as f64 * s_v.first().copied().unwrap_or(0.0) * f64::EPSILON * 2.0;
5492                // pinv = V * diag(1/σ) * U^T
5493                let mut result = Array2::<f64>::zeros((nn, mm));
5494                for j in 0..k {
5495                    if s_v[j] > tol {
5496                        let inv_s = 1.0 / s_v[j];
5497                        for r in 0..nn {
5498                            for c in 0..mm {
5499                                result[[r, c]] += v_c[[r, j]] * inv_s * u_c[[c, j]];
5500                            }
5501                        }
5502                    }
5503                }
5504                Ok(Value::Matrix(result))
5505            }
5506            _ => Err("pinv: argument must be a real numeric matrix".to_string()),
5507        },
5508
5509        // ── Phase 26 — FFT ───────────────────────────────────────────────────────
5510        ("fft", 1) => fft_call(&args[0], None),
5511        ("fft", 2) => {
5512            let n = scalar_arg(&args[1], "fft", 2)?;
5513            let n = n as usize;
5514            if n == 0 {
5515                return Err("fft: length must be positive".to_string());
5516            }
5517            fft_call(&args[0], Some(n))
5518        }
5519        ("ifft", 1) => ifft_call(&args[0]),
5520
5521        // fftshift(x) — circular shift by floor(N/2), no feature flag required
5522        ("fftshift", 1) => match &args[0] {
5523            Value::Scalar(s) => Ok(Value::Scalar(*s)),
5524            Value::Matrix(m) => {
5525                let (nrows, ncols) = (m.nrows(), m.ncols());
5526                if nrows == 1 {
5527                    let n = ncols;
5528                    let shift = n / 2;
5529                    let data: Vec<f64> = m.iter().copied().collect();
5530                    let mut out = vec![0.0f64; n];
5531                    for (i, &x) in data.iter().enumerate() {
5532                        out[(i + shift) % n] = x;
5533                    }
5534                    Ok(Value::Matrix(Array2::from_shape_vec((1, n), out).unwrap()))
5535                } else if ncols == 1 {
5536                    let n = nrows;
5537                    let shift = n / 2;
5538                    let data: Vec<f64> = m.iter().copied().collect();
5539                    let mut out = vec![0.0f64; n];
5540                    for (i, &x) in data.iter().enumerate() {
5541                        out[(i + shift) % n] = x;
5542                    }
5543                    Ok(Value::Matrix(Array2::from_shape_vec((n, 1), out).unwrap()))
5544                } else {
5545                    let row_shift = nrows / 2;
5546                    let col_shift = ncols / 2;
5547                    let mut out = Array2::<f64>::zeros((nrows, ncols));
5548                    for i in 0..nrows {
5549                        for j in 0..ncols {
5550                            out[[(i + row_shift) % nrows, (j + col_shift) % ncols]] = m[[i, j]];
5551                        }
5552                    }
5553                    Ok(Value::Matrix(out))
5554                }
5555            }
5556            _ => Err("fftshift: argument must be a numeric matrix".to_string()),
5557        },
5558
5559        // ifftshift(x) — inverse circular shift by ceil(N/2), no feature flag required
5560        ("ifftshift", 1) => match &args[0] {
5561            Value::Scalar(s) => Ok(Value::Scalar(*s)),
5562            Value::Matrix(m) => {
5563                let (nrows, ncols) = (m.nrows(), m.ncols());
5564                if nrows == 1 {
5565                    let n = ncols;
5566                    let shift = n.div_ceil(2);
5567                    let data: Vec<f64> = m.iter().copied().collect();
5568                    let mut out = vec![0.0f64; n];
5569                    for (i, &x) in data.iter().enumerate() {
5570                        out[(i + shift) % n] = x;
5571                    }
5572                    Ok(Value::Matrix(Array2::from_shape_vec((1, n), out).unwrap()))
5573                } else if ncols == 1 {
5574                    let n = nrows;
5575                    let shift = n.div_ceil(2);
5576                    let data: Vec<f64> = m.iter().copied().collect();
5577                    let mut out = vec![0.0f64; n];
5578                    for (i, &x) in data.iter().enumerate() {
5579                        out[(i + shift) % n] = x;
5580                    }
5581                    Ok(Value::Matrix(Array2::from_shape_vec((n, 1), out).unwrap()))
5582                } else {
5583                    let row_shift = nrows.div_ceil(2);
5584                    let col_shift = ncols.div_ceil(2);
5585                    let mut out = Array2::<f64>::zeros((nrows, ncols));
5586                    for i in 0..nrows {
5587                        for j in 0..ncols {
5588                            out[[(i + row_shift) % nrows, (j + col_shift) % ncols]] = m[[i, j]];
5589                        }
5590                    }
5591                    Ok(Value::Matrix(out))
5592                }
5593            }
5594            _ => Err("ifftshift: argument must be a numeric matrix".to_string()),
5595        },
5596
5597        // fftfreq(n, d) — DFT sample frequencies, no feature flag required
5598        ("fftfreq", 2) => {
5599            let n = match &args[0] {
5600                Value::Scalar(s) => {
5601                    let n = *s as usize;
5602                    if *s < 1.0 || (*s - n as f64).abs() > 1e-9 {
5603                        return Err("fftfreq: n must be a positive integer".to_string());
5604                    }
5605                    n
5606                }
5607                _ => return Err("fftfreq: first argument must be a scalar integer".to_string()),
5608            };
5609            let d = scalar_arg(&args[1], "fftfreq", 2)?;
5610            if d == 0.0 {
5611                return Err("fftfreq: sample spacing d must be nonzero".to_string());
5612            }
5613            // NumPy-compatible formula: [0:pos_count-1, -neg_count:-1] / (n*d)
5614            let pos_count = (n - 1) / 2 + 1;
5615            let neg_count = n / 2;
5616            let factor = 1.0 / (n as f64 * d);
5617            let mut freqs = Vec::with_capacity(n);
5618            for k in 0..pos_count as i64 {
5619                freqs.push(k as f64 * factor);
5620            }
5621            let neg_start = -(neg_count as i64);
5622            for k in neg_start..0 {
5623                freqs.push(k as f64 * factor);
5624            }
5625            Ok(Value::Matrix(
5626                Array2::from_shape_vec((1, n), freqs).unwrap(),
5627            ))
5628        }
5629
5630        // jsondecode(str) / jsonencode(val)
5631        ("jsondecode", 1) => jsondecode_impl(&args[0]),
5632        ("jsonencode", 1) => jsonencode_impl(&args[0]),
5633
5634        // load('file.mat') — assignment form: data = load('file.mat')
5635        ("load", 1) => {
5636            let path = match &args[0] {
5637                Value::Str(s) | Value::StringObj(s) => s.clone(),
5638                _ => return Err("load: argument must be a string path".to_string()),
5639            };
5640            if !path.ends_with(".mat") {
5641                return Err("load: use bare 'load path' syntax for non-.mat files".to_string());
5642            }
5643            load_mat_file(&path)
5644        }
5645
5646        // assert(cond)
5647        ("assert", 1) => {
5648            let truthy = match &args[0] {
5649                Value::Scalar(n) => *n != 0.0 && !n.is_nan(),
5650                Value::Matrix(m) => m.iter().all(|&x| x != 0.0 && !x.is_nan()),
5651                Value::Complex(re, im) => *re != 0.0 || *im != 0.0,
5652                Value::Str(s) | Value::StringObj(s) => !s.is_empty(),
5653                _ => false,
5654            };
5655            if truthy {
5656                Ok(Value::Void)
5657            } else {
5658                Err("assert: condition is false".to_string())
5659            }
5660        }
5661
5662        // assert(expected, actual)
5663        ("assert", 2) => assert_values_equal(&args[0], &args[1], None),
5664
5665        // assert(expected, actual, tol)
5666        ("assert", 3) => {
5667            let tol = match &args[2] {
5668                Value::Scalar(t) => *t,
5669                _ => return Err("assert: tolerance must be a scalar".to_string()),
5670            };
5671            assert_values_equal(&args[0], &args[1], Some(tol))
5672        }
5673
5674        // ── datetime() constructor ────────────────────────────────────────────
5675        ("datetime", 1) => match &args[0] {
5676            Value::Str(s) | Value::StringObj(s) => {
5677                let s = s.as_str();
5678                if s == "now" {
5679                    return Ok(Value::DateTime(crate::datetime::now_timestamp()));
5680                }
5681                if s == "today" {
5682                    return Ok(Value::DateTime(crate::datetime::today_timestamp()));
5683                }
5684                crate::datetime::parse_iso8601(s).map(Value::DateTime)
5685            }
5686            _ => Err("datetime: expected a string or numeric constructor arguments".to_string()),
5687        },
5688        // datetime(ts, 'ConvertFrom', 'posixtime') — must come before the 3-scalar form
5689        ("datetime", 3) if matches!(&args[1], Value::Str(_) | Value::StringObj(_)) => {
5690            let ts = scalar_arg(&args[0], "datetime", 1)?;
5691            match (&args[1], &args[2]) {
5692                (Value::Str(k) | Value::StringObj(k), Value::Str(v) | Value::StringObj(v))
5693                    if k.eq_ignore_ascii_case("convertfrom")
5694                        && v.eq_ignore_ascii_case("posixtime") =>
5695                {
5696                    Ok(Value::DateTime(ts))
5697                }
5698                _ => Err("datetime: unsupported arguments".to_string()),
5699            }
5700        }
5701        ("datetime", 3) => {
5702            let y = scalar_arg(&args[0], "datetime", 1)? as i64;
5703            let mo = scalar_arg(&args[1], "datetime", 2)? as u32;
5704            let d = scalar_arg(&args[2], "datetime", 3)? as u32;
5705            Ok(Value::DateTime(crate::datetime::civil_to_timestamp(
5706                y, mo, d, 0, 0, 0.0,
5707            )))
5708        }
5709        ("datetime", 6) => {
5710            let y = scalar_arg(&args[0], "datetime", 1)? as i64;
5711            let mo = scalar_arg(&args[1], "datetime", 2)? as u32;
5712            let d = scalar_arg(&args[2], "datetime", 3)? as u32;
5713            let h = scalar_arg(&args[3], "datetime", 4)? as u32;
5714            let mi = scalar_arg(&args[4], "datetime", 5)? as u32;
5715            let s = scalar_arg(&args[5], "datetime", 6)?;
5716            Ok(Value::DateTime(crate::datetime::civil_to_timestamp(
5717                y, mo, d, h, mi, s,
5718            )))
5719        }
5720
5721        // ── Component extractors ──────────────────────────────────────────────
5722        ("year", 1) => match &args[0] {
5723            Value::DateTime(ts) => {
5724                let (y, ..) = crate::datetime::timestamp_to_civil(*ts);
5725                Ok(Value::Scalar(y as f64))
5726            }
5727            Value::DateTimeArray(v) => {
5728                let rows: Vec<f64> = v
5729                    .iter()
5730                    .map(|ts| {
5731                        let (y, ..) = crate::datetime::timestamp_to_civil(*ts);
5732                        y as f64
5733                    })
5734                    .collect();
5735                Ok(Value::Matrix(
5736                    ndarray::Array2::from_shape_vec((rows.len(), 1), rows)
5737                        .map_err(|e| e.to_string())?,
5738                ))
5739            }
5740            _ => Err("year: argument must be a datetime".to_string()),
5741        },
5742        ("month", 1) => match &args[0] {
5743            Value::DateTime(ts) => {
5744                let (_, mo, ..) = crate::datetime::timestamp_to_civil(*ts);
5745                Ok(Value::Scalar(mo as f64))
5746            }
5747            Value::DateTimeArray(v) => {
5748                let rows: Vec<f64> = v
5749                    .iter()
5750                    .map(|ts| {
5751                        let (_, mo, ..) = crate::datetime::timestamp_to_civil(*ts);
5752                        mo as f64
5753                    })
5754                    .collect();
5755                Ok(Value::Matrix(
5756                    ndarray::Array2::from_shape_vec((rows.len(), 1), rows)
5757                        .map_err(|e| e.to_string())?,
5758                ))
5759            }
5760            _ => Err("month: argument must be a datetime".to_string()),
5761        },
5762        ("day", 1) => match &args[0] {
5763            Value::DateTime(ts) => {
5764                let (_, _, d, ..) = crate::datetime::timestamp_to_civil(*ts);
5765                Ok(Value::Scalar(d as f64))
5766            }
5767            Value::DateTimeArray(v) => {
5768                let rows: Vec<f64> = v
5769                    .iter()
5770                    .map(|ts| {
5771                        let (_, _, d, ..) = crate::datetime::timestamp_to_civil(*ts);
5772                        d as f64
5773                    })
5774                    .collect();
5775                Ok(Value::Matrix(
5776                    ndarray::Array2::from_shape_vec((rows.len(), 1), rows)
5777                        .map_err(|e| e.to_string())?,
5778                ))
5779            }
5780            _ => Err("day: argument must be a datetime".to_string()),
5781        },
5782        ("hour", 1) => match &args[0] {
5783            Value::DateTime(ts) => {
5784                let (_, _, _, h, ..) = crate::datetime::timestamp_to_civil(*ts);
5785                Ok(Value::Scalar(h as f64))
5786            }
5787            Value::DateTimeArray(v) => {
5788                let rows: Vec<f64> = v
5789                    .iter()
5790                    .map(|ts| {
5791                        let (_, _, _, h, ..) = crate::datetime::timestamp_to_civil(*ts);
5792                        h as f64
5793                    })
5794                    .collect();
5795                Ok(Value::Matrix(
5796                    ndarray::Array2::from_shape_vec((rows.len(), 1), rows)
5797                        .map_err(|e| e.to_string())?,
5798                ))
5799            }
5800            _ => Err("hour: argument must be a datetime or duration".to_string()),
5801        },
5802        ("minute", 1) => match &args[0] {
5803            Value::DateTime(ts) => {
5804                let (_, _, _, _, mi, ..) = crate::datetime::timestamp_to_civil(*ts);
5805                Ok(Value::Scalar(mi as f64))
5806            }
5807            Value::DateTimeArray(v) => {
5808                let rows: Vec<f64> = v
5809                    .iter()
5810                    .map(|ts| {
5811                        let (_, _, _, _, mi, ..) = crate::datetime::timestamp_to_civil(*ts);
5812                        mi as f64
5813                    })
5814                    .collect();
5815                Ok(Value::Matrix(
5816                    ndarray::Array2::from_shape_vec((rows.len(), 1), rows)
5817                        .map_err(|e| e.to_string())?,
5818                ))
5819            }
5820            _ => Err("minute: argument must be a datetime or duration".to_string()),
5821        },
5822        ("second", 1) => match &args[0] {
5823            Value::DateTime(ts) => {
5824                let (_, _, _, _, _, s) = crate::datetime::timestamp_to_civil(*ts);
5825                Ok(Value::Scalar(s))
5826            }
5827            Value::DateTimeArray(v) => {
5828                let rows: Vec<f64> = v
5829                    .iter()
5830                    .map(|ts| {
5831                        let (_, _, _, _, _, s) = crate::datetime::timestamp_to_civil(*ts);
5832                        s
5833                    })
5834                    .collect();
5835                Ok(Value::Matrix(
5836                    ndarray::Array2::from_shape_vec((rows.len(), 1), rows)
5837                        .map_err(|e| e.to_string())?,
5838                ))
5839            }
5840            _ => Err("second: argument must be a datetime or duration".to_string()),
5841        },
5842
5843        // ── Predicates ────────────────────────────────────────────────────────
5844        ("isdatetime", 1) => Ok(Value::Scalar(bool_to_f64(matches!(
5845            &args[0],
5846            Value::DateTime(_) | Value::DateTimeArray(_)
5847        )))),
5848        ("isduration", 1) => Ok(Value::Scalar(bool_to_f64(matches!(
5849            &args[0],
5850            Value::Duration(_) | Value::DurationArray(_)
5851        )))),
5852        ("isnat", 1) => match &args[0] {
5853            Value::DateTime(ts) => Ok(Value::Scalar(bool_to_f64(ts.is_nan()))),
5854            Value::DateTimeArray(v) => {
5855                let rows: Vec<f64> = v
5856                    .iter()
5857                    .map(|ts| if ts.is_nan() { 1.0 } else { 0.0 })
5858                    .collect();
5859                Ok(Value::Matrix(
5860                    ndarray::Array2::from_shape_vec((rows.len(), 1), rows)
5861                        .map_err(|e| e.to_string())?,
5862                ))
5863            }
5864            _ => Ok(Value::Scalar(0.0)),
5865        },
5866
5867        // ── Duration constructors / extractors (overloaded) ───────────────────
5868        ("hours", 1) => match &args[0] {
5869            Value::Duration(s) => Ok(Value::Scalar(*s / 3600.0)),
5870            Value::DurationArray(v) => {
5871                let rows: Vec<f64> = v.iter().map(|s| s / 3600.0).collect();
5872                Ok(Value::Matrix(
5873                    ndarray::Array2::from_shape_vec((rows.len(), 1), rows)
5874                        .map_err(|e| e.to_string())?,
5875                ))
5876            }
5877            _ => {
5878                let s = scalar_arg(&args[0], "hours", 1)?;
5879                Ok(Value::Duration(s * 3600.0))
5880            }
5881        },
5882        ("minutes", 1) => match &args[0] {
5883            Value::Duration(s) => Ok(Value::Scalar(*s / 60.0)),
5884            Value::DurationArray(v) => {
5885                let rows: Vec<f64> = v.iter().map(|s| s / 60.0).collect();
5886                Ok(Value::Matrix(
5887                    ndarray::Array2::from_shape_vec((rows.len(), 1), rows)
5888                        .map_err(|e| e.to_string())?,
5889                ))
5890            }
5891            _ => {
5892                let s = scalar_arg(&args[0], "minutes", 1)?;
5893                Ok(Value::Duration(s * 60.0))
5894            }
5895        },
5896        ("seconds", 1) => match &args[0] {
5897            Value::Duration(s) => Ok(Value::Scalar(*s)),
5898            Value::DurationArray(v) => {
5899                let rows = v.to_vec();
5900                Ok(Value::Matrix(
5901                    ndarray::Array2::from_shape_vec((rows.len(), 1), rows)
5902                        .map_err(|e| e.to_string())?,
5903                ))
5904            }
5905            _ => {
5906                let s = scalar_arg(&args[0], "seconds", 1)?;
5907                Ok(Value::Duration(s))
5908            }
5909        },
5910        ("days", 1) => match &args[0] {
5911            Value::Duration(s) => Ok(Value::Scalar(*s / 86400.0)),
5912            Value::DurationArray(v) => {
5913                let rows: Vec<f64> = v.iter().map(|s| s / 86400.0).collect();
5914                Ok(Value::Matrix(
5915                    ndarray::Array2::from_shape_vec((rows.len(), 1), rows)
5916                        .map_err(|e| e.to_string())?,
5917                ))
5918            }
5919            _ => {
5920                let s = scalar_arg(&args[0], "days", 1)?;
5921                Ok(Value::Duration(s * 86400.0))
5922            }
5923        },
5924        ("milliseconds", 1) => match &args[0] {
5925            Value::Duration(s) => Ok(Value::Scalar(*s * 1000.0)),
5926            Value::DurationArray(v) => {
5927                let rows: Vec<f64> = v.iter().map(|s| s * 1000.0).collect();
5928                Ok(Value::Matrix(
5929                    ndarray::Array2::from_shape_vec((rows.len(), 1), rows)
5930                        .map_err(|e| e.to_string())?,
5931                ))
5932            }
5933            _ => {
5934                let s = scalar_arg(&args[0], "milliseconds", 1)?;
5935                Ok(Value::Duration(s / 1000.0))
5936            }
5937        },
5938        ("years", 1) => match &args[0] {
5939            Value::Duration(s) => Ok(Value::Scalar(*s / (365.2425 * 86400.0))),
5940            Value::DurationArray(v) => {
5941                let rows: Vec<f64> = v.iter().map(|s| s / (365.2425 * 86400.0)).collect();
5942                Ok(Value::Matrix(
5943                    ndarray::Array2::from_shape_vec((rows.len(), 1), rows)
5944                        .map_err(|e| e.to_string())?,
5945                ))
5946            }
5947            _ => {
5948                let s = scalar_arg(&args[0], "years", 1)?;
5949                Ok(Value::Duration(s * 365.2425 * 86400.0))
5950            }
5951        },
5952        // duration(H, M, S)
5953        ("duration", 3) => {
5954            let h = scalar_arg(&args[0], "duration", 1)?;
5955            let m = scalar_arg(&args[1], "duration", 2)?;
5956            let s = scalar_arg(&args[2], "duration", 3)?;
5957            Ok(Value::Duration(h * 3600.0 + m * 60.0 + s))
5958        }
5959
5960        // ── Formatting and conversion ─────────────────────────────────────────
5961        ("datestr", 1) => match &args[0] {
5962            Value::DateTime(ts) => {
5963                let s = crate::datetime::format_datestr(*ts, "dd-MMM-yyyy HH:mm:ss");
5964                Ok(Value::Str(s))
5965            }
5966            Value::DateTimeArray(v) => Ok(Value::Cell(
5967                v.iter()
5968                    .map(|ts| {
5969                        Value::Str(crate::datetime::format_datestr(*ts, "dd-MMM-yyyy HH:mm:ss"))
5970                    })
5971                    .collect(),
5972            )),
5973            _ => Err("datestr: argument must be a datetime".to_string()),
5974        },
5975        ("datestr", 2) => {
5976            let fmt_str = match &args[1] {
5977                Value::Str(s) | Value::StringObj(s) => s.clone(),
5978                _ => return Err("datestr: second argument must be a format string".to_string()),
5979            };
5980            match &args[0] {
5981                Value::DateTime(ts) => {
5982                    Ok(Value::Str(crate::datetime::format_datestr(*ts, &fmt_str)))
5983                }
5984                Value::DateTimeArray(v) => Ok(Value::Cell(
5985                    v.iter()
5986                        .map(|ts| Value::Str(crate::datetime::format_datestr(*ts, &fmt_str)))
5987                        .collect(),
5988                )),
5989                _ => Err("datestr: first argument must be a datetime".to_string()),
5990            }
5991        }
5992        ("datevec", 1) => match &args[0] {
5993            Value::DateTime(ts) => {
5994                let (y, mo, d, h, mi, s) = crate::datetime::timestamp_to_civil(*ts);
5995                let sec_i = s.floor() as u32;
5996                let data = vec![
5997                    y as f64,
5998                    mo as f64,
5999                    d as f64,
6000                    h as f64,
6001                    mi as f64,
6002                    sec_i as f64,
6003                ];
6004                Ok(Value::Matrix(
6005                    ndarray::Array2::from_shape_vec((1, 6), data).map_err(|e| e.to_string())?,
6006                ))
6007            }
6008            _ => Err("datevec: argument must be a datetime".to_string()),
6009        },
6010        ("datenum", 1) => match &args[0] {
6011            Value::DateTime(ts) => Ok(Value::Scalar(crate::datetime::to_datenum(*ts))),
6012            _ => Err("datenum: argument must be a datetime".to_string()),
6013        },
6014        ("datenum", 3) => {
6015            let y = scalar_arg(&args[0], "datenum", 1)? as i64;
6016            let mo = scalar_arg(&args[1], "datenum", 2)? as u32;
6017            let d = scalar_arg(&args[2], "datenum", 3)? as u32;
6018            let ts = crate::datetime::civil_to_timestamp(y, mo, d, 0, 0, 0.0);
6019            Ok(Value::Scalar(crate::datetime::to_datenum(ts)))
6020        }
6021        ("posixtime", 1) => match &args[0] {
6022            Value::DateTime(ts) => Ok(Value::Scalar(*ts)),
6023            _ => Err("posixtime: argument must be a datetime".to_string()),
6024        },
6025
6026        // ── diff for datetime/duration arrays ─────────────────────────────────
6027        ("diff", 1) => match &args[0] {
6028            Value::DateTimeArray(v) if v.len() >= 2 => {
6029                let diffs: Vec<f64> = v.windows(2).map(|w| w[1] - w[0]).collect();
6030                Ok(Value::DurationArray(diffs))
6031            }
6032            Value::DurationArray(v) if v.len() >= 2 => {
6033                let diffs: Vec<f64> = v.windows(2).map(|w| w[1] - w[0]).collect();
6034                Ok(Value::DurationArray(diffs))
6035            }
6036            Value::Matrix(m) => {
6037                // diff on numeric matrix: successive differences along first non-singleton dim
6038                let (nrows, ncols) = (m.nrows(), m.ncols());
6039                if ncols > 1 && nrows == 1 {
6040                    // Row vector → diff along columns
6041                    let data: Vec<f64> =
6042                        (0..ncols - 1).map(|j| m[[0, j + 1]] - m[[0, j]]).collect();
6043                    Ok(Value::Matrix(
6044                        ndarray::Array2::from_shape_vec((1, data.len()), data)
6045                            .map_err(|e| e.to_string())?,
6046                    ))
6047                } else if nrows > 1 {
6048                    // Column vector or matrix → diff along rows
6049                    let data: Vec<f64> = (0..nrows - 1)
6050                        .flat_map(|i| (0..ncols).map(move |j| m[[i + 1, j]] - m[[i, j]]))
6051                        .collect();
6052                    Ok(Value::Matrix(
6053                        ndarray::Array2::from_shape_vec((nrows - 1, ncols), data)
6054                            .map_err(|e| e.to_string())?,
6055                    ))
6056                } else {
6057                    Err("diff: input must have at least 2 elements".to_string())
6058                }
6059            }
6060            _ => Err("diff: unsupported argument type".to_string()),
6061        },
6062
6063        // ── Phase 23a — Matrix shape utilities ───────────────────────────────
6064        ("triu", 1) => match &args[0] {
6065            Value::Matrix(m) => {
6066                let mut r = m.clone();
6067                for i in 0..m.nrows() {
6068                    for j in 0..m.ncols() {
6069                        if (j as isize) < (i as isize) {
6070                            r[[i, j]] = 0.0;
6071                        }
6072                    }
6073                }
6074                Ok(Value::Matrix(r))
6075            }
6076            Value::Scalar(n) => Ok(Value::Scalar(*n)),
6077            _ => Err("triu: argument must be a numeric matrix".to_string()),
6078        },
6079        ("triu", 2) => match (&args[0], &args[1]) {
6080            (Value::Matrix(m), Value::Scalar(k)) => {
6081                let k = *k as isize;
6082                let mut r = m.clone();
6083                for i in 0..m.nrows() {
6084                    for j in 0..m.ncols() {
6085                        if (j as isize) - (i as isize) < k {
6086                            r[[i, j]] = 0.0;
6087                        }
6088                    }
6089                }
6090                Ok(Value::Matrix(r))
6091            }
6092            _ => Err("triu: expects (matrix, scalar)".to_string()),
6093        },
6094
6095        ("tril", 1) => match &args[0] {
6096            Value::Matrix(m) => {
6097                let mut r = m.clone();
6098                for i in 0..m.nrows() {
6099                    for j in 0..m.ncols() {
6100                        if (j as isize) > (i as isize) {
6101                            r[[i, j]] = 0.0;
6102                        }
6103                    }
6104                }
6105                Ok(Value::Matrix(r))
6106            }
6107            Value::Scalar(n) => Ok(Value::Scalar(*n)),
6108            _ => Err("tril: argument must be a numeric matrix".to_string()),
6109        },
6110        ("tril", 2) => match (&args[0], &args[1]) {
6111            (Value::Matrix(m), Value::Scalar(k)) => {
6112                let k = *k as isize;
6113                let mut r = m.clone();
6114                for i in 0..m.nrows() {
6115                    for j in 0..m.ncols() {
6116                        if (j as isize) - (i as isize) > k {
6117                            r[[i, j]] = 0.0;
6118                        }
6119                    }
6120                }
6121                Ok(Value::Matrix(r))
6122            }
6123            _ => Err("tril: expects (matrix, scalar)".to_string()),
6124        },
6125
6126        ("repmat", 3) => match (&args[0], &args[1], &args[2]) {
6127            (Value::Matrix(a), Value::Scalar(rm), Value::Scalar(cn)) => {
6128                let rm = *rm as usize;
6129                let cn = *cn as usize;
6130                if rm == 0 || cn == 0 {
6131                    return Ok(Value::Matrix(Array2::zeros((0, 0))));
6132                }
6133                let row_tile: Vec<Array2<f64>> = std::iter::repeat_n(a.view(), cn)
6134                    .map(|v| v.to_owned())
6135                    .collect();
6136                let row_block = ndarray::concatenate(
6137                    ndarray::Axis(1),
6138                    &row_tile.iter().map(|m| m.view()).collect::<Vec<_>>(),
6139                )
6140                .map_err(|e| e.to_string())?;
6141                let col_tiles: Vec<Array2<f64>> = std::iter::repeat_n(row_block.view(), rm)
6142                    .map(|v| v.to_owned())
6143                    .collect();
6144                let result = ndarray::concatenate(
6145                    ndarray::Axis(0),
6146                    &col_tiles.iter().map(|m| m.view()).collect::<Vec<_>>(),
6147                )
6148                .map_err(|e| e.to_string())?;
6149                Ok(Value::Matrix(result))
6150            }
6151            (Value::Scalar(s), Value::Scalar(rm), Value::Scalar(cn)) => {
6152                let rm = *rm as usize;
6153                let cn = *cn as usize;
6154                Ok(Value::Matrix(Array2::from_elem((rm, cn), *s)))
6155            }
6156            _ => Err("repmat: expects (matrix, m, n)".to_string()),
6157        },
6158
6159        ("kron", 2) => match (&args[0], &args[1]) {
6160            (Value::Matrix(a), Value::Matrix(b)) => {
6161                let (ra, ca) = (a.nrows(), a.ncols());
6162                let (rb, cb) = (b.nrows(), b.ncols());
6163                let mut result = Array2::<f64>::zeros((ra * rb, ca * cb));
6164                for i in 0..ra {
6165                    for j in 0..ca {
6166                        let aij = a[[i, j]];
6167                        for p in 0..rb {
6168                            for q in 0..cb {
6169                                result[[i * rb + p, j * cb + q]] = aij * b[[p, q]];
6170                            }
6171                        }
6172                    }
6173                }
6174                Ok(Value::Matrix(result))
6175            }
6176            (Value::Scalar(s), Value::Matrix(b)) => Ok(Value::Matrix(b.mapv(|x| x * s))),
6177            (Value::Matrix(a), Value::Scalar(s)) => Ok(Value::Matrix(a.mapv(|x| x * s))),
6178            (Value::Scalar(a), Value::Scalar(b)) => Ok(Value::Scalar(a * b)),
6179            _ => Err("kron: arguments must be numeric matrices".to_string()),
6180        },
6181
6182        // ── Phase 23b — Vector products ──────────────────────────────────────
6183        ("cross", 2) => {
6184            fn to_vec3(v: &Value, argn: usize) -> Result<[f64; 3], String> {
6185                match v {
6186                    Value::Matrix(m) => {
6187                        let flat: Vec<f64> = m.iter().copied().collect();
6188                        if flat.len() != 3 {
6189                            Err(format!(
6190                                "cross: argument {} must have exactly 3 elements",
6191                                argn
6192                            ))
6193                        } else {
6194                            Ok([flat[0], flat[1], flat[2]])
6195                        }
6196                    }
6197                    _ => Err(format!(
6198                        "cross: argument {} must be a 3-element vector",
6199                        argn
6200                    )),
6201                }
6202            }
6203            let a = to_vec3(&args[0], 1)?;
6204            let b = to_vec3(&args[1], 2)?;
6205            let cx = a[1] * b[2] - a[2] * b[1];
6206            let cy = a[2] * b[0] - a[0] * b[2];
6207            let cz = a[0] * b[1] - a[1] * b[0];
6208            // Result orientation follows first argument
6209            let result = match &args[0] {
6210                Value::Matrix(m) if m.nrows() == 1 => {
6211                    Array2::from_shape_vec((1, 3), vec![cx, cy, cz]).unwrap()
6212                }
6213                _ => Array2::from_shape_vec((3, 1), vec![cx, cy, cz]).unwrap(),
6214            };
6215            Ok(Value::Matrix(result))
6216        }
6217
6218        ("dot", 2) => {
6219            fn to_flat(v: &Value, argn: usize) -> Result<Vec<f64>, String> {
6220                match v {
6221                    Value::Matrix(m) => Ok(m.iter().copied().collect()),
6222                    Value::Scalar(s) => Ok(vec![*s]),
6223                    _ => Err(format!("dot: argument {} must be a numeric vector", argn)),
6224                }
6225            }
6226            let a = to_flat(&args[0], 1)?;
6227            let b = to_flat(&args[1], 2)?;
6228            if a.len() != b.len() {
6229                return Err(format!(
6230                    "dot: vectors must have the same length ({} vs {})",
6231                    a.len(),
6232                    b.len()
6233                ));
6234            }
6235            let s: f64 = a.iter().zip(b.iter()).map(|(x, y)| x * y).sum();
6236            Ok(Value::Scalar(s))
6237        }
6238
6239        // ── Phase 23c — Set operations ────────────────────────────────────────
6240        ("intersect", 2) => {
6241            fn to_sorted_vec(v: &Value, fname: &str) -> Result<Vec<f64>, String> {
6242                match v {
6243                    Value::Matrix(m) => {
6244                        let mut vals: Vec<f64> = m.iter().copied().collect();
6245                        vals.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
6246                        Ok(vals)
6247                    }
6248                    Value::Scalar(s) => Ok(vec![*s]),
6249                    _ => Err(format!("{fname}: arguments must be numeric vectors")),
6250                }
6251            }
6252            let a = to_sorted_vec(&args[0], "intersect")?;
6253            let b = to_sorted_vec(&args[1], "intersect")?;
6254            let b_set: std::collections::HashSet<u64> = b
6255                .iter()
6256                .filter(|x| !x.is_nan())
6257                .map(|x| x.to_bits())
6258                .collect();
6259            let mut result: Vec<f64> = Vec::new();
6260            for x in &a {
6261                if !x.is_nan()
6262                    && b_set.contains(&x.to_bits())
6263                    && result.last().is_none_or(|&last| last != *x)
6264                {
6265                    result.push(*x);
6266                }
6267            }
6268            let n = result.len();
6269            if n == 0 {
6270                Ok(Value::Matrix(Array2::zeros((1, 0))))
6271            } else {
6272                Ok(Value::Matrix(
6273                    Array2::from_shape_vec((1, n), result).unwrap(),
6274                ))
6275            }
6276        }
6277
6278        ("union", 2) => {
6279            fn collect_vals(v: &Value, fname: &str) -> Result<Vec<f64>, String> {
6280                match v {
6281                    Value::Matrix(m) => Ok(m.iter().copied().collect()),
6282                    Value::Scalar(s) => Ok(vec![*s]),
6283                    _ => Err(format!("{fname}: arguments must be numeric vectors")),
6284                }
6285            }
6286            let mut combined = collect_vals(&args[0], "union")?;
6287            combined.extend(collect_vals(&args[1], "union")?);
6288            combined.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
6289            let mut result: Vec<f64> = Vec::new();
6290            for x in combined {
6291                if result.last().is_none_or(|&last| last != x) {
6292                    result.push(x);
6293                }
6294            }
6295            let n = result.len();
6296            if n == 0 {
6297                Ok(Value::Matrix(Array2::zeros((1, 0))))
6298            } else {
6299                Ok(Value::Matrix(
6300                    Array2::from_shape_vec((1, n), result).unwrap(),
6301                ))
6302            }
6303        }
6304
6305        ("setdiff", 2) => {
6306            fn collect_vals2(v: &Value, fname: &str) -> Result<Vec<f64>, String> {
6307                match v {
6308                    Value::Matrix(m) => Ok(m.iter().copied().collect()),
6309                    Value::Scalar(s) => Ok(vec![*s]),
6310                    _ => Err(format!("{fname}: arguments must be numeric vectors")),
6311                }
6312            }
6313            let a = collect_vals2(&args[0], "setdiff")?;
6314            let b = collect_vals2(&args[1], "setdiff")?;
6315            let b_set: std::collections::HashSet<u64> = b
6316                .iter()
6317                .filter(|x| !x.is_nan())
6318                .map(|x| x.to_bits())
6319                .collect();
6320            let mut a_sorted = a.clone();
6321            a_sorted.sort_by(|x, y| x.partial_cmp(y).unwrap_or(std::cmp::Ordering::Equal));
6322            let mut result: Vec<f64> = Vec::new();
6323            for x in a_sorted {
6324                if !x.is_nan()
6325                    && !b_set.contains(&x.to_bits())
6326                    && result.last().is_none_or(|&last| last != x)
6327                {
6328                    result.push(x);
6329                }
6330            }
6331            let n = result.len();
6332            if n == 0 {
6333                Ok(Value::Matrix(Array2::zeros((1, 0))))
6334            } else {
6335                Ok(Value::Matrix(
6336                    Array2::from_shape_vec((1, n), result).unwrap(),
6337                ))
6338            }
6339        }
6340
6341        ("ismember", 2) => {
6342            fn collect_vals3(v: &Value, fname: &str) -> Result<Vec<f64>, String> {
6343                match v {
6344                    Value::Matrix(m) => Ok(m.iter().copied().collect()),
6345                    Value::Scalar(s) => Ok(vec![*s]),
6346                    _ => Err(format!("{fname}: arguments must be numeric")),
6347                }
6348            }
6349            let set: std::collections::HashSet<u64> = collect_vals3(&args[1], "ismember")?
6350                .into_iter()
6351                .filter(|x| !x.is_nan())
6352                .map(|x| x.to_bits())
6353                .collect();
6354            match &args[0] {
6355                Value::Scalar(s) => {
6356                    let found = !s.is_nan() && set.contains(&s.to_bits());
6357                    Ok(Value::Scalar(if found { 1.0 } else { 0.0 }))
6358                }
6359                Value::Matrix(m) => {
6360                    let result: Vec<f64> = m
6361                        .iter()
6362                        .map(|x| {
6363                            if !x.is_nan() && set.contains(&x.to_bits()) {
6364                                1.0
6365                            } else {
6366                                0.0
6367                            }
6368                        })
6369                        .collect();
6370                    let shape = m.raw_dim();
6371                    Ok(Value::Matrix(
6372                        Array2::from_shape_vec(shape, result).unwrap(),
6373                    ))
6374                }
6375                _ => Err("ismember: first argument must be numeric".to_string()),
6376            }
6377        }
6378
6379        // ── Phase 23d — Index utilities and element repetition ────────────────
6380        ("sub2ind", 3) => {
6381            let sz = match &args[0] {
6382                Value::Matrix(m) if m.len() == 2 => (m[[0, 0]] as usize, m[[0, 1]] as usize),
6383                _ => return Err("sub2ind: first argument must be [rows cols]".to_string()),
6384            };
6385            let rows = sz.0;
6386            fn idx_vals(v: &Value, argn: usize) -> Result<Vec<f64>, String> {
6387                match v {
6388                    Value::Scalar(s) => Ok(vec![*s]),
6389                    Value::Matrix(m) => Ok(m.iter().copied().collect()),
6390                    _ => Err(format!("sub2ind: argument {} must be numeric", argn)),
6391                }
6392            }
6393            let r = idx_vals(&args[1], 2)?;
6394            let c = idx_vals(&args[2], 3)?;
6395            if r.len() != c.len() {
6396                return Err(
6397                    "sub2ind: row and column index vectors must have the same length".to_string(),
6398                );
6399            }
6400            if r.len() == 1 {
6401                let idx = (c[0] as usize - 1) * rows + r[0] as usize;
6402                Ok(Value::Scalar(idx as f64))
6403            } else {
6404                let vals: Vec<f64> = r
6405                    .iter()
6406                    .zip(c.iter())
6407                    .map(|(&ri, &ci)| ((ci as usize - 1) * rows + ri as usize) as f64)
6408                    .collect();
6409                let n = vals.len();
6410                Ok(Value::Matrix(Array2::from_shape_vec((1, n), vals).unwrap()))
6411            }
6412        }
6413
6414        ("ind2sub", 2) => {
6415            let sz = match &args[0] {
6416                Value::Matrix(m) if m.len() == 2 => (m[[0, 0]] as usize, m[[0, 1]] as usize),
6417                _ => return Err("ind2sub: first argument must be [rows cols]".to_string()),
6418            };
6419            let rows = sz.0;
6420            fn idx_vals2(v: &Value, argn: usize) -> Result<Vec<f64>, String> {
6421                match v {
6422                    Value::Scalar(s) => Ok(vec![*s]),
6423                    Value::Matrix(m) => Ok(m.iter().copied().collect()),
6424                    _ => Err(format!("ind2sub: argument {} must be numeric", argn)),
6425                }
6426            }
6427            let indices = idx_vals2(&args[1], 2)?;
6428            if indices.len() == 1 {
6429                let idx = indices[0] as usize;
6430                let r = ((idx - 1) % rows + 1) as f64;
6431                let c = ((idx - 1) / rows + 1) as f64;
6432                Ok(Value::Tuple(vec![Value::Scalar(r), Value::Scalar(c)]))
6433            } else {
6434                let n = indices.len();
6435                let rs: Vec<f64> = indices
6436                    .iter()
6437                    .map(|&idx| ((idx as usize - 1) % rows + 1) as f64)
6438                    .collect();
6439                let cs: Vec<f64> = indices
6440                    .iter()
6441                    .map(|&idx| ((idx as usize - 1) / rows + 1) as f64)
6442                    .collect();
6443                let rm = Value::Matrix(Array2::from_shape_vec((1, n), rs).unwrap());
6444                let cm = Value::Matrix(Array2::from_shape_vec((1, n), cs).unwrap());
6445                Ok(Value::Tuple(vec![rm, cm]))
6446            }
6447        }
6448
6449        ("repelem", 2) => match (&args[0], &args[1]) {
6450            (Value::Matrix(a), Value::Scalar(n)) => {
6451                let n = *n as usize;
6452                let flat: Vec<f64> = a.iter().flat_map(|&x| std::iter::repeat_n(x, n)).collect();
6453                let total = flat.len();
6454                Ok(Value::Matrix(
6455                    Array2::from_shape_vec((1, total), flat).unwrap(),
6456                ))
6457            }
6458            (Value::Matrix(a), Value::Matrix(ns)) => {
6459                let av: Vec<f64> = a.iter().copied().collect();
6460                let nv: Vec<f64> = ns.iter().copied().collect();
6461                if av.len() != nv.len() {
6462                    return Err(
6463                        "repelem: element count vector must match source vector length".to_string(),
6464                    );
6465                }
6466                let flat: Vec<f64> = av
6467                    .iter()
6468                    .zip(nv.iter())
6469                    .flat_map(|(&x, &n)| std::iter::repeat_n(x, n as usize))
6470                    .collect();
6471                let total = flat.len();
6472                Ok(Value::Matrix(
6473                    Array2::from_shape_vec((1, total), flat).unwrap(),
6474                ))
6475            }
6476            (Value::Scalar(s), Value::Scalar(n)) => {
6477                let n = *n as usize;
6478                Ok(Value::Matrix(Array2::from_elem((1, n), *s)))
6479            }
6480            _ => Err("repelem: unsupported argument types".to_string()),
6481        },
6482        ("repelem", 3) => match (&args[0], &args[1], &args[2]) {
6483            (Value::Matrix(a), Value::Scalar(rm), Value::Scalar(cn)) => {
6484                let rm = *rm as usize;
6485                let cn = *cn as usize;
6486                let (nrows, ncols) = (a.nrows(), a.ncols());
6487                let mut result = Array2::<f64>::zeros((nrows * rm, ncols * cn));
6488                for i in 0..nrows {
6489                    for j in 0..ncols {
6490                        let v = a[[i, j]];
6491                        for di in 0..rm {
6492                            for dj in 0..cn {
6493                                result[[i * rm + di, j * cn + dj]] = v;
6494                            }
6495                        }
6496                    }
6497                }
6498                Ok(Value::Matrix(result))
6499            }
6500            (Value::Scalar(s), Value::Scalar(rm), Value::Scalar(cn)) => Ok(Value::Matrix(
6501                Array2::from_elem((*rm as usize, *cn as usize), *s),
6502            )),
6503            _ => Err("repelem: expects (matrix, m, n) for 2D repetition".to_string()),
6504        },
6505
6506        // ── Phase 24a — Polynomial evaluation, fitting, and roots ────────────
6507        ("polyval", 2) => {
6508            let coeffs = poly_coeffs(&args[0], "polyval")?;
6509            if coeffs.is_empty() {
6510                return Err("polyval: polynomial vector is empty".to_string());
6511            }
6512            match &args[1] {
6513                Value::Scalar(x) => Ok(Value::Scalar(horner(&coeffs, *x))),
6514                Value::Matrix(m) => Ok(Value::Matrix(m.mapv(|x| horner(&coeffs, x)))),
6515                _ => Err("polyval: second argument must be a real numeric value".to_string()),
6516            }
6517        }
6518
6519        ("polyfit", 3) => {
6520            let xv = poly_coeffs(&args[0], "polyfit")?;
6521            let yv = poly_coeffs(&args[1], "polyfit")?;
6522            let deg = match &args[2] {
6523                Value::Scalar(n) => {
6524                    let d = *n as usize;
6525                    if *n < 0.0 || (*n - d as f64).abs() > 1e-9 {
6526                        return Err("polyfit: degree must be a non-negative integer".to_string());
6527                    }
6528                    d
6529                }
6530                _ => return Err("polyfit: degree must be a scalar".to_string()),
6531            };
6532            if xv.len() != yv.len() {
6533                return Err("polyfit: x and y must have the same length".to_string());
6534            }
6535            let m = xv.len();
6536            let ncols = deg + 1;
6537            if ncols > m {
6538                return Err(format!(
6539                    "polyfit: not enough data points ({m}) for degree-{deg} fit"
6540                ));
6541            }
6542            // Build Vandermonde matrix (m × ncols), highest power first
6543            let mut vander = Array2::<f64>::zeros((m, ncols));
6544            for (i, &xi) in xv.iter().enumerate() {
6545                for j in 0..ncols {
6546                    vander[[i, j]] = xi.powi((deg - j) as i32);
6547                }
6548            }
6549            // Solve via QR: V*c = y  →  Q*R*c = y  →  R*c = Q^T*y
6550            let (q, r) = qr_decompose(&vander)?;
6551            let qty: Vec<f64> = (0..ncols)
6552                .map(|i| (0..m).map(|k| q[[k, i]] * yv[k]).sum())
6553                .collect();
6554            // Extract upper-left ncols×ncols block of R
6555            let mut r_sq = Array2::<f64>::zeros((ncols, ncols));
6556            for i in 0..ncols {
6557                for j in 0..ncols {
6558                    r_sq[[i, j]] = r[[i, j]];
6559                }
6560            }
6561            let coeffs = poly_back_sub(&r_sq, &qty)?;
6562            let result = Array2::from_shape_vec((1, ncols), coeffs)
6563                .map_err(|e| format!("polyfit: internal error: {e}"))?;
6564            Ok(Value::Matrix(result))
6565        }
6566
6567        ("roots", 1) => {
6568            let raw = poly_coeffs(&args[0], "roots")?;
6569            // Strip leading zeros
6570            let start = raw.iter().position(|&c| c != 0.0).unwrap_or(raw.len());
6571            let coeffs = &raw[start..];
6572            if coeffs.len() <= 1 {
6573                return Ok(Value::Matrix(Array2::zeros((0, 1))));
6574            }
6575            let roots = durand_kerner(coeffs)?;
6576            Ok(roots_to_value(&roots))
6577        }
6578
6579        ("poly", 1) => match &args[0] {
6580            Value::Scalar(r) => {
6581                let data = vec![1.0, -*r];
6582                Ok(Value::Matrix(Array2::from_shape_vec((1, 2), data).unwrap()))
6583            }
6584            Value::Matrix(m) => {
6585                if m.nrows() == 1 || m.ncols() == 1 {
6586                    // Root vector: expand (x − r_1)(x − r_2)…
6587                    let roots: Vec<f64> = if m.nrows() == 1 {
6588                        m.row(0).iter().copied().collect()
6589                    } else {
6590                        m.column(0).iter().copied().collect()
6591                    };
6592                    let mut p = vec![1.0_f64];
6593                    for &r in &roots {
6594                        p = poly_conv(&p, &[1.0, -r]);
6595                    }
6596                    let ncols = p.len();
6597                    Ok(Value::Matrix(
6598                        Array2::from_shape_vec((1, ncols), p).unwrap(),
6599                    ))
6600                } else {
6601                    // Square matrix: characteristic polynomial via Faddeev-LeVerrier
6602                    let coeffs = characteristic_poly(m)?;
6603                    let ncols = coeffs.len();
6604                    Ok(Value::Matrix(
6605                        Array2::from_shape_vec((1, ncols), coeffs).unwrap(),
6606                    ))
6607                }
6608            }
6609            _ => Err("poly: argument must be a numeric vector or square matrix".to_string()),
6610        },
6611
6612        // ── Phase 24b — Convolution, deconvolution, interpolation ────────────
6613        ("conv", 2) => {
6614            let a = poly_coeffs(&args[0], "conv")?;
6615            let b = poly_coeffs(&args[1], "conv")?;
6616            if a.is_empty() || b.is_empty() {
6617                return Ok(Value::Matrix(Array2::zeros((1, 0))));
6618            }
6619            let c = poly_conv(&a, &b);
6620            let len = c.len();
6621            Ok(Value::Matrix(Array2::from_shape_vec((1, len), c).unwrap()))
6622        }
6623
6624        ("deconv", 2) => {
6625            let c = poly_coeffs(&args[0], "deconv")?;
6626            let b = poly_coeffs(&args[1], "deconv")?;
6627            let (q, r) = poly_deconv(&c, &b)?;
6628            let qn = q.len();
6629            let rn = r.len();
6630            let q_val = Value::Matrix(Array2::from_shape_vec((1, qn), q).unwrap());
6631            let r_val = Value::Matrix(Array2::from_shape_vec((1, rn), r).unwrap());
6632            Ok(Value::Tuple(vec![q_val, r_val]))
6633        }
6634
6635        ("interp1", 3) => {
6636            let xv = poly_coeffs(&args[0], "interp1")?;
6637            let yv = poly_coeffs(&args[1], "interp1")?;
6638            if xv.len() != yv.len() {
6639                return Err("interp1: x and y must have the same length".to_string());
6640            }
6641            if xv.len() < 2 {
6642                return Err("interp1: requires at least two knot points".to_string());
6643            }
6644            match &args[2] {
6645                Value::Scalar(xi) => Ok(Value::Scalar(interp1_at(&xv, &yv, *xi, "linear"))),
6646                Value::Matrix(xi_m) => Ok(Value::Matrix(
6647                    xi_m.mapv(|xi| interp1_at(&xv, &yv, xi, "linear")),
6648                )),
6649                _ => Err("interp1: query points must be numeric".to_string()),
6650            }
6651        }
6652
6653        ("interp1", 4) => {
6654            let xv = poly_coeffs(&args[0], "interp1")?;
6655            let yv = poly_coeffs(&args[1], "interp1")?;
6656            let method = match &args[3] {
6657                Value::Str(s) | Value::StringObj(s) => s.clone(),
6658                _ => return Err("interp1: method argument must be a string".to_string()),
6659            };
6660            if !matches!(method.as_str(), "linear" | "nearest" | "previous" | "next") {
6661                return Err(format!(
6662                    "interp1: unknown method '{method}'; supported: linear nearest previous next"
6663                ));
6664            }
6665            if xv.len() != yv.len() {
6666                return Err("interp1: x and y must have the same length".to_string());
6667            }
6668            if xv.len() < 2 {
6669                return Err("interp1: requires at least two knot points".to_string());
6670            }
6671            match &args[2] {
6672                Value::Scalar(xi) => Ok(Value::Scalar(interp1_at(&xv, &yv, *xi, &method))),
6673                Value::Matrix(xi_m) => {
6674                    let m_str = method.as_str();
6675                    Ok(Value::Matrix(
6676                        xi_m.mapv(|xi| interp1_at(&xv, &yv, xi, m_str)),
6677                    ))
6678                }
6679                _ => Err("interp1: query points must be numeric".to_string()),
6680            }
6681        }
6682
6683        // ── 25b: tic / toc ────────────────────────────────────────────────────
6684        ("tic", 0) => {
6685            TIC_TIME.with(|t| t.set(Some(std::time::Instant::now())));
6686            Ok(Value::Void)
6687        }
6688        ("toc", 0) => {
6689            let elapsed = TIC_TIME.with(|t| t.get().map(|s| s.elapsed().as_secs_f64()));
6690            match elapsed {
6691                Some(t) => Ok(Value::Scalar(t)),
6692                None => Err("toc: tic must be called before toc".to_string()),
6693            }
6694        }
6695
6696        // ── 25a: eval (expression context — env mutations do not persist) ────
6697        ("eval", 1) => {
6698            let code = match &args[0] {
6699                Value::Str(s) | Value::StringObj(s) => s.clone(),
6700                _ => return Err("eval: argument must be a string".to_string()),
6701            };
6702            call_eval_str_hook(&code, env)
6703        }
6704        ("eval", 2) => {
6705            let code = match &args[0] {
6706                Value::Str(s) | Value::StringObj(s) => s.clone(),
6707                _ => return Err("eval: argument must be a string".to_string()),
6708            };
6709            match call_eval_str_hook(&code, env) {
6710                Err(e) => {
6711                    set_last_err(&e);
6712                    let catch = match &args[1] {
6713                        Value::Str(s) | Value::StringObj(s) => s.clone(),
6714                        _ => return Err("eval: catch argument must be a string".to_string()),
6715                    };
6716                    call_eval_str_hook(&catch, env)
6717                }
6718                ok => ok,
6719            }
6720        }
6721
6722        _ => {
6723            let hint = suggest_similar(name, env);
6724            match hint {
6725                Some(s) => Err(format!("Unknown function '{name}'; did you mean '{s}'?")),
6726                None => Err(format!("Unknown function: '{name}'")),
6727            }
6728        }
6729    }
6730}
6731
6732/// Interprets backslash escape sequences in delimiter strings.
6733/// `\t` → tab, `\n` → newline. Other strings are used as-is.
6734fn interpret_delim(s: &str) -> String {
6735    match s {
6736        r"\t" => "\t".to_string(),
6737        r"\n" => "\n".to_string(),
6738        other => other.to_string(),
6739    }
6740}
6741
6742/// Returns true if splitting every line by `delim` gives the same field count > 1.
6743fn delim_consistent(lines: &[&str], delim: char) -> bool {
6744    let counts: Vec<usize> = lines.iter().map(|l| l.split(delim).count()).collect();
6745    counts.iter().all(|&c| c > 1) && counts.windows(2).all(|w| w[0] == w[1])
6746}
6747
6748/// Reads a delimiter-separated numeric file and returns a `Value::Matrix`.
6749fn dlmread_impl(path: &str, explicit_delim: Option<String>) -> Result<Value, String> {
6750    let content =
6751        std::fs::read_to_string(path).map_err(|e| format!("dlmread: cannot read '{path}': {e}"))?;
6752
6753    let lines: Vec<&str> = content.lines().filter(|l| !l.trim().is_empty()).collect();
6754
6755    if lines.is_empty() {
6756        return Ok(Value::Matrix(Array2::zeros((0, 0))));
6757    }
6758
6759    // Determine delimiter: explicit → auto-detect (comma → tab → whitespace)
6760    let delim: Option<String> = match explicit_delim {
6761        Some(d) => Some(d),
6762        None => {
6763            if delim_consistent(&lines, ',') {
6764                Some(",".to_string())
6765            } else if delim_consistent(&lines, '\t') {
6766                Some("\t".to_string())
6767            } else {
6768                None // split by whitespace
6769            }
6770        }
6771    };
6772
6773    let mut rows: Vec<Vec<f64>> = Vec::new();
6774    for (line_num, line) in lines.iter().enumerate() {
6775        let fields: Vec<&str> = match &delim {
6776            Some(d) => line.split(d.as_str()).collect(),
6777            None => line.split_whitespace().collect(),
6778        };
6779        let mut row_vals: Vec<f64> = Vec::with_capacity(fields.len());
6780        for field in &fields {
6781            let trimmed = field.trim();
6782            if trimmed.is_empty() {
6783                row_vals.push(0.0);
6784            } else {
6785                row_vals.push(trimmed.parse::<f64>().map_err(|_| {
6786                    format!(
6787                        "dlmread: non-numeric value '{trimmed}' on line {}",
6788                        line_num + 1
6789                    )
6790                })?);
6791            }
6792        }
6793        if !row_vals.is_empty() {
6794            rows.push(row_vals);
6795        }
6796    }
6797
6798    if rows.is_empty() {
6799        return Ok(Value::Matrix(Array2::zeros((0, 0))));
6800    }
6801
6802    let ncols = rows[0].len();
6803    for (i, row) in rows.iter().enumerate() {
6804        if row.len() != ncols {
6805            return Err(format!(
6806                "dlmread: row {} has {} fields, expected {ncols}",
6807                i + 1,
6808                row.len()
6809            ));
6810        }
6811    }
6812
6813    let nrows = rows.len();
6814    let flat: Vec<f64> = rows.into_iter().flatten().collect();
6815    Array2::from_shape_vec((nrows, ncols), flat)
6816        .map_err(|e| format!("dlmread: shape error: {e}"))
6817        .map(Value::Matrix)
6818}
6819
6820/// Formats one f64 value for use in a delimited file.
6821/// Integers are written without decimal point; floats use full precision.
6822fn fmt_dlm_number(n: f64) -> String {
6823    if n.is_finite() && n == n.trunc() && n.abs() < 1e15 {
6824        format!("{}", n as i64)
6825    } else {
6826        format!("{n}")
6827    }
6828}
6829
6830/// Writes a scalar or matrix to a delimiter-separated file.
6831fn dlmwrite_impl(path: &str, val: &Value, explicit_delim: Option<String>) -> Result<Value, String> {
6832    let delim = explicit_delim.unwrap_or_else(|| ",".to_string());
6833
6834    let content = match val {
6835        Value::Scalar(n) => format!("{}\n", fmt_dlm_number(*n)),
6836        Value::Matrix(m) => {
6837            let mut out = String::new();
6838            for row in m.rows() {
6839                let parts: Vec<String> = row.iter().map(|n| fmt_dlm_number(*n)).collect();
6840                out.push_str(&parts.join(&delim));
6841                out.push('\n');
6842            }
6843            out
6844        }
6845        _ => {
6846            return Err("dlmwrite: second argument must be a numeric scalar or matrix".to_string());
6847        }
6848    };
6849
6850    std::fs::write(path, content).map_err(|e| format!("dlmwrite: cannot write '{path}': {e}"))?;
6851    Ok(Value::Void)
6852}
6853
6854// --- CSV read/write helpers (readmatrix / readtable / writetable) ---
6855
6856/// Selects the delimiter shared across all lines; falls back to `None` (whitespace splitting).
6857///
6858/// Uses CSV-aware splitting (quoting) when checking for comma consistency.
6859fn auto_detect_delim(lines: &[&str]) -> Option<String> {
6860    // Comma: use CSV-aware split so quoted fields with commas don't confuse the count.
6861    let comma_counts: Vec<usize> = lines.iter().map(|l| split_csv_row(l, ",").len()).collect();
6862    if comma_counts.iter().all(|&c| c > 1) && comma_counts.windows(2).all(|w| w[0] == w[1]) {
6863        return Some(",".to_string());
6864    }
6865    if delim_consistent(lines, '\t') {
6866        Some("\t".to_string())
6867    } else {
6868        None
6869    }
6870}
6871
6872/// Splits one CSV line by `delim`, respecting RFC 4180 double-quoted fields.
6873/// `""` inside a quoted field encodes a literal `"`.
6874/// Falls back to a plain `str::split` for multi-character delimiters.
6875fn split_csv_row(line: &str, delim: &str) -> Vec<String> {
6876    if delim.chars().count() != 1 {
6877        return line.split(delim).map(str::to_string).collect();
6878    }
6879    let delim_char = delim.chars().next().unwrap();
6880    let chars: Vec<char> = line.chars().collect();
6881    let mut fields: Vec<String> = Vec::new();
6882    let mut field = String::new();
6883    let mut i = 0;
6884    let mut in_quotes = false;
6885    while i < chars.len() {
6886        let c = chars[i];
6887        if in_quotes {
6888            if c == '"' && i + 1 < chars.len() && chars[i + 1] == '"' {
6889                field.push('"');
6890                i += 2;
6891                continue;
6892            } else if c == '"' {
6893                in_quotes = false;
6894            } else {
6895                field.push(c);
6896            }
6897        } else if c == '"' {
6898            in_quotes = true;
6899        } else if c == delim_char {
6900            fields.push(std::mem::take(&mut field));
6901        } else {
6902            field.push(c);
6903        }
6904        i += 1;
6905    }
6906    fields.push(field);
6907    fields
6908}
6909
6910/// Splits a CSV row with an optional delimiter; `None` splits by whitespace.
6911fn split_csv_row_opt(line: &str, delim: &Option<String>) -> Vec<String> {
6912    match delim {
6913        None => line.split_whitespace().map(str::to_string).collect(),
6914        Some(d) => split_csv_row(line, d),
6915    }
6916}
6917
6918/// Returns `true` if any non-empty field in `fields` cannot be parsed as `f64`.
6919fn row_is_header(fields: &[String]) -> bool {
6920    fields
6921        .iter()
6922        .any(|f| !f.trim().is_empty() && f.trim().parse::<f64>().is_err())
6923}
6924
6925/// Converts a raw header string to a valid identifier-like name.
6926/// Runs of non-alphanumeric characters collapse to `_`; a leading digit gets an `x` prefix.
6927/// Empty results fall back to `x{col}`.
6928fn sanitize_header(s: &str, col_1based: usize) -> String {
6929    let s = s.trim();
6930    if s.is_empty() {
6931        return format!("x{col_1based}");
6932    }
6933    let mut out = String::new();
6934    for c in s.chars() {
6935        if c.is_alphanumeric() || c == '_' {
6936            out.push(c);
6937        } else if !out.ends_with('_') {
6938            out.push('_');
6939        }
6940    }
6941    let out = out.trim_end_matches('_').to_string();
6942    if out.is_empty() {
6943        return format!("x{col_1based}");
6944    }
6945    if out.chars().next().unwrap().is_ascii_digit() {
6946        format!("x{out}")
6947    } else {
6948        out
6949    }
6950}
6951
6952/// Appends `_N` (1-based) suffixes to duplicate entries in a header list.
6953/// Note: collisions between deduplicated names and pre-existing `_N` names are not resolved.
6954fn deduplicate_headers(headers: Vec<String>) -> Vec<String> {
6955    let mut count: HashMap<String, usize> = HashMap::new();
6956    for h in &headers {
6957        *count.entry(h.clone()).or_insert(0) += 1;
6958    }
6959    let mut seen: HashMap<String, usize> = HashMap::new();
6960    headers
6961        .into_iter()
6962        .map(|h| {
6963            if *count.get(&h).unwrap() == 1 {
6964                h
6965            } else {
6966                let idx = seen.entry(h.clone()).or_insert(0);
6967                *idx += 1;
6968                format!("{h}_{idx}")
6969            }
6970        })
6971        .collect()
6972}
6973
6974/// Parses an optional `('Delimiter', d)` argument pair starting at `args[start]`.
6975/// Returns `Ok(None)` when no extra arguments are present.
6976fn parse_delimiter_opt(
6977    fn_name: &str,
6978    args: &[Value],
6979    start: usize,
6980) -> Result<Option<String>, String> {
6981    if args.len() <= start {
6982        return Ok(None);
6983    }
6984    let key = string_arg(&args[start], fn_name, start + 1)?;
6985    if !key.eq_ignore_ascii_case("delimiter") {
6986        return Err(format!(
6987            "{fn_name}: expected 'Delimiter' option at argument {}, got '{key}'",
6988            start + 1
6989        ));
6990    }
6991    if args.len() <= start + 1 {
6992        return Err(format!("{fn_name}: 'Delimiter' option requires a value"));
6993    }
6994    let val = interpret_delim(string_arg(&args[start + 1], fn_name, start + 2)?);
6995    Ok(Some(val))
6996}
6997
6998/// Reads a delimiter-separated file and returns a [`Value::Matrix`].
6999///
7000/// Auto-detects the delimiter (comma → tab → whitespace). When the first row contains
7001/// non-numeric text it is treated as a header and skipped. Empty cells become `NaN`
7002/// (unlike [`dlmread_impl`], which uses `0.0`).
7003fn readmatrix_impl(path: &str, explicit_delim: Option<String>) -> Result<Value, String> {
7004    let content = std::fs::read_to_string(path)
7005        .map_err(|e| format!("readmatrix: cannot read '{path}': {e}"))?;
7006
7007    let lines: Vec<&str> = content.lines().filter(|l| !l.trim().is_empty()).collect();
7008    if lines.is_empty() {
7009        return Ok(Value::Matrix(Array2::<f64>::zeros((0, 0))));
7010    }
7011
7012    let delim = match explicit_delim {
7013        Some(d) => Some(d),
7014        None => auto_detect_delim(&lines),
7015    };
7016
7017    let first_fields = split_csv_row_opt(lines[0], &delim);
7018    let skip_header = row_is_header(&first_fields);
7019    let data_lines = if skip_header { &lines[1..] } else { &lines[..] };
7020
7021    if data_lines.is_empty() {
7022        return Ok(Value::Matrix(Array2::<f64>::zeros((0, 0))));
7023    }
7024
7025    let mut rows: Vec<Vec<f64>> = Vec::new();
7026    for (i, line) in data_lines.iter().enumerate() {
7027        let fields = split_csv_row_opt(line, &delim);
7028        let mut row: Vec<f64> = Vec::with_capacity(fields.len());
7029        for f in &fields {
7030            let t = f.trim();
7031            if t.is_empty() {
7032                row.push(f64::NAN);
7033            } else {
7034                row.push(t.parse::<f64>().map_err(|_| {
7035                    format!(
7036                        "readmatrix: non-numeric value '{t}' on line {}",
7037                        i + 1 + usize::from(skip_header)
7038                    )
7039                })?);
7040            }
7041        }
7042        rows.push(row);
7043    }
7044
7045    if rows.is_empty() {
7046        return Ok(Value::Matrix(Array2::<f64>::zeros((0, 0))));
7047    }
7048
7049    let ncols = rows[0].len();
7050    for (i, row) in rows.iter().enumerate() {
7051        if row.len() != ncols {
7052            return Err(format!(
7053                "readmatrix: row {} has {} fields, expected {ncols}",
7054                i + 1,
7055                row.len()
7056            ));
7057        }
7058    }
7059
7060    let nrows = rows.len();
7061    let flat: Vec<f64> = rows.into_iter().flatten().collect();
7062    Array2::from_shape_vec((nrows, ncols), flat)
7063        .map_err(|e| format!("readmatrix: shape error: {e}"))
7064        .map(Value::Matrix)
7065}
7066
7067/// Reads a delimiter-separated file with a header row and returns a [`Value::Struct`] of columns.
7068///
7069/// The first row is always treated as column headers. Numeric columns become `Matrix` (N×1);
7070/// columns with any non-numeric cell become `Cell` of [`Value::Str`].
7071/// Whitespace is trimmed from all cell values after CSV unquoting.
7072fn readtable_impl(path: &str, explicit_delim: Option<String>) -> Result<Value, String> {
7073    let content = std::fs::read_to_string(path)
7074        .map_err(|e| format!("readtable: cannot read '{path}': {e}"))?;
7075
7076    let lines: Vec<&str> = content.lines().filter(|l| !l.trim().is_empty()).collect();
7077    if lines.is_empty() {
7078        return Ok(Value::Struct(IndexMap::new()));
7079    }
7080
7081    let delim = match explicit_delim {
7082        Some(d) => Some(d),
7083        None => auto_detect_delim(&lines),
7084    };
7085
7086    let raw_headers = split_csv_row_opt(lines[0], &delim);
7087    let ncols = raw_headers.len();
7088    let headers: Vec<String> = deduplicate_headers(
7089        raw_headers
7090            .iter()
7091            .enumerate()
7092            .map(|(i, h)| sanitize_header(h.trim(), i + 1))
7093            .collect(),
7094    );
7095
7096    let data_lines = &lines[1..];
7097    if data_lines.is_empty() {
7098        let mut s: IndexMap<String, Value> = IndexMap::new();
7099        for h in &headers {
7100            s.insert(h.clone(), Value::Matrix(Array2::<f64>::zeros((0, 1))));
7101        }
7102        return Ok(Value::Struct(s));
7103    }
7104
7105    let mut all_rows: Vec<Vec<String>> = Vec::new();
7106    for (i, line) in data_lines.iter().enumerate() {
7107        let fields = split_csv_row_opt(line, &delim);
7108        if fields.len() != ncols {
7109            return Err(format!(
7110                "readtable: row {} has {} fields, expected {ncols}",
7111                i + 2,
7112                fields.len()
7113            ));
7114        }
7115        all_rows.push(fields.into_iter().map(|f| f.trim().to_string()).collect());
7116    }
7117
7118    let nrows = all_rows.len();
7119    let mut s: IndexMap<String, Value> = IndexMap::new();
7120    for col in 0..ncols {
7121        let all_numeric = all_rows.iter().all(|row| {
7122            let t = row[col].as_str();
7123            t.is_empty() || t.parse::<f64>().is_ok()
7124        });
7125        if all_numeric {
7126            let vals: Vec<f64> = all_rows
7127                .iter()
7128                .map(|row| {
7129                    let t = row[col].as_str();
7130                    if t.is_empty() {
7131                        f64::NAN
7132                    } else {
7133                        t.parse::<f64>().unwrap()
7134                    }
7135                })
7136                .collect();
7137            let col_mat = Array2::from_shape_vec((nrows, 1), vals)
7138                .map_err(|e| format!("readtable: shape error: {e}"))?;
7139            s.insert(headers[col].clone(), Value::Matrix(col_mat));
7140        } else {
7141            let vals: Vec<Value> = all_rows
7142                .iter()
7143                .map(|row| Value::Str(row[col].clone()))
7144                .collect();
7145            s.insert(headers[col].clone(), Value::Cell(vals));
7146        }
7147    }
7148    Ok(Value::Struct(s))
7149}
7150
7151/// Quotes a CSV cell if it contains the delimiter, a double-quote, or a newline (RFC 4180).
7152fn csv_quote_cell(s: &str, delim: &str) -> String {
7153    if s.contains('"') || s.contains('\n') || s.contains(delim) {
7154        let escaped = s.replace('"', "\"\"");
7155        format!("\"{escaped}\"")
7156    } else {
7157        s.to_string()
7158    }
7159}
7160
7161/// Returns the number of rows in a struct column value.
7162///
7163/// Accepts `Matrix` (N×1), `Cell`, `Scalar`, and `Str`/`StringObj` (1 row each).
7164/// Returns `None` for unsupported types or non-column matrices.
7165fn col_nrows(v: &Value) -> Option<usize> {
7166    match v {
7167        Value::Matrix(m) if m.ncols() == 1 || m.nrows() == 0 => Some(m.nrows()),
7168        Value::Cell(c) => Some(c.len()),
7169        Value::Scalar(_) => Some(1),
7170        Value::Str(_) | Value::StringObj(_) => Some(1),
7171        _ => None,
7172    }
7173}
7174
7175/// Returns the formatted CSV cell string for row `row` of a struct column value.
7176fn col_cell_str(v: &Value, row: usize, delim: &str) -> Result<String, String> {
7177    match v {
7178        Value::Matrix(m) => Ok(csv_quote_cell(&fmt_dlm_number(m[[row, 0]]), delim)),
7179        Value::Cell(c) => match &c[row] {
7180            Value::Str(s) | Value::StringObj(s) => Ok(csv_quote_cell(s, delim)),
7181            Value::Scalar(n) => Ok(csv_quote_cell(&fmt_dlm_number(*n), delim)),
7182            _ => Err(format!(
7183                "writetable: cell element at row {} has unsupported type",
7184                row + 1
7185            )),
7186        },
7187        Value::Scalar(n) => Ok(csv_quote_cell(&fmt_dlm_number(*n), delim)),
7188        Value::Str(s) | Value::StringObj(s) => Ok(csv_quote_cell(s, delim)),
7189        _ => Err(format!(
7190            "writetable: unsupported column type at row {}",
7191            row + 1
7192        )),
7193    }
7194}
7195
7196/// Writes a struct table to a delimiter-separated file with a header row.
7197///
7198/// Each struct field is one column. All fields must have the same number of rows.
7199/// Accepts `Matrix` (N×1), `Cell`, `Scalar`, and `Str`/`StringObj` columns.
7200/// Cell values that contain the delimiter, `"`, or newlines are quoted per RFC 4180.
7201fn writetable_impl(
7202    tbl: &Value,
7203    path: &str,
7204    explicit_delim: Option<String>,
7205) -> Result<Value, String> {
7206    let delim = explicit_delim.unwrap_or_else(|| ",".to_string());
7207    let fields = match tbl {
7208        Value::Struct(m) => m,
7209        _ => return Err("writetable: first argument must be a struct".to_string()),
7210    };
7211    if fields.is_empty() {
7212        std::fs::write(path, "").map_err(|e| format!("writetable: cannot write '{path}': {e}"))?;
7213        return Ok(Value::Void);
7214    }
7215
7216    let nrows = {
7217        let (first_name, first_val) = fields.iter().next().unwrap();
7218        col_nrows(first_val).ok_or_else(|| {
7219            format!("writetable: column '{first_name}' must be a Matrix (N×1), Cell, or scalar")
7220        })?
7221    };
7222    for (cname, cval) in fields.iter() {
7223        let n = col_nrows(cval).ok_or_else(|| {
7224            format!("writetable: column '{cname}' must be a Matrix (N×1), Cell, or scalar")
7225        })?;
7226        if n != nrows {
7227            return Err(format!(
7228                "writetable: column '{cname}' has {n} rows, expected {nrows}"
7229            ));
7230        }
7231    }
7232
7233    let mut out = String::new();
7234    let header_parts: Vec<String> = fields.keys().map(|k| csv_quote_cell(k, &delim)).collect();
7235    out.push_str(&header_parts.join(&delim));
7236    out.push('\n');
7237
7238    for row in 0..nrows {
7239        let mut parts: Vec<String> = Vec::with_capacity(fields.len());
7240        for cval in fields.values() {
7241            parts.push(col_cell_str(cval, row, &delim)?);
7242        }
7243        out.push_str(&parts.join(&delim));
7244        out.push('\n');
7245    }
7246
7247    std::fs::write(path, out).map_err(|e| format!("writetable: cannot write '{path}': {e}"))?;
7248    Ok(Value::Void)
7249}
7250
7251/// Converts an f64 to u64 for bitwise operations.
7252/// Requires a non-negative integer value; returns an error otherwise.
7253fn to_bits(v: f64, fname: &str, pos: usize) -> Result<u64, String> {
7254    if v < 0.0 {
7255        return Err(format!(
7256            "{fname}: argument {pos} must be non-negative, got {v}"
7257        ));
7258    }
7259    if v.fract() != 0.0 {
7260        return Err(format!(
7261            "{fname}: argument {pos} must be an integer, got {v}"
7262        ));
7263    }
7264    if v > u64::MAX as f64 {
7265        return Err(format!(
7266            "{fname}: argument {pos} is too large for bitwise operations"
7267        ));
7268    }
7269    Ok(v as u64)
7270}
7271
7272/// Computes determinant of a square matrix via Gaussian elimination.
7273/// Computes the determinant of a square matrix via Gaussian elimination with
7274/// partial pivoting (pure Rust, no external dependencies).
7275fn det_matrix(m: &Array2<f64>) -> Result<f64, String> {
7276    let n = m.nrows();
7277    if m.ncols() != n {
7278        return Err("det: matrix must be square".to_string());
7279    }
7280    if n == 0 {
7281        return Ok(1.0);
7282    }
7283    let mut a = m.clone();
7284    let mut sign: f64 = 1.0;
7285    for col in 0..n {
7286        // Partial pivoting: swap in the row with the largest absolute value.
7287        let pivot = (col..n)
7288            .max_by(|&r1, &r2| a[[r1, col]].abs().partial_cmp(&a[[r2, col]].abs()).unwrap())
7289            .unwrap();
7290        if a[[pivot, col]].abs() < 1e-15 {
7291            return Ok(0.0); // singular
7292        }
7293        if pivot != col {
7294            for j in 0..n {
7295                let tmp = a[[pivot, j]];
7296                a[[pivot, j]] = a[[col, j]];
7297                a[[col, j]] = tmp;
7298            }
7299            sign = -sign;
7300        }
7301        let pv = a[[col, col]];
7302        for row in (col + 1)..n {
7303            let factor = a[[row, col]] / pv;
7304            for j in col..n {
7305                let val = a[[col, j]] * factor;
7306                a[[row, j]] -= val;
7307            }
7308        }
7309    }
7310    Ok(sign * (0..n).map(|i| a[[i, i]]).product::<f64>())
7311}
7312
7313/// Computes the inverse of a square matrix via Gauss-Jordan elimination with
7314/// partial pivoting (pure Rust, no external dependencies).
7315fn inv_matrix(m: &Array2<f64>) -> Result<Array2<f64>, String> {
7316    let n = m.nrows();
7317    if m.ncols() != n {
7318        return Err("inv: matrix must be square".to_string());
7319    }
7320    let cols = 2 * n;
7321    let mut aug = vec![0.0f64; n * cols];
7322    for i in 0..n {
7323        for j in 0..n {
7324            aug[i * cols + j] = m[[i, j]];
7325        }
7326        aug[i * cols + n + i] = 1.0;
7327    }
7328    for col in 0..n {
7329        // Partial pivoting: swap in the row with the largest absolute value.
7330        let pivot = (col..n)
7331            .max_by(|&r1, &r2| {
7332                aug[r1 * cols + col]
7333                    .abs()
7334                    .partial_cmp(&aug[r2 * cols + col].abs())
7335                    .unwrap()
7336            })
7337            .filter(|&r| aug[r * cols + col].abs() > 1e-12)
7338            .ok_or_else(|| "inv: matrix is singular".to_string())?;
7339        if pivot != col {
7340            for j in 0..cols {
7341                aug.swap(col * cols + j, pivot * cols + j);
7342            }
7343        }
7344        let pv = aug[col * cols + col];
7345        for j in 0..cols {
7346            aug[col * cols + j] /= pv;
7347        }
7348        for row in 0..n {
7349            if row == col {
7350                continue;
7351            }
7352            let factor = aug[row * cols + col];
7353            for j in 0..cols {
7354                let val = aug[col * cols + j] * factor;
7355                aug[row * cols + j] -= val;
7356            }
7357        }
7358    }
7359    let mut result = Array2::<f64>::zeros((n, n));
7360    for i in 0..n {
7361        for j in 0..n {
7362            result[[i, j]] = aug[i * cols + n + j];
7363        }
7364    }
7365    Ok(result)
7366}
7367
7368/// Solves the linear system `A * x = B` using Gaussian elimination with partial pivoting.
7369///
7370/// `A` must be square (n×n); `B` must have n rows. Returns x (n × k where k = B.ncols()).
7371/// This is the engine for the `\` left-division operator.
7372fn solve_linear(a: &Array2<f64>, b: &Array2<f64>) -> Result<Array2<f64>, String> {
7373    let n = a.nrows();
7374    if a.ncols() != n {
7375        return Err(format!(
7376            "\\: coefficient matrix must be square, got {}×{}",
7377            n,
7378            a.ncols()
7379        ));
7380    }
7381    let k = b.ncols();
7382    if b.nrows() != n {
7383        return Err(format!(
7384            "\\: size mismatch — A is {}×{} but b has {} rows",
7385            n,
7386            n,
7387            b.nrows()
7388        ));
7389    }
7390    if n == 0 {
7391        return Ok(Array2::zeros((0, k)));
7392    }
7393    let cols = n + k;
7394    let mut aug = vec![0.0f64; n * cols];
7395    for i in 0..n {
7396        for j in 0..n {
7397            aug[i * cols + j] = a[[i, j]];
7398        }
7399        for j in 0..k {
7400            aug[i * cols + n + j] = b[[i, j]];
7401        }
7402    }
7403    for col in 0..n {
7404        let pivot = (col..n)
7405            .max_by(|&r1, &r2| {
7406                aug[r1 * cols + col]
7407                    .abs()
7408                    .partial_cmp(&aug[r2 * cols + col].abs())
7409                    .unwrap()
7410            })
7411            .filter(|&r| aug[r * cols + col].abs() > 1e-12)
7412            .ok_or_else(|| "\\: matrix is singular or nearly singular".to_string())?;
7413        if pivot != col {
7414            for j in 0..cols {
7415                aug.swap(col * cols + j, pivot * cols + j);
7416            }
7417        }
7418        let pv = aug[col * cols + col];
7419        for j in col..cols {
7420            aug[col * cols + j] /= pv;
7421        }
7422        for row in 0..n {
7423            if row == col {
7424                continue;
7425            }
7426            let factor = aug[row * cols + col];
7427            if factor == 0.0 {
7428                continue;
7429            }
7430            for j in col..cols {
7431                let val = aug[col * cols + j] * factor;
7432                aug[row * cols + j] -= val;
7433            }
7434        }
7435    }
7436    let mut result = Array2::<f64>::zeros((n, k));
7437    for i in 0..n {
7438        for j in 0..k {
7439            result[[i, j]] = aug[i * cols + n + j];
7440        }
7441    }
7442    Ok(result)
7443}
7444
7445// ---------------------------------------------------------------------------
7446// Advanced linear algebra helpers (Phase 18)
7447// ---------------------------------------------------------------------------
7448
7449/// QR decomposition via Householder reflectors.
7450///
7451/// For an m×n matrix A returns (Q, R) where Q is m×m orthogonal and R is
7452/// m×n upper triangular such that A = Q * R.
7453fn qr_decompose(a: &Array2<f64>) -> Result<(Array2<f64>, Array2<f64>), String> {
7454    let m = a.nrows();
7455    let n = a.ncols();
7456    let k = m.min(n);
7457    let mut r = a.clone();
7458    let mut q = Array2::<f64>::eye(m);
7459
7460    for j in 0..k {
7461        let col_len = m - j;
7462        let mut v: Vec<f64> = (j..m).map(|i| r[[i, j]]).collect();
7463
7464        let norm_x = v.iter().map(|&x| x * x).sum::<f64>().sqrt();
7465        if norm_x < 1e-14 {
7466            continue;
7467        }
7468        // Householder sign convention avoids cancellation.
7469        v[0] += if v[0] >= 0.0 { norm_x } else { -norm_x };
7470        let v_sq: f64 = v.iter().map(|&x| x * x).sum();
7471        if v_sq < 1e-28 {
7472            continue;
7473        }
7474
7475        // Apply H from left to R: R[j:m, :] -= 2*v*(v^T*R[j:m,:])/v^Tv
7476        for col in j..n {
7477            let dot: f64 = (0..col_len).map(|i| v[i] * r[[j + i, col]]).sum();
7478            let fac = 2.0 * dot / v_sq;
7479            for i in 0..col_len {
7480                r[[j + i, col]] -= fac * v[i];
7481            }
7482        }
7483        // Accumulate Q from right: Q[:, j:m] -= (Q[:,j:m]*v) * 2*v^T/v^Tv
7484        for row in 0..m {
7485            let dot: f64 = (0..col_len).map(|i| q[[row, j + i]] * v[i]).sum();
7486            let fac = 2.0 * dot / v_sq;
7487            for i in 0..col_len {
7488                q[[row, j + i]] -= fac * v[i];
7489            }
7490        }
7491    }
7492
7493    Ok((q, r))
7494}
7495
7496/// LU decomposition with partial pivoting.
7497///
7498/// For an n×n square matrix A returns (L, U, P) where P*A = L*U,
7499/// L is unit lower triangular, U is upper triangular, and P is a
7500/// permutation matrix.
7501type LuResult = Result<(Array2<f64>, Array2<f64>, Array2<f64>), String>;
7502fn lu_decompose(a: &Array2<f64>) -> LuResult {
7503    let n = a.nrows();
7504    if a.ncols() != n {
7505        return Err("lu: matrix must be square".to_string());
7506    }
7507    let mut u = a.clone();
7508    let mut l = Array2::<f64>::eye(n);
7509    let mut perm: Vec<usize> = (0..n).collect();
7510
7511    for j in 0..n {
7512        let pivot = (j..n)
7513            .max_by(|&r1, &r2| {
7514                u[[r1, j]]
7515                    .abs()
7516                    .partial_cmp(&u[[r2, j]].abs())
7517                    .unwrap_or(std::cmp::Ordering::Equal)
7518            })
7519            .unwrap();
7520
7521        if pivot != j {
7522            for col in 0..n {
7523                let tmp = u[[j, col]];
7524                u[[j, col]] = u[[pivot, col]];
7525                u[[pivot, col]] = tmp;
7526            }
7527            for col in 0..j {
7528                let tmp = l[[j, col]];
7529                l[[j, col]] = l[[pivot, col]];
7530                l[[pivot, col]] = tmp;
7531            }
7532            perm.swap(j, pivot);
7533        }
7534
7535        if u[[j, j]].abs() < 1e-15 {
7536            continue;
7537        }
7538        for i in (j + 1)..n {
7539            l[[i, j]] = u[[i, j]] / u[[j, j]];
7540            for k in j..n {
7541                let val = l[[i, j]] * u[[j, k]];
7542                u[[i, k]] -= val;
7543            }
7544        }
7545    }
7546
7547    let mut p = Array2::<f64>::zeros((n, n));
7548    for (i, &j) in perm.iter().enumerate() {
7549        p[[i, j]] = 1.0;
7550    }
7551    Ok((l, u, p))
7552}
7553
7554/// Cholesky decomposition.
7555///
7556/// For a symmetric positive-definite n×n matrix A returns the upper triangular
7557/// factor R such that A = R^T * R (MATLAB convention).
7558fn chol_decompose(a: &Array2<f64>) -> Result<Array2<f64>, String> {
7559    let n = a.nrows();
7560    if a.ncols() != n {
7561        return Err("chol: matrix must be square".to_string());
7562    }
7563    let mut r = Array2::<f64>::zeros((n, n));
7564    for j in 0..n {
7565        let mut s = a[[j, j]];
7566        for k in 0..j {
7567            s -= r[[k, j]] * r[[k, j]];
7568        }
7569        if s <= 0.0 {
7570            return Err("chol: matrix is not positive definite".to_string());
7571        }
7572        r[[j, j]] = s.sqrt();
7573        for i in (j + 1)..n {
7574            let mut t = a[[j, i]];
7575            for k in 0..j {
7576                t -= r[[k, j]] * r[[k, i]];
7577            }
7578            r[[j, i]] = t / r[[j, j]];
7579        }
7580    }
7581    Ok(r)
7582}
7583
7584/// One-sided Jacobi SVD (economy form).
7585///
7586/// For an m×n matrix A returns (U, s, V) where
7587/// - U is m×k with orthonormal columns (k = min(m,n))
7588/// - s is a `Vec<f64>` of singular values in descending order (length k)
7589/// - V is n×k with orthonormal columns
7590///
7591/// For m < n the inputs are transparently transposed and outputs swapped.
7592type SvdResult = Result<(Array2<f64>, Vec<f64>, Array2<f64>), String>;
7593fn svd_compute(a: &Array2<f64>) -> SvdResult {
7594    let m = a.nrows();
7595    let n = a.ncols();
7596    if m < n {
7597        let (v, s, u) = svd_compute(&a.t().to_owned())?;
7598        return Ok((u, s, v));
7599    }
7600    // m >= n from here.
7601    let k = n;
7602    let mut b = a.clone();
7603    let mut v = Array2::<f64>::eye(k);
7604
7605    const MAX_ITER: usize = 200;
7606    const EPS: f64 = 1e-14;
7607
7608    'outer: for _ in 0..MAX_ITER {
7609        let mut changed = false;
7610        for p in 0..k {
7611            for q in (p + 1)..k {
7612                let alpha: f64 = (0..m).map(|i| b[[i, p]] * b[[i, p]]).sum();
7613                let beta: f64 = (0..m).map(|i| b[[i, q]] * b[[i, q]]).sum();
7614                let gamma: f64 = (0..m).map(|i| b[[i, p]] * b[[i, q]]).sum();
7615
7616                if gamma.abs() <= EPS * (alpha * beta).sqrt() {
7617                    continue;
7618                }
7619                changed = true;
7620
7621                let zeta = (beta - alpha) / (2.0 * gamma);
7622                let t = zeta.signum() / (zeta.abs() + (1.0 + zeta * zeta).sqrt());
7623                let c = 1.0 / (1.0 + t * t).sqrt();
7624                let s = c * t;
7625
7626                for i in 0..m {
7627                    let bp = b[[i, p]];
7628                    let bq = b[[i, q]];
7629                    b[[i, p]] = c * bp - s * bq;
7630                    b[[i, q]] = s * bp + c * bq;
7631                }
7632                for i in 0..k {
7633                    let vp = v[[i, p]];
7634                    let vq = v[[i, q]];
7635                    v[[i, p]] = c * vp - s * vq;
7636                    v[[i, q]] = s * vp + c * vq;
7637                }
7638            }
7639        }
7640        if !changed {
7641            break 'outer;
7642        }
7643    }
7644
7645    let mut sigma: Vec<f64> = (0..k)
7646        .map(|j| (0..m).map(|i| b[[i, j]] * b[[i, j]]).sum::<f64>().sqrt())
7647        .collect();
7648    let mut u_mat = Array2::<f64>::zeros((m, k));
7649    for j in 0..k {
7650        if sigma[j] > EPS {
7651            for i in 0..m {
7652                u_mat[[i, j]] = b[[i, j]] / sigma[j];
7653            }
7654        }
7655    }
7656
7657    // Sort descending by singular value.
7658    let mut order: Vec<usize> = (0..k).collect();
7659    order.sort_by(|&a, &b| {
7660        sigma[b]
7661            .partial_cmp(&sigma[a])
7662            .unwrap_or(std::cmp::Ordering::Equal)
7663    });
7664    let sigma_s: Vec<f64> = order.iter().map(|&i| sigma[i]).collect();
7665    let mut u_s = Array2::<f64>::zeros((m, k));
7666    let mut v_s = Array2::<f64>::zeros((n, k));
7667    for (ni, &oi) in order.iter().enumerate() {
7668        for r in 0..m {
7669            u_s[[r, ni]] = u_mat[[r, oi]];
7670        }
7671        for r in 0..k {
7672            v_s[[r, ni]] = v[[r, oi]];
7673        }
7674    }
7675    sigma = sigma_s;
7676
7677    Ok((u_s, sigma, v_s))
7678}
7679
7680/// Extends an m×k matrix with orthonormal columns to a full m×m orthogonal matrix.
7681///
7682/// Tries each standard basis vector e_0..e_{m-1} in order; keeps those that
7683/// have non-negligible component orthogonal to the existing basis.
7684fn complete_orthonormal_basis(u: &Array2<f64>) -> Array2<f64> {
7685    let m = u.nrows();
7686    let k = u.ncols();
7687    let mut basis: Vec<Vec<f64>> = (0..k).map(|j| u.column(j).to_vec()).collect();
7688
7689    let mut ei = 0usize;
7690    while basis.len() < m && ei < m {
7691        let mut v: Vec<f64> = vec![0.0; m];
7692        v[ei] = 1.0;
7693        ei += 1;
7694        for b in &basis {
7695            let dot: f64 = v.iter().zip(b.iter()).map(|(&a, &b)| a * b).sum();
7696            for (vi, &bi) in v.iter_mut().zip(b.iter()) {
7697                *vi -= dot * bi;
7698            }
7699        }
7700        let norm = v.iter().map(|&x| x * x).sum::<f64>().sqrt();
7701        if norm > 1e-10 {
7702            for vi in &mut v {
7703                *vi /= norm;
7704            }
7705            basis.push(v);
7706        }
7707    }
7708
7709    let mut result = Array2::<f64>::zeros((m, m));
7710    for (j, b) in basis.iter().enumerate() {
7711        for (i, &val) in b.iter().enumerate() {
7712            result[[i, j]] = val;
7713        }
7714    }
7715    result
7716}
7717
7718/// QR-iteration eigendecomposition for a real square matrix.
7719///
7720/// Returns `(eigenvalues, eigenvectors)` where eigenvalues is a `Vec<f64>` of
7721/// length n and eigenvectors is an n×n matrix whose columns are the eigenvectors.
7722/// Uses the basic QR iteration with a simple diagonal shift (Wilkinson-style).
7723/// Convergence is reliable for symmetric matrices; general matrices converge for
7724/// most well-conditioned inputs within `MAX_ITER` steps.
7725fn eig_compute(a: &Array2<f64>) -> Result<(Vec<Complex<f64>>, Array2<f64>), String> {
7726    let n = a.nrows();
7727    if a.ncols() != n {
7728        return Err("eig: matrix must be square".to_string());
7729    }
7730    if n == 0 {
7731        return Ok((vec![], Array2::zeros((0, 0))));
7732    }
7733    if n == 1 {
7734        return Ok((vec![Complex::new(a[[0, 0]], 0.0)], Array2::eye(1)));
7735    }
7736
7737    let mut ak = a.clone();
7738    let mut evecs = Array2::<f64>::eye(n);
7739
7740    const MAX_ITER: usize = 2000;
7741    const EPS: f64 = 1e-12;
7742
7743    for _ in 0..MAX_ITER {
7744        // Wilkinson shift: uses the trailing 2×2 submatrix for cubic convergence.
7745        let mu = {
7746            let d = ak[[n - 1, n - 1]];
7747            if n >= 2 {
7748                let a = ak[[n - 2, n - 2]];
7749                let b = ak[[n - 2, n - 1]];
7750                let delta = (a - d) / 2.0;
7751                if delta.abs() < 1e-30 {
7752                    d - b.abs()
7753                } else {
7754                    d - b * b / (delta + delta.signum() * (delta * delta + b * b).sqrt())
7755                }
7756            } else {
7757                d
7758            }
7759        };
7760
7761        for i in 0..n {
7762            ak[[i, i]] -= mu;
7763        }
7764        let (q, r) = qr_decompose(&ak)?;
7765        ak = r.dot(&q);
7766        for i in 0..n {
7767            ak[[i, i]] += mu;
7768        }
7769        evecs = evecs.dot(&q);
7770
7771        // Convergence check: all sub-diagonals small (real eigenvalues only).
7772        let max_sub = (0..(n - 1))
7773            .map(|i| ak[[i + 1, i]].abs())
7774            .fold(0.0_f64, f64::max);
7775        if max_sub < EPS {
7776            break;
7777        }
7778    }
7779
7780    // Post-convergence scan: extract complex conjugate pairs from 2×2 quasi-triangular blocks.
7781    // A sub-diagonal entry larger than EPS_BLOCK indicates a complex eigenvalue pair.
7782    const EPS_BLOCK: f64 = 1e-8;
7783    let mut evals: Vec<Complex<f64>> = Vec::with_capacity(n);
7784    let mut i = 0;
7785    while i < n {
7786        if i + 1 < n && ak[[i + 1, i]].abs() > EPS_BLOCK {
7787            let (a_ii, b, c, d_ii) = (
7788                ak[[i, i]],
7789                ak[[i, i + 1]],
7790                ak[[i + 1, i]],
7791                ak[[i + 1, i + 1]],
7792            );
7793            let p = (a_ii + d_ii) / 2.0;
7794            let disc = ((a_ii - d_ii) / 2.0).powi(2) + b * c;
7795            if disc < 0.0 {
7796                let q = (-disc).sqrt();
7797                evals.push(Complex::new(p, q));
7798                evals.push(Complex::new(p, -q));
7799            } else {
7800                let q = disc.sqrt();
7801                evals.push(Complex::new(p + q, 0.0));
7802                evals.push(Complex::new(p - q, 0.0));
7803            }
7804            i += 2;
7805        } else {
7806            evals.push(Complex::new(ak[[i, i]], 0.0));
7807            i += 1;
7808        }
7809    }
7810
7811    Ok((evals, evecs))
7812}
7813
7814// ---------------------------------------------------------------------------
7815// Indexing
7816// ---------------------------------------------------------------------------
7817
7818/// Creates a copy of `env` with `end` set to `dim_size`.
7819/// Used by `eval_index` so that `end` in index expressions resolves to the correct dimension size.
7820fn env_with_end(env: &Env, dim_size: usize) -> Env {
7821    let mut e = env.clone();
7822    e.insert("end".to_string(), Value::Scalar(dim_size as f64));
7823    e
7824}
7825
7826/// Returns `true` if `expr` (or any sub-expression) references the identifier `end`.
7827///
7828/// Used to skip the full [`Env`] clone inside [`eval_index`] when `end` is absent.
7829pub(crate) fn contains_end(expr: &Expr) -> bool {
7830    match expr {
7831        Expr::Var(s) => s == "end",
7832        Expr::Number(_)
7833        | Expr::Colon
7834        | Expr::StrLiteral(_)
7835        | Expr::StringObjLiteral(_)
7836        | Expr::NaT
7837        | Expr::FuncHandle(_) => false,
7838        Expr::UnaryMinus(e)
7839        | Expr::UnaryNot(e)
7840        | Expr::Transpose(e)
7841        | Expr::PlainTranspose(e)
7842        | Expr::FieldGet(e, _) => contains_end(e),
7843        Expr::BinOp(l, _, r) => contains_end(l) || contains_end(r),
7844        Expr::Call(_, args) | Expr::DotCall(_, args) => args.iter().any(contains_end),
7845        Expr::Matrix(rows) => rows.iter().flat_map(|r| r.iter()).any(contains_end),
7846        Expr::Range(a, step, b) => {
7847            contains_end(a) || step.as_deref().is_some_and(contains_end) || contains_end(b)
7848        }
7849        Expr::Lambda { body, .. } => contains_end(body),
7850        Expr::CellLiteral(elems) => elems.iter().any(contains_end),
7851        Expr::CellIndex(a, b) => contains_end(a) || contains_end(b),
7852    }
7853}
7854
7855/// Evaluates `val(args...)` — indexing a variable with one or two index arguments.
7856///
7857/// Disambiguation rule (Octave semantics): a name that exists in `Env` is always
7858/// treated as a variable to be indexed, never as a function call.
7859fn eval_index(val: &Value, args: &[Expr], env: &Env) -> Result<Value, String> {
7860    match args.len() {
7861        0 => Err("Indexing requires at least one index".to_string()),
7862        1 => {
7863            // v(i), v(1:3), v(:), v(end), v(end-1:end)
7864            match val {
7865                Value::Void => Err("Cannot index into void".to_string()),
7866                Value::Lambda(_) | Value::Function { .. } | Value::Tuple(_) => {
7867                    Err("Cannot index into a function value".to_string())
7868                }
7869                Value::Cell(_) => Err("Use c{i} to index into a cell array, not c(i)".to_string()),
7870                Value::Struct(_) => {
7871                    Err("Use s.field to access struct fields, not s(i)".to_string())
7872                }
7873                Value::StructArray(arr) => {
7874                    let total = arr.len();
7875                    let _owned_env;
7876                    let env1: &Env = if contains_end(&args[0]) {
7877                        _owned_env = env_with_end(env, total);
7878                        &_owned_env
7879                    } else {
7880                        env
7881                    };
7882                    match resolve_dim(&args[0], total, env1)? {
7883                        DimIdx::All => {
7884                            // s(:) — return all elements as a new struct array
7885                            Ok(Value::StructArray(arr.clone()))
7886                        }
7887                        DimIdx::Indices(idxs) => {
7888                            if idxs.len() == 1 {
7889                                let i = idxs[0];
7890                                if i >= total {
7891                                    return Err(format!(
7892                                        "Index {} out of range (1..{})",
7893                                        i + 1,
7894                                        total
7895                                    ));
7896                                }
7897                                Ok(Value::Struct(arr[i].clone()))
7898                            } else {
7899                                let mut selected = Vec::with_capacity(idxs.len());
7900                                for &i in &idxs {
7901                                    if i >= total {
7902                                        return Err(format!(
7903                                            "Index {} out of range (1..{})",
7904                                            i + 1,
7905                                            total
7906                                        ));
7907                                    }
7908                                    selected.push(arr[i].clone());
7909                                }
7910                                Ok(Value::StructArray(selected))
7911                            }
7912                        }
7913                    }
7914                }
7915                Value::Scalar(n) => {
7916                    let _owned_env;
7917                    let env1: &Env = if contains_end(&args[0]) {
7918                        _owned_env = env_with_end(env, 1);
7919                        &_owned_env
7920                    } else {
7921                        env
7922                    };
7923                    match resolve_dim(&args[0], 1, env1)? {
7924                        DimIdx::All | DimIdx::Indices(_) => Ok(Value::Scalar(*n)),
7925                    }
7926                }
7927                Value::Complex(re, im) => {
7928                    let _owned_env;
7929                    let env1: &Env = if contains_end(&args[0]) {
7930                        _owned_env = env_with_end(env, 1);
7931                        &_owned_env
7932                    } else {
7933                        env
7934                    };
7935                    match resolve_dim(&args[0], 1, env1)? {
7936                        DimIdx::All | DimIdx::Indices(_) => Ok(Value::Complex(*re, *im)),
7937                    }
7938                }
7939                Value::ComplexMatrix(m) => {
7940                    let total = m.nrows() * m.ncols();
7941                    let _owned_env;
7942                    let env1: &Env = if contains_end(&args[0]) {
7943                        _owned_env = env_with_end(env, total);
7944                        &_owned_env
7945                    } else {
7946                        env
7947                    };
7948                    match resolve_dim(&args[0], total, env1)? {
7949                        DimIdx::All => {
7950                            // A(:) → column vector (column-major), as ComplexMatrix
7951                            let mut flat: Vec<Complex<f64>> = Vec::with_capacity(total);
7952                            for col in 0..m.ncols() {
7953                                for row in 0..m.nrows() {
7954                                    flat.push(m[[row, col]]);
7955                                }
7956                            }
7957                            Ok(Value::ComplexMatrix(
7958                                Array2::from_shape_vec((total, 1), flat).unwrap(),
7959                            ))
7960                        }
7961                        DimIdx::Indices(idxs) => {
7962                            let nrows = m.nrows();
7963                            let ncols_m = m.ncols();
7964                            let vals: Result<Vec<Complex<f64>>, String> = idxs
7965                                .iter()
7966                                .map(|&i| {
7967                                    let row = i % nrows;
7968                                    let col = i / nrows;
7969                                    if col >= ncols_m {
7970                                        Err(format!("Index {} out of range (1..{})", i + 1, total))
7971                                    } else {
7972                                        Ok(m[[row, col]])
7973                                    }
7974                                })
7975                                .collect();
7976                            let vals = vals?;
7977                            if vals.len() == 1 {
7978                                let c = vals[0];
7979                                Ok(make_complex(c.re, c.im))
7980                            } else {
7981                                let n = vals.len();
7982                                Ok(Value::ComplexMatrix(
7983                                    Array2::from_shape_vec((1, n), vals).unwrap(),
7984                                ))
7985                            }
7986                        }
7987                    }
7988                }
7989                Value::Matrix(m) => {
7990                    let total = m.nrows() * m.ncols();
7991                    let _owned_env;
7992                    let env1: &Env = if contains_end(&args[0]) {
7993                        _owned_env = env_with_end(env, total);
7994                        &_owned_env
7995                    } else {
7996                        env
7997                    };
7998                    match resolve_dim(&args[0], total, env1)? {
7999                        DimIdx::All => {
8000                            // A(:) → column vector, column-major order
8001                            let mut flat = Vec::with_capacity(total);
8002                            for col in 0..m.ncols() {
8003                                for row in 0..m.nrows() {
8004                                    flat.push(m[[row, col]]);
8005                                }
8006                            }
8007                            Ok(Value::Matrix(
8008                                Array2::from_shape_vec((total, 1), flat).unwrap(),
8009                            ))
8010                        }
8011                        DimIdx::Indices(idxs) => {
8012                            // Column-major linear indexing
8013                            let nrows = m.nrows();
8014                            let ncols_m = m.ncols();
8015                            let vals: Result<Vec<f64>, String> = idxs
8016                                .iter()
8017                                .map(|&i| {
8018                                    // i is 0-based, column-major
8019                                    let row = i % nrows;
8020                                    let col = i / nrows;
8021                                    if col >= ncols_m {
8022                                        Err(format!("Index {} out of range (1..{})", i + 1, total))
8023                                    } else {
8024                                        Ok(m[[row, col]])
8025                                    }
8026                                })
8027                                .collect();
8028                            let vals = vals?;
8029                            if vals.len() == 1 {
8030                                Ok(Value::Scalar(vals[0]))
8031                            } else {
8032                                let n = vals.len();
8033                                Ok(Value::Matrix(Array2::from_shape_vec((1, n), vals).unwrap()))
8034                            }
8035                        }
8036                    }
8037                }
8038                Value::Str(s) => {
8039                    // Index into a char array — returns char code(s)
8040                    let chars: Vec<char> = s.chars().collect();
8041                    let total = chars.len();
8042                    let _owned_env;
8043                    let env1: &Env = if contains_end(&args[0]) {
8044                        _owned_env = env_with_end(env, total);
8045                        &_owned_env
8046                    } else {
8047                        env
8048                    };
8049                    match resolve_dim(&args[0], total, env1)? {
8050                        DimIdx::All => {
8051                            let codes: Vec<f64> = chars.iter().map(|&c| c as u32 as f64).collect();
8052                            if codes.len() == 1 {
8053                                Ok(Value::Scalar(codes[0]))
8054                            } else {
8055                                let n = codes.len();
8056                                Ok(Value::Matrix(
8057                                    Array2::from_shape_vec((1, n), codes).unwrap(),
8058                                ))
8059                            }
8060                        }
8061                        DimIdx::Indices(idxs) => {
8062                            let mut selected = String::new();
8063                            for &i in &idxs {
8064                                if i >= chars.len() {
8065                                    return Err(format!("Index {} out of range", i + 1));
8066                                }
8067                                selected.push(chars[i]);
8068                            }
8069                            if selected.chars().count() == 1 {
8070                                Ok(Value::Scalar(selected.chars().next().unwrap() as u32 as f64))
8071                            } else {
8072                                Ok(Value::Str(selected))
8073                            }
8074                        }
8075                    }
8076                }
8077                Value::StringObj(s) => {
8078                    // String object indexing — treat as single element
8079                    let _owned_env;
8080                    let env1: &Env = if contains_end(&args[0]) {
8081                        _owned_env = env_with_end(env, 1);
8082                        &_owned_env
8083                    } else {
8084                        env
8085                    };
8086                    match resolve_dim(&args[0], 1, env1)? {
8087                        DimIdx::All | DimIdx::Indices(_) => Ok(Value::StringObj(s.clone())),
8088                    }
8089                }
8090                Value::DateTimeArray(v) => {
8091                    let total = v.len();
8092                    let _owned_env;
8093                    let env1: &Env = if contains_end(&args[0]) {
8094                        _owned_env = env_with_end(env, total);
8095                        &_owned_env
8096                    } else {
8097                        env
8098                    };
8099                    match resolve_dim(&args[0], total, env1)? {
8100                        DimIdx::All => Ok(Value::DateTimeArray(v.clone())),
8101                        DimIdx::Indices(idxs) => {
8102                            if idxs.len() == 1 {
8103                                let i = idxs[0];
8104                                if i >= total {
8105                                    return Err(format!(
8106                                        "Index {} out of range (1..{})",
8107                                        i + 1,
8108                                        total
8109                                    ));
8110                                }
8111                                Ok(Value::DateTime(v[i]))
8112                            } else {
8113                                let mut sel = Vec::with_capacity(idxs.len());
8114                                for &i in &idxs {
8115                                    if i >= total {
8116                                        return Err(format!(
8117                                            "Index {} out of range (1..{})",
8118                                            i + 1,
8119                                            total
8120                                        ));
8121                                    }
8122                                    sel.push(v[i]);
8123                                }
8124                                Ok(Value::DateTimeArray(sel))
8125                            }
8126                        }
8127                    }
8128                }
8129                Value::DurationArray(v) => {
8130                    let total = v.len();
8131                    let _owned_env;
8132                    let env1: &Env = if contains_end(&args[0]) {
8133                        _owned_env = env_with_end(env, total);
8134                        &_owned_env
8135                    } else {
8136                        env
8137                    };
8138                    match resolve_dim(&args[0], total, env1)? {
8139                        DimIdx::All => Ok(Value::DurationArray(v.clone())),
8140                        DimIdx::Indices(idxs) => {
8141                            if idxs.len() == 1 {
8142                                let i = idxs[0];
8143                                if i >= total {
8144                                    return Err(format!(
8145                                        "Index {} out of range (1..{})",
8146                                        i + 1,
8147                                        total
8148                                    ));
8149                                }
8150                                Ok(Value::Duration(v[i]))
8151                            } else {
8152                                let mut sel = Vec::with_capacity(idxs.len());
8153                                for &i in &idxs {
8154                                    if i >= total {
8155                                        return Err(format!(
8156                                            "Index {} out of range (1..{})",
8157                                            i + 1,
8158                                            total
8159                                        ));
8160                                    }
8161                                    sel.push(v[i]);
8162                                }
8163                                Ok(Value::DurationArray(sel))
8164                            }
8165                        }
8166                    }
8167                }
8168                Value::DateTime(_) | Value::Duration(_) => {
8169                    // Scalar datetime/duration: indexing with (1) is valid, returns self.
8170                    let _owned_env;
8171                    let env1: &Env = if contains_end(&args[0]) {
8172                        _owned_env = env_with_end(env, 1);
8173                        &_owned_env
8174                    } else {
8175                        env
8176                    };
8177                    match resolve_dim(&args[0], 1, env1)? {
8178                        DimIdx::All | DimIdx::Indices(_) => Ok(val.clone()),
8179                    }
8180                }
8181            }
8182        }
8183        2 => {
8184            // A(i, j), A(:, j), A(i, :), A(:, :), A(end, :), A(1:end, 2)
8185            if matches!(
8186                val,
8187                Value::Void
8188                    | Value::Str(_)
8189                    | Value::StringObj(_)
8190                    | Value::Lambda(_)
8191                    | Value::Function { .. }
8192                    | Value::Tuple(_)
8193                    | Value::Cell(_)
8194                    | Value::Struct(_)
8195                    | Value::StructArray(_)
8196                    | Value::DateTime(_)
8197                    | Value::Duration(_)
8198                    | Value::DateTimeArray(_)
8199                    | Value::DurationArray(_)
8200            ) {
8201                return Err("2D indexing not supported for this type".to_string());
8202            }
8203            // ComplexMatrix is explicitly supported below (not in the guard above)
8204            let (nrows, ncols) = match val {
8205                Value::Scalar(_) | Value::Complex(_, _) => (1, 1),
8206                Value::Matrix(m) => (m.nrows(), m.ncols()),
8207                Value::ComplexMatrix(m) => (m.nrows(), m.ncols()),
8208                _ => unreachable!(),
8209            };
8210            let _owned_r;
8211            let env_r: &Env = if contains_end(&args[0]) {
8212                _owned_r = env_with_end(env, nrows);
8213                &_owned_r
8214            } else {
8215                env
8216            };
8217            let _owned_c;
8218            let env_c: &Env = if contains_end(&args[1]) {
8219                _owned_c = env_with_end(env, ncols);
8220                &_owned_c
8221            } else {
8222                env
8223            };
8224            let row_idx = resolve_dim(&args[0], nrows, env_r)?;
8225            let col_idx = resolve_dim(&args[1], ncols, env_c)?;
8226
8227            let rows: Vec<usize> = match row_idx {
8228                DimIdx::All => (0..nrows).collect(),
8229                DimIdx::Indices(v) => v,
8230            };
8231            let cols: Vec<usize> = match col_idx {
8232                DimIdx::All => (0..ncols).collect(),
8233                DimIdx::Indices(v) => v,
8234            };
8235
8236            if rows.len() == 1 && cols.len() == 1 {
8237                match val {
8238                    Value::Scalar(n) => Ok(Value::Scalar(*n)),
8239                    Value::Complex(re, im) => Ok(Value::Complex(*re, *im)),
8240                    Value::Matrix(m) => Ok(Value::Scalar(m[[rows[0], cols[0]]])),
8241                    Value::ComplexMatrix(m) => {
8242                        let c = m[[rows[0], cols[0]]];
8243                        Ok(make_complex(c.re, c.im))
8244                    }
8245                    _ => unreachable!(),
8246                }
8247            } else {
8248                let out_r = rows.len();
8249                let out_c = cols.len();
8250                match val {
8251                    Value::ComplexMatrix(m) => {
8252                        let flat: Vec<Complex<f64>> = rows
8253                            .iter()
8254                            .flat_map(|&r| cols.iter().map(move |&c| m[[r, c]]))
8255                            .collect();
8256                        Ok(Value::ComplexMatrix(
8257                            Array2::from_shape_vec((out_r, out_c), flat).unwrap(),
8258                        ))
8259                    }
8260                    _ => {
8261                        let flat: Vec<f64> = rows
8262                            .iter()
8263                            .flat_map(|&r| {
8264                                cols.iter().map(move |&c| match val {
8265                                    Value::Scalar(n) => *n,
8266                                    Value::Complex(re, _) => *re,
8267                                    Value::Matrix(m) => m[[r, c]],
8268                                    _ => unreachable!(),
8269                                })
8270                            })
8271                            .collect();
8272                        Ok(Value::Matrix(
8273                            Array2::from_shape_vec((out_r, out_c), flat).unwrap(),
8274                        ))
8275                    }
8276                }
8277            }
8278        }
8279        n => Err(format!(
8280            "Indexing with {n} indices is not supported (max 2)"
8281        )),
8282    }
8283}
8284
8285/// Resolved index along one dimension. Indices are 0-based.
8286enum DimIdx {
8287    All,
8288    Indices(Vec<usize>),
8289}
8290
8291/// Resolves one index argument for a dimension of size `dim_size`.
8292/// `Expr::Colon` → `DimIdx::All`.
8293/// Scalar → single 0-based index (validates 1-based bounds).
8294/// Row/column vector → multiple 0-based indices.
8295/// Logical mask: a 0/1 vector whose length equals `dim_size` selects positions where value is 1.
8296fn resolve_dim(expr: &Expr, dim_size: usize, env: &Env) -> Result<DimIdx, String> {
8297    if matches!(expr, Expr::Colon) {
8298        return Ok(DimIdx::All);
8299    }
8300    let val = eval(expr, env)?;
8301    let floats: Vec<f64> = match val {
8302        Value::Void => {
8303            return Err("Index must be numeric, not void".to_string());
8304        }
8305        Value::Scalar(n) => vec![n],
8306        Value::Complex(re, im) => {
8307            if im != 0.0 {
8308                return Err("Index must be real, not complex".to_string());
8309            }
8310            vec![re]
8311        }
8312        Value::Matrix(m) => {
8313            // Allow 2-D matrices only when they qualify as a logical mask (same numel as dim_size).
8314            let total = m.nrows() * m.ncols();
8315            if m.nrows() > 1 && m.ncols() > 1 && total != dim_size {
8316                return Err("Index must be a scalar or vector, not a matrix".to_string());
8317            }
8318            // Collect in column-major order so mask positions align with linear indexing.
8319            if m.nrows() > 1 && m.ncols() > 1 {
8320                let mut v = Vec::with_capacity(total);
8321                for col in 0..m.ncols() {
8322                    for row in 0..m.nrows() {
8323                        v.push(m[[row, col]]);
8324                    }
8325                }
8326                v
8327            } else {
8328                m.iter().copied().collect()
8329            }
8330        }
8331        Value::Str(_) | Value::StringObj(_) => {
8332            return Err("Index must be numeric, not a string".to_string());
8333        }
8334        Value::ComplexMatrix(_) => {
8335            return Err("Index must be real, not a complex matrix".to_string());
8336        }
8337        Value::Lambda(_)
8338        | Value::Function { .. }
8339        | Value::Tuple(_)
8340        | Value::Cell(_)
8341        | Value::Struct(_)
8342        | Value::StructArray(_)
8343        | Value::DateTime(_)
8344        | Value::Duration(_)
8345        | Value::DateTimeArray(_)
8346        | Value::DurationArray(_) => {
8347            return Err("Index must be numeric, not a function or datetime".to_string());
8348        }
8349    };
8350    // Logical mask: a 0/1 array whose element count matches dim_size selects by boolean mask.
8351    if dim_size > 0 && floats.len() == dim_size && floats.iter().all(|&f| f == 0.0 || f == 1.0) {
8352        let idxs: Vec<usize> = floats
8353            .iter()
8354            .enumerate()
8355            .filter(|&(_, &f)| f == 1.0)
8356            .map(|(i, _)| i)
8357            .collect();
8358        return Ok(DimIdx::Indices(idxs));
8359    }
8360    let mut idxs = Vec::with_capacity(floats.len());
8361    for n in floats {
8362        let i = n.round() as i64;
8363        if i < 1 || i as usize > dim_size {
8364            return Err(format!("Index {i} out of range (1..{dim_size})"));
8365        }
8366        idxs.push(i as usize - 1);
8367    }
8368    Ok(DimIdx::Indices(idxs))
8369}
8370
8371/// Formats a number for display: integers without decimal point,
8372/// floats with up to 10 significant fractional digits, trailing zeros trimmed.
8373/// Always decimal — used for expression re-display, not user-facing output.
8374pub fn format_number(n: f64) -> String {
8375    if n.fract() == 0.0 && n.abs() < 1e15 {
8376        format!("{}", n as i64)
8377    } else if n != 0.0 && (n.abs() >= 1e15 || n.abs() < 1e-9) {
8378        trim_sci(&format!("{:.15e}", n))
8379    } else {
8380        let s = format!("{:.10}", n);
8381        s.trim_end_matches('0').trim_end_matches('.').to_string()
8382    }
8383}
8384
8385/// Formats a scalar `f64` for user-facing output using the given base and format mode.
8386pub fn format_scalar(n: f64, base: Base, mode: &FormatMode) -> String {
8387    // FormatMode::Hex always shows IEEE 754 bits regardless of base.
8388    if matches!(mode, FormatMode::Hex) {
8389        return format_decimal(n, mode);
8390    }
8391    match base {
8392        Base::Dec => format_decimal(n, mode),
8393        _ => format_non_dec(n, base),
8394    }
8395}
8396
8397/// Formats a complex number `re + im*i` for display.
8398///
8399/// - `a + 0i` → `a`  (pure real)
8400/// - `0 + bi` → `bi`
8401/// - `im == ±1` suppresses the coefficient: `i`, `-i`, `a + i`, `a - i`
8402pub fn format_complex(re: f64, im: f64, mode: &FormatMode) -> String {
8403    if im == 0.0 {
8404        return format_decimal(re, mode);
8405    }
8406    let im_abs = im.abs();
8407    let im_str = if im_abs == 1.0 {
8408        String::new()
8409    } else {
8410        format_decimal(im_abs, mode)
8411    };
8412    if re == 0.0 {
8413        if im < 0.0 {
8414            format!("-{}i", im_str)
8415        } else {
8416            format!("{}i", im_str)
8417        }
8418    } else {
8419        let re_str = format_decimal(re, mode);
8420        if im < 0.0 {
8421            format!("{} - {}i", re_str, im_str)
8422        } else {
8423            format!("{} + {}i", re_str, im_str)
8424        }
8425    }
8426}
8427
8428/// Reconstructs a source-like string from an `Expr`.
8429///
8430/// Used to populate the display string of lambda values so that
8431/// `f = @(x) x.^2` shows `f = @(x) x .^ 2` in the REPL.
8432pub fn expr_to_string(e: &Expr) -> String {
8433    match e {
8434        Expr::Number(n) => {
8435            if n.is_nan() {
8436                "nan".to_string()
8437            } else if n.is_infinite() {
8438                if *n > 0.0 {
8439                    "inf".to_string()
8440                } else {
8441                    "-inf".to_string()
8442                }
8443            } else {
8444                format!("{n}")
8445            }
8446        }
8447        Expr::Var(name) => name.clone(),
8448        Expr::UnaryMinus(e) => format!("-{}", expr_to_string(e)),
8449        Expr::UnaryNot(e) => format!("~{}", expr_to_string(e)),
8450        Expr::BinOp(l, op, r) => {
8451            let op_str = match op {
8452                Op::Add => "+",
8453                Op::Sub => "-",
8454                Op::Mul => "*",
8455                Op::Div => "/",
8456                Op::Pow => "^",
8457                Op::ElemMul => ".*",
8458                Op::ElemDiv => "./",
8459                Op::ElemPow => ".^",
8460                Op::Eq => "==",
8461                Op::NotEq => "~=",
8462                Op::Lt => "<",
8463                Op::Gt => ">",
8464                Op::LtEq => "<=",
8465                Op::GtEq => ">=",
8466                Op::And => "&&",
8467                Op::Or => "||",
8468                Op::ElemAnd => "&",
8469                Op::ElemOr => "|",
8470                Op::LDiv => "\\",
8471            };
8472            format!("{} {op_str} {}", expr_to_string(l), expr_to_string(r))
8473        }
8474        Expr::Call(name, args) => {
8475            let args_str = args
8476                .iter()
8477                .map(expr_to_string)
8478                .collect::<Vec<_>>()
8479                .join(", ");
8480            format!("{name}({args_str})")
8481        }
8482        Expr::Transpose(e) => format!("{}'", expr_to_string(e)),
8483        Expr::PlainTranspose(e) => format!("{}.'", expr_to_string(e)),
8484        Expr::Range(start, step, stop) => {
8485            if let Some(step) = step {
8486                format!(
8487                    "{}:{}:{}",
8488                    expr_to_string(start),
8489                    expr_to_string(step),
8490                    expr_to_string(stop)
8491                )
8492            } else {
8493                format!("{}:{}", expr_to_string(start), expr_to_string(stop))
8494            }
8495        }
8496        Expr::StrLiteral(s) => format!("'{s}'"),
8497        Expr::StringObjLiteral(s) => format!("\"{s}\""),
8498        Expr::Lambda { params, body, .. } => {
8499            format!("@({}) {}", params.join(", "), expr_to_string(body))
8500        }
8501        Expr::FuncHandle(name) => format!("@{name}"),
8502        Expr::Matrix(_) => "[...]".to_string(),
8503        Expr::CellLiteral(_) => "{...}".to_string(),
8504        Expr::CellIndex(e, i) => format!("{}{{{}}}", expr_to_string(e), expr_to_string(i)),
8505        Expr::Colon => ":".to_string(),
8506        Expr::NaT => "NaT".to_string(),
8507        Expr::FieldGet(base, field) => format!("{}.{field}", expr_to_string(base)),
8508        Expr::DotCall(segs, args) => {
8509            let args_str = args
8510                .iter()
8511                .map(expr_to_string)
8512                .collect::<Vec<_>>()
8513                .join(", ");
8514            format!("{}({args_str})", segs.join("."))
8515        }
8516    }
8517}
8518
8519/// Formats a `Value` compactly: scalars as a number string, matrices as `[NxM double]`.
8520pub fn format_value(v: &Value, base: Base, mode: &FormatMode) -> String {
8521    match v {
8522        Value::Void => String::new(),
8523        Value::Scalar(n) => format_scalar(*n, base, mode),
8524        Value::Matrix(m) => format!("[{}x{} double]", m.nrows(), m.ncols()),
8525        Value::ComplexMatrix(m) => format!("[{}×{} complex]", m.nrows(), m.ncols()),
8526        Value::Complex(re, im) => format_complex(*re, *im, mode),
8527        Value::Str(s) => s.clone(),
8528        Value::StringObj(s) => s.clone(),
8529        Value::Lambda(lf) => lf.1.clone(),
8530        Value::Function {
8531            params, outputs, ..
8532        } => {
8533            let params_str = params.join(", ");
8534            let out_str = match outputs.len() {
8535                0 => String::new(),
8536                1 => format!("{} = ", outputs[0]),
8537                _ => format!("[{}] = ", outputs.join(", ")),
8538            };
8539            format!("@function {out_str}f({params_str})")
8540        }
8541        Value::Tuple(vals) => {
8542            let parts: Vec<String> = vals.iter().map(|v| format_value(v, base, mode)).collect();
8543            format!("({})", parts.join(", "))
8544        }
8545        Value::Cell(v) => format!("{{1×{} cell}}", v.len()),
8546        Value::Struct(_) => "[1×1 struct]".to_string(),
8547        Value::StructArray(arr) => format!("[1×{} struct]", arr.len()),
8548        Value::DateTime(ts) => crate::datetime::format_datetime(*ts),
8549        Value::Duration(s) => crate::datetime::format_duration(*s),
8550        Value::DateTimeArray(v) => format!("[{}×1 datetime]", v.len()),
8551        Value::DurationArray(v) => format!("[{}×1 duration]", v.len()),
8552    }
8553}
8554
8555/// Returns `None` for scalars, complex numbers, strings, and void (displayed inline or suppressed);
8556/// `Some(full_string)` for matrices (MATLAB-style column-aligned display).
8557pub fn format_value_full(v: &Value, mode: &FormatMode) -> Option<String> {
8558    match v {
8559        Value::Void
8560        | Value::Scalar(_)
8561        | Value::Complex(_, _)
8562        | Value::Str(_)
8563        | Value::StringObj(_)
8564        | Value::Lambda(_)
8565        | Value::Function { .. }
8566        | Value::Tuple(_)
8567        | Value::DateTime(_)
8568        | Value::Duration(_) => None,
8569        Value::Matrix(m) => Some(format_matrix(m, mode)),
8570        Value::ComplexMatrix(m) => Some(format_complex_matrix(m, mode)),
8571        Value::Cell(elems) => Some(format_cell(elems, mode)),
8572        Value::Struct(map) => Some(format_struct(map, mode)),
8573        Value::StructArray(arr) => Some(format_struct_array(arr, mode)),
8574        Value::DateTimeArray(v) => Some(format_datetime_array(v)),
8575        Value::DurationArray(v) => Some(format_duration_array(v)),
8576    }
8577}
8578
8579/// Formats a cell array in MATLAB-style multi-line display.
8580fn format_cell(elems: &[Value], mode: &FormatMode) -> String {
8581    if elems.is_empty() {
8582        return "  {}".to_string();
8583    }
8584    let mut lines = vec!["  {".to_string()];
8585    for (i, val) in elems.iter().enumerate() {
8586        let label = format!("    [1,{}]", i + 1);
8587        match val {
8588            Value::Matrix(_) => {
8589                lines.push(format!("{label}:"));
8590                if let Some(full) = format_value_full(val, mode) {
8591                    for line in full.lines() {
8592                        lines.push(format!("   {line}"));
8593                    }
8594                }
8595            }
8596            Value::Cell(_) => {
8597                lines.push(format!("{label}: {}", format_value(val, Base::Dec, mode)));
8598            }
8599            _ => {
8600                lines.push(format!("{label}: {}", format_value(val, Base::Dec, mode)));
8601            }
8602        }
8603    }
8604    lines.push("  }".to_string());
8605    lines.join("\n")
8606}
8607
8608/// Formats a struct in MATLAB 2014b+ multi-line style.
8609fn format_struct(map: &IndexMap<String, Value>, mode: &FormatMode) -> String {
8610    let mut lines = vec![
8611        String::new(),
8612        "  struct with fields:".to_string(),
8613        String::new(),
8614    ];
8615    for (key, val) in map {
8616        let val_str = match val {
8617            Value::Struct(_) => "[1×1 struct]".to_string(),
8618            Value::StructArray(arr) => format!("[1×{} struct]", arr.len()),
8619            Value::Matrix(m) => format!("[{}×{} double]", m.nrows(), m.ncols()),
8620            Value::Cell(v) => format!("{{1×{} cell}}", v.len()),
8621            _ => format_value(val, Base::Dec, mode),
8622        };
8623        lines.push(format!("    {key}: {val_str}"));
8624    }
8625    lines.join("\n")
8626}
8627
8628/// Formats a 1×N struct array (shows each element's fields).
8629fn format_struct_array(arr: &[IndexMap<String, Value>], mode: &FormatMode) -> String {
8630    let n = arr.len();
8631    let mut lines = vec![
8632        String::new(),
8633        format!("  1×{n} struct array with fields:"),
8634        String::new(),
8635    ];
8636    // Collect field names from the first element
8637    if let Some(first) = arr.first() {
8638        for key in first.keys() {
8639            lines.push(format!("    {key}"));
8640        }
8641    }
8642    // Show first element's values if array has exactly 1 element
8643    if n == 1
8644        && let Some(first) = arr.first()
8645    {
8646        lines.clear();
8647        lines.push(String::new());
8648        lines.push("  struct with fields:".to_string());
8649        lines.push(String::new());
8650        for (key, val) in first {
8651            let val_str = match val {
8652                Value::Struct(_) => "[1×1 struct]".to_string(),
8653                Value::StructArray(a) => format!("[1×{} struct]", a.len()),
8654                Value::Matrix(m) => format!("[{}×{} double]", m.nrows(), m.ncols()),
8655                Value::Cell(v) => format!("{{1×{} cell}}", v.len()),
8656                _ => format_value(val, Base::Dec, mode),
8657            };
8658            lines.push(format!("    {key}: {val_str}"));
8659        }
8660    }
8661    lines.join("\n")
8662}
8663
8664fn format_datetime_array(v: &[f64]) -> String {
8665    let mut lines = Vec::with_capacity(v.len());
8666    for ts in v {
8667        lines.push(format!("  {}", crate::datetime::format_datetime(*ts)));
8668    }
8669    lines.join("\n")
8670}
8671
8672fn format_duration_array(v: &[f64]) -> String {
8673    let mut lines = Vec::with_capacity(v.len());
8674    for secs in v {
8675        lines.push(format!("  {}", crate::datetime::format_duration(*secs)));
8676    }
8677    lines.join("\n")
8678}
8679
8680/// Formats a complex matrix with right-aligned columns, 3-space indent, 3 spaces between columns.
8681/// Each element is formatted using [`format_complex`]; columns are aligned to the widest entry.
8682fn format_complex_matrix(m: &Array2<Complex<f64>>, mode: &FormatMode) -> String {
8683    if m.nrows() == 0 || m.ncols() == 0 {
8684        return "   []".to_string();
8685    }
8686    let ncols = m.ncols();
8687
8688    // Split each cell into (re_str, sign " + "/" - ", im_abs_str) so that the
8689    // real and imaginary parts can be right-aligned independently.  This keeps
8690    // the leading indent uniform regardless of how many digits each part needs.
8691    let parts: Vec<Vec<(String, &'static str, String)>> = m
8692        .rows()
8693        .into_iter()
8694        .map(|row| {
8695            row.iter()
8696                .map(|c| {
8697                    let re_str = format_decimal(c.re, mode);
8698                    let im_abs = format_decimal(c.im.abs(), mode);
8699                    let sign = if c.im < 0.0 { " - " } else { " + " };
8700                    (re_str, sign, im_abs)
8701                })
8702                .collect()
8703        })
8704        .collect();
8705
8706    // Per-column max widths for re and im parts independently.
8707    let re_widths: Vec<usize> = (0..ncols)
8708        .map(|c| parts.iter().map(|row| row[c].0.len()).max().unwrap_or(0))
8709        .collect();
8710    let im_widths: Vec<usize> = (0..ncols)
8711        .map(|c| parts.iter().map(|row| row[c].2.len()).max().unwrap_or(0))
8712        .collect();
8713
8714    let mut lines = Vec::new();
8715    for row in &parts {
8716        let mut line = String::from("   ");
8717        for (c, (re_str, sign, im_str)) in row.iter().enumerate() {
8718            if c > 0 {
8719                // Pad the previous column's im part so column boundaries stay fixed.
8720                // Inter-column gap is 3 spaces past the widest im in this column.
8721                let prev_im_pad = im_widths[c - 1].saturating_sub(row[c - 1].2.len());
8722                for _ in 0..prev_im_pad {
8723                    line.push(' ');
8724                }
8725                line.push_str("   ");
8726            }
8727            let re_pad = re_widths[c].saturating_sub(re_str.len());
8728            for _ in 0..re_pad {
8729                line.push(' ');
8730            }
8731            line.push_str(re_str);
8732            line.push_str(sign);
8733            line.push_str(im_str);
8734            line.push('i');
8735        }
8736        lines.push(line);
8737    }
8738    lines.join("\n")
8739}
8740
8741/// Formats a matrix with right-aligned columns, 3-space indent, 3 spaces between columns.
8742/// `FormatMode::Plus` renders a sign grid (`+`, `-`, `0`).
8743fn format_matrix(m: &Array2<f64>, mode: &FormatMode) -> String {
8744    if m.nrows() == 0 || m.ncols() == 0 {
8745        return "   []".to_string();
8746    }
8747    // Special rendering for format +
8748    if matches!(mode, FormatMode::Plus) {
8749        let lines: Vec<String> = m
8750            .rows()
8751            .into_iter()
8752            .map(|row| {
8753                let chars: String = row
8754                    .iter()
8755                    .map(|&x| {
8756                        if x > 0.0 {
8757                            '+'
8758                        } else if x < 0.0 {
8759                            '-'
8760                        } else {
8761                            '0'
8762                        }
8763                    })
8764                    .collect();
8765                format!("   {}", chars)
8766            })
8767            .collect();
8768        return lines.join("\n");
8769    }
8770    let ncols = m.ncols();
8771    let cells: Vec<Vec<String>> = m
8772        .rows()
8773        .into_iter()
8774        .map(|row| row.iter().map(|&x| format_decimal(x, mode)).collect())
8775        .collect();
8776    let col_widths: Vec<usize> = (0..ncols)
8777        .map(|c| cells.iter().map(|row| row[c].len()).max().unwrap_or(0))
8778        .collect();
8779    let mut lines = Vec::new();
8780    for row in &cells {
8781        let mut line = String::from("   ");
8782        for (c, cell) in row.iter().enumerate() {
8783            if c > 0 {
8784                line.push_str("   ");
8785            }
8786            let pad = col_widths[c].saturating_sub(cell.len());
8787            for _ in 0..pad {
8788                line.push(' ');
8789            }
8790            line.push_str(cell);
8791        }
8792        lines.push(line);
8793    }
8794    lines.join("\n")
8795}
8796
8797/// Formats a number in a non-decimal integer base (hex/bin/oct).
8798/// Rounds to the nearest integer before formatting.
8799pub fn format_non_dec(n: f64, base: Base) -> String {
8800    let i = n.round() as i64;
8801    let u = i.unsigned_abs();
8802    let sign = if i < 0 { "-" } else { "" };
8803    match base {
8804        Base::Hex => format!("{}0x{:X}", sign, u),
8805        Base::Bin => format!("{}0b{:b}", sign, u),
8806        Base::Oct => format!("{}0o{:o}", sign, u),
8807        Base::Dec => format_decimal(n, &FormatMode::default()),
8808    }
8809}
8810
8811// ---------------------------------------------------------------------------
8812// Internal decimal formatters
8813// ---------------------------------------------------------------------------
8814
8815fn format_decimal(n: f64, mode: &FormatMode) -> String {
8816    if n.is_nan() {
8817        return "NaN".to_string();
8818    }
8819    if n.is_infinite() {
8820        return if n > 0.0 { "Inf" } else { "-Inf" }.to_string();
8821    }
8822    match mode {
8823        FormatMode::Short | FormatMode::ShortG => fmt_auto_sig(n, 5),
8824        FormatMode::Long | FormatMode::LongG => fmt_auto_sig(n, 15),
8825        FormatMode::ShortE => fmt_sci_dp(n, 4),
8826        FormatMode::LongE => fmt_sci_dp(n, 14),
8827        FormatMode::Bank => format!("{:.2}", n),
8828        FormatMode::Rat => fmt_rat(n),
8829        FormatMode::Hex => fmt_hex_ieee754(n),
8830        FormatMode::Plus => fmt_plus_sign(n),
8831        FormatMode::Custom(prec) => fmt_custom_prec(n, *prec),
8832    }
8833}
8834
8835/// Integer shortcut: fits in i64 without fractional part.
8836#[inline]
8837fn is_exact_int(n: f64) -> bool {
8838    n.fract() == 0.0 && n.abs() < 1e15
8839}
8840
8841/// Auto fixed/scientific with `sig` significant digits (MATLAB-compatible).
8842/// Uses fixed notation for exponents in [-3, sig), scientific otherwise.
8843/// Integers are shown without a decimal point.
8844fn fmt_auto_sig(n: f64, sig: usize) -> String {
8845    if is_exact_int(n) {
8846        return format!("{}", n as i64);
8847    }
8848    let abs_n = n.abs();
8849    let exp = if abs_n == 0.0 {
8850        0i32
8851    } else {
8852        abs_n.log10().floor() as i32
8853    };
8854    if exp >= -3 && exp < sig as i32 {
8855        let dp = (sig as i32 - 1 - exp) as usize;
8856        let s = format!("{:.prec$}", n, prec = dp);
8857        // Only strip trailing zeros when there is a decimal point.
8858        if s.contains('.') {
8859            s.trim_end_matches('0').trim_end_matches('.').to_string()
8860        } else {
8861            s
8862        }
8863    } else {
8864        let s = format!("{:.prec$e}", n, prec = sig - 1);
8865        trim_sci(&s)
8866    }
8867}
8868
8869/// Always scientific notation with `dp` decimal places.
8870fn fmt_sci_dp(n: f64, dp: usize) -> String {
8871    let s = format!("{:.prec$e}", n, prec = dp);
8872    trim_sci(&s)
8873}
8874
8875/// Legacy custom-precision: N decimal places, auto fixed/scientific.
8876fn fmt_custom_prec(n: f64, prec: usize) -> String {
8877    if is_exact_int(n) {
8878        return format!("{}", n as i64);
8879    }
8880    if n.abs() >= 1e15 || (n != 0.0 && n.abs() < 1e-9) {
8881        let s = format!("{:.prec$e}", n, prec = prec);
8882        trim_sci(&s)
8883    } else {
8884        let s = format!("{:.prec$}", n, prec = prec);
8885        s.trim_end_matches('0').trim_end_matches('.').to_string()
8886    }
8887}
8888
8889/// Rational approximation via continued fractions. Returns `"p/q"` or `"p"` if denominator is 1.
8890fn fmt_rat(n: f64) -> String {
8891    if is_exact_int(n) {
8892        return format!("{}", n as i64);
8893    }
8894    let sign = if n < 0.0 { -1i64 } else { 1i64 };
8895    let x = n.abs();
8896    let (mut h1, mut h2): (i64, i64) = (1, 0);
8897    let (mut k1, mut k2): (i64, i64) = (0, 1);
8898    let mut b = x;
8899    for _ in 0..64 {
8900        let a = b.floor() as i64;
8901        let (nh, nk) = (a * h1 + h2, a * k1 + k2);
8902        if nk > 10_000 {
8903            break;
8904        }
8905        h2 = h1;
8906        h1 = nh;
8907        k2 = k1;
8908        k1 = nk;
8909        let frac = b - a as f64;
8910        if frac < 1e-12 || (h1 as f64 / k1 as f64 - x).abs() < 1e-6 {
8911            break;
8912        }
8913        b = 1.0 / frac;
8914    }
8915    let p = sign * h1;
8916    if k1 == 1 {
8917        format!("{}", p)
8918    } else {
8919        format!("{}/{}", p, k1)
8920    }
8921}
8922
8923/// IEEE 754 double-precision bit pattern as 16 uppercase hex digits.
8924fn fmt_hex_ieee754(n: f64) -> String {
8925    format!("{:016X}", n.to_bits())
8926}
8927
8928/// Sign indicator: `+`, `-`, or ` ` for zero.
8929fn fmt_plus_sign(n: f64) -> String {
8930    if n > 0.0 {
8931        "+".to_string()
8932    } else if n < 0.0 {
8933        "-".to_string()
8934    } else {
8935        " ".to_string()
8936    }
8937}
8938
8939fn trim_sci(s: &str) -> String {
8940    if let Some(e_pos) = s.find('e') {
8941        let mantissa = s[..e_pos].trim_end_matches('0').trim_end_matches('.');
8942        let exp_str = &s[e_pos + 1..];
8943        let (sign, digits) = if let Some(d) = exp_str.strip_prefix('-') {
8944            ("-", d)
8945        } else if let Some(d) = exp_str.strip_prefix('+') {
8946            ("+", d)
8947        } else {
8948            ("+", exp_str)
8949        };
8950        let exp_num: i32 = digits.parse().unwrap_or(0);
8951        format!("{}e{}{:02}", mantissa, sign, exp_num)
8952    } else {
8953        s.to_string()
8954    }
8955}
8956
8957// --- MAT built-in helpers ---
8958
8959/// Loads a MATLAB Level 5/7 MAT file and returns a [`Value::Struct`].
8960///
8961/// Requires the `mat` Cargo feature; without it, always returns an error.
8962pub fn load_mat_file(path: &str) -> Result<Value, String> {
8963    load_mat_file_impl(path)
8964}
8965
8966#[cfg(feature = "mat")]
8967fn load_mat_file_impl(path: &str) -> Result<Value, String> {
8968    crate::mat::mat_load(path)
8969}
8970
8971#[cfg(not(feature = "mat"))]
8972fn load_mat_file_impl(_path: &str) -> Result<Value, String> {
8973    Err("load: .mat support not available — rebuild with --features mat".to_string())
8974}
8975
8976// --- Regex built-in helpers ---
8977
8978#[cfg(feature = "regex")]
8979fn regexp_impl(
8980    fname: &str,
8981    s: &str,
8982    pat: &str,
8983    ignore_case: bool,
8984    return_match: bool,
8985) -> Result<Value, String> {
8986    use ndarray::Array2;
8987    let full_pat = if ignore_case {
8988        format!("(?i){pat}")
8989    } else {
8990        pat.to_string()
8991    };
8992    let re = regex::Regex::new(&full_pat).map_err(|e| format!("{fname}: invalid pattern: {e}"))?;
8993    if return_match {
8994        let matches: Vec<Value> = re
8995            .find_iter(s)
8996            .map(|m| Value::Str(m.as_str().to_string()))
8997            .collect();
8998        Ok(Value::Cell(matches))
8999    } else {
9000        match re.find(s) {
9001            Some(m) => Ok(Value::Scalar((s[..m.start()].chars().count() + 1) as f64)),
9002            None => Ok(Value::Matrix(Array2::zeros((0, 0)))),
9003        }
9004    }
9005}
9006
9007#[cfg(not(feature = "regex"))]
9008fn regexp_impl(
9009    fname: &str,
9010    _s: &str,
9011    _pat: &str,
9012    _ignore_case: bool,
9013    _return_match: bool,
9014) -> Result<Value, String> {
9015    Err(format!(
9016        "{fname}: not available — rebuild with --features regex"
9017    ))
9018}
9019
9020#[cfg(feature = "regex")]
9021fn regexprep_impl(s: &str, pat: &str, rep: &str) -> Result<Value, String> {
9022    let re = regex::Regex::new(pat).map_err(|e| format!("regexprep: invalid pattern: {e}"))?;
9023    let result = re.replace_all(s, regex::NoExpand(rep));
9024    Ok(Value::Str(result.into_owned()))
9025}
9026
9027#[cfg(not(feature = "regex"))]
9028fn regexprep_impl(_s: &str, _pat: &str, _rep: &str) -> Result<Value, String> {
9029    Err("regexprep: not available — rebuild with --features regex".to_string())
9030}
9031
9032// ── Phase 26 — FFT built-in helpers ─────────────────────────────────────────
9033
9034/// Extracts a flat real vector from a Scalar or 1-D Matrix (row or column).
9035#[cfg(feature = "fft")]
9036fn extract_real_vec(v: &Value, name: &str) -> Result<Vec<f64>, String> {
9037    match v {
9038        Value::Scalar(s) => Ok(vec![*s]),
9039        Value::Matrix(m) if m.nrows() == 1 || m.ncols() == 1 => Ok(m.iter().copied().collect()),
9040        Value::Matrix(m) => Err(format!(
9041            "{name}: input must be a vector (got {}×{} matrix)",
9042            m.nrows(),
9043            m.ncols()
9044        )),
9045        _ => Err(format!("{name}: input must be a real numeric vector")),
9046    }
9047}
9048
9049/// Wraps a `Vec<(f64,f64)>` of FFT output into a 1×N `Value::ComplexMatrix`.
9050#[cfg(feature = "fft")]
9051fn complex_pairs_to_complex_matrix(data: Vec<(f64, f64)>) -> Value {
9052    let n = data.len();
9053    if n == 0 {
9054        return Value::ComplexMatrix(Array2::zeros((1, 0)));
9055    }
9056    let elems: Vec<Complex<f64>> = data
9057        .into_iter()
9058        .map(|(re, im)| Complex::new(re, im))
9059        .collect();
9060    Value::ComplexMatrix(Array2::from_shape_vec((1, n), elems).unwrap())
9061}
9062
9063/// Extracts a flat complex vector from a [`Value::ComplexMatrix`], `Cell`, or real matrix.
9064#[cfg(feature = "fft")]
9065fn extract_complex_vec(v: &Value, name: &str) -> Result<Vec<(f64, f64)>, String> {
9066    match v {
9067        Value::Scalar(s) => Ok(vec![(*s, 0.0)]),
9068        Value::Matrix(m) => Ok(m.iter().copied().map(|x| (x, 0.0)).collect()),
9069        Value::ComplexMatrix(m) => Ok(m.iter().map(|c| (c.re, c.im)).collect()),
9070        Value::Cell(elems) => elems
9071            .iter()
9072            .enumerate()
9073            .map(|(i, e)| match e {
9074                Value::Complex(re, im) => Ok((*re, *im)),
9075                Value::Scalar(s) => Ok((*s, 0.0)),
9076                _ => Err(format!(
9077                    "{name}: cell element {} must be a complex or real number",
9078                    i + 1
9079                )),
9080            })
9081            .collect(),
9082        _ => Err(format!(
9083            "{name}: input must be a complex matrix, cell array, or numeric vector"
9084        )),
9085    }
9086}
9087
9088#[cfg(feature = "fft")]
9089fn fft_call(v: &Value, n_opt: Option<usize>) -> Result<Value, String> {
9090    let real = extract_real_vec(v, "fft")?;
9091    let n = n_opt.unwrap_or(real.len());
9092    if n == 0 {
9093        return Err("fft: length must be positive".to_string());
9094    }
9095    let out = crate::fft::fft_forward(&real, n);
9096    Ok(complex_pairs_to_complex_matrix(out))
9097}
9098
9099#[cfg(not(feature = "fft"))]
9100fn fft_call(_v: &Value, _n_opt: Option<usize>) -> Result<Value, String> {
9101    Err("fft: not available — rebuild with --features fft".to_string())
9102}
9103
9104#[cfg(feature = "fft")]
9105fn ifft_call(v: &Value) -> Result<Value, String> {
9106    let complex = extract_complex_vec(v, "ifft")?;
9107    if complex.is_empty() {
9108        return Ok(Value::Matrix(ndarray::Array2::zeros((1, 0))));
9109    }
9110    let out = crate::fft::fft_inverse(&complex);
9111    if out.iter().all(|(_, im)| im.abs() < 1e-12) {
9112        let real: Vec<f64> = out.iter().map(|(re, _)| *re).collect();
9113        let n = real.len();
9114        Ok(Value::Matrix(
9115            ndarray::Array2::from_shape_vec((1, n), real).unwrap(),
9116        ))
9117    } else {
9118        Ok(complex_pairs_to_complex_matrix(out))
9119    }
9120}
9121
9122#[cfg(not(feature = "fft"))]
9123fn ifft_call(_v: &Value) -> Result<Value, String> {
9124    Err("ifft: not available — rebuild with --features fft".to_string())
9125}
9126
9127// --- JSON built-in helpers ---
9128
9129#[cfg(feature = "json")]
9130fn jsondecode_impl(arg: &Value) -> Result<Value, String> {
9131    let s = match arg {
9132        Value::Str(s) | Value::StringObj(s) => s.as_str(),
9133        _ => return Err("jsondecode: argument must be a string".to_string()),
9134    };
9135    let jval: serde_json::Value =
9136        serde_json::from_str(s).map_err(|e| format!("jsondecode: invalid JSON: {e}"))?;
9137    Ok(crate::json::json_to_value(&jval))
9138}
9139
9140#[cfg(not(feature = "json"))]
9141fn jsondecode_impl(_arg: &Value) -> Result<Value, String> {
9142    Err("jsondecode: not available — rebuild with --features json".to_string())
9143}
9144
9145#[cfg(feature = "json")]
9146fn jsonencode_impl(arg: &Value) -> Result<Value, String> {
9147    let jval = crate::json::value_to_json(arg)?;
9148    let s = serde_json::to_string(&jval)
9149        .map_err(|e| format!("jsonencode: serialization error: {e}"))?;
9150    Ok(Value::Str(s))
9151}
9152
9153#[cfg(not(feature = "json"))]
9154fn jsonencode_impl(_arg: &Value) -> Result<Value, String> {
9155    Err("jsonencode: not available — rebuild with --features json".to_string())
9156}
9157
9158// ---------------------------------------------------------------------------
9159// Phase 24 — Polynomial helpers
9160// ---------------------------------------------------------------------------
9161
9162/// Evaluates a polynomial with real coefficients at a complex point using Horner's method.
9163fn cpoly_eval(coeffs: &[f64], z: (f64, f64)) -> (f64, f64) {
9164    let mut acc = (0.0_f64, 0.0_f64);
9165    for &c in coeffs {
9166        // acc = acc * z + c
9167        acc = (acc.0 * z.0 - acc.1 * z.1 + c, acc.0 * z.1 + acc.1 * z.0);
9168    }
9169    acc
9170}
9171
9172/// Evaluates a polynomial at a real point using Horner's method.
9173fn horner(coeffs: &[f64], x: f64) -> f64 {
9174    coeffs.iter().fold(0.0, |acc, &c| acc * x + c)
9175}
9176
9177/// Extracts polynomial (or 1-D knot) coefficients from a scalar or row/column vector `Value`.
9178fn poly_coeffs(v: &Value, fname: &str) -> Result<Vec<f64>, String> {
9179    match v {
9180        Value::Scalar(s) => Ok(vec![*s]),
9181        Value::Matrix(m) => {
9182            if m.nrows() == 1 {
9183                Ok(m.row(0).iter().copied().collect())
9184            } else if m.ncols() == 1 {
9185                Ok(m.column(0).iter().copied().collect())
9186            } else {
9187                Err(format!(
9188                    "{fname}: argument must be a vector, got {}×{}",
9189                    m.nrows(),
9190                    m.ncols()
9191                ))
9192            }
9193        }
9194        _ => Err(format!("{fname}: argument must be a real numeric vector")),
9195    }
9196}
9197
9198/// Discrete linear convolution of two sequences. Result length = `a.len() + b.len() − 1`.
9199fn poly_conv(a: &[f64], b: &[f64]) -> Vec<f64> {
9200    if a.is_empty() || b.is_empty() {
9201        return vec![];
9202    }
9203    let mut result = vec![0.0_f64; a.len() + b.len() - 1];
9204    for (i, &ai) in a.iter().enumerate() {
9205        for (j, &bj) in b.iter().enumerate() {
9206            result[i + j] += ai * bj;
9207        }
9208    }
9209    result
9210}
9211
9212/// Polynomial long division `c / b` → `(quotient, remainder)`.
9213///
9214/// The remainder has the same length as `c` (MATLAB convention), satisfying
9215/// `conv(b, q) + r == c` element-wise.
9216fn poly_deconv(c: &[f64], b: &[f64]) -> Result<(Vec<f64>, Vec<f64>), String> {
9217    if b.is_empty() || b.iter().all(|&x| x == 0.0) {
9218        return Err("deconv: divisor polynomial must not be zero".to_string());
9219    }
9220    let mc = c.len();
9221    let mb = b.len();
9222    if mb > mc {
9223        return Ok((vec![0.0], c.to_vec()));
9224    }
9225    let q_len = mc - mb + 1;
9226    let mut remainder = c.to_vec();
9227    let mut q = vec![0.0_f64; q_len];
9228    for i in 0..q_len {
9229        let coeff = remainder[i] / b[0];
9230        q[i] = coeff;
9231        for j in 0..mb {
9232            remainder[i + j] -= coeff * b[j];
9233        }
9234    }
9235    // Zero out rounding residuals relative to the input scale
9236    let scale = c.iter().map(|v| v.abs()).fold(0.0_f64, f64::max).max(1.0);
9237    for x in &mut remainder {
9238        if x.abs() < 1e-10 * scale {
9239            *x = 0.0;
9240        }
9241    }
9242    Ok((q, remainder))
9243}
9244
9245/// Finds all roots of `coeffs` (degree = `coeffs.len() − 1`) using the
9246/// Durand–Kerner (Weierstrass) iteration.
9247///
9248/// Returns roots as `(re, im)` pairs sorted by descending real part, then
9249/// descending imaginary part.
9250fn durand_kerner(coeffs: &[f64]) -> Result<Vec<(f64, f64)>, String> {
9251    let n = coeffs.len() - 1; // degree
9252    if n == 0 {
9253        return Ok(vec![]);
9254    }
9255    let lc = coeffs[0];
9256    if lc == 0.0 {
9257        return Err("roots: leading coefficient must not be zero".to_string());
9258    }
9259    // Normalize to monic polynomial
9260    let monic: Vec<f64> = coeffs.iter().map(|&c| c / lc).collect();
9261
9262    // Cauchy root bound: all roots have |z| ≤ r
9263    let r = 1.0 + monic[1..].iter().map(|c| c.abs()).fold(0.0_f64, f64::max);
9264
9265    // Initial guesses on a circle, rotated by 0.25/n turns to avoid the real axis
9266    // (a purely real start can stall for polynomials with purely imaginary roots).
9267    let mut z: Vec<(f64, f64)> = (0..n)
9268        .map(|k| {
9269            let angle = 2.0 * std::f64::consts::PI * (k as f64 + 0.25) / n as f64;
9270            (r * angle.cos(), r * angle.sin())
9271        })
9272        .collect();
9273
9274    const MAX_ITER: usize = 2000;
9275    const EPS: f64 = 1e-12;
9276
9277    for _ in 0..MAX_ITER {
9278        let z_old = z.clone();
9279        let mut max_corr = 0.0_f64;
9280        for i in 0..n {
9281            let (pre, pim) = cpoly_eval(&monic, z_old[i]);
9282            // denominator = Π_{j≠i}(z_i − z_j)
9283            let mut dre = 1.0_f64;
9284            let mut dim = 0.0_f64;
9285            for j in 0..n {
9286                if j == i {
9287                    continue;
9288                }
9289                let (dr, di) = (z_old[i].0 - z_old[j].0, z_old[i].1 - z_old[j].1);
9290                (dre, dim) = (dre * dr - dim * di, dre * di + dim * dr);
9291            }
9292            // correction = p(z_i) / denom
9293            let d2 = dre * dre + dim * dim;
9294            let (cre, cim) = if d2 > 0.0 {
9295                ((pre * dre + pim * dim) / d2, (pim * dre - pre * dim) / d2)
9296            } else {
9297                (pre, pim)
9298            };
9299            let corr_abs = (cre * cre + cim * cim).sqrt();
9300            max_corr = max_corr.max(corr_abs);
9301            z[i] = (z_old[i].0 - cre, z_old[i].1 - cim);
9302        }
9303        if max_corr < EPS {
9304            break;
9305        }
9306    }
9307
9308    // Sort by descending real part, then descending imaginary part
9309    z.sort_by(|a, b| {
9310        b.0.partial_cmp(&a.0)
9311            .unwrap_or(std::cmp::Ordering::Equal)
9312            .then(b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal))
9313    });
9314
9315    Ok(z)
9316}
9317
9318/// Converts a list of complex roots into a `Value`.
9319///
9320/// Returns a real `Matrix` (column vector) when all imaginary parts are below
9321/// `1e-9`; otherwise returns a `Cell` of `Scalar`/`Complex` elements.
9322fn roots_to_value(roots: &[(f64, f64)]) -> Value {
9323    const IMAG_TOL: f64 = 1e-9;
9324    let all_real = roots.iter().all(|(_, im)| im.abs() < IMAG_TOL);
9325    if all_real {
9326        let data: Vec<f64> = roots.iter().map(|(re, _)| *re).collect();
9327        let n = data.len();
9328        Value::Matrix(Array2::from_shape_vec((n, 1), data).unwrap())
9329    } else {
9330        let vals: Vec<Value> = roots
9331            .iter()
9332            .map(|&(re, im)| {
9333                if im.abs() < IMAG_TOL {
9334                    Value::Scalar(re)
9335                } else {
9336                    Value::Complex(re, im)
9337                }
9338            })
9339            .collect();
9340        Value::Cell(vals)
9341    }
9342}
9343
9344/// Computes the characteristic polynomial of a square matrix using the
9345/// Faddeev-LeVerrier algorithm.
9346///
9347/// Returns coefficients `[1, c_{n-1}, …, c_0]` in descending degree order.
9348fn characteristic_poly(a: &Array2<f64>) -> Result<Vec<f64>, String> {
9349    let n = a.nrows();
9350    if a.ncols() != n {
9351        return Err("poly: matrix must be square".to_string());
9352    }
9353    if n == 0 {
9354        return Ok(vec![1.0]);
9355    }
9356    let mut coeffs = vec![0.0_f64; n + 1];
9357    coeffs[0] = 1.0;
9358    let mut nk = Array2::<f64>::eye(n); // N_0 = I
9359    for (k, coeff) in coeffs.iter_mut().enumerate().skip(1) {
9360        let ank = a.dot(&nk); // A * N_{k-1}
9361        let tr: f64 = (0..n).map(|i| ank[[i, i]]).sum();
9362        let ak = -tr / k as f64;
9363        *coeff = ak;
9364        nk = ank; // N_k = A*N_{k-1} + a_k*I
9365        for i in 0..n {
9366            nk[[i, i]] += ak;
9367        }
9368    }
9369    Ok(coeffs)
9370}
9371
9372/// Back-substitution solver for upper-triangular system `R * x = b`.
9373fn poly_back_sub(r: &Array2<f64>, b: &[f64]) -> Result<Vec<f64>, String> {
9374    let n = r.nrows();
9375    let mut x = vec![0.0_f64; n];
9376    for i in (0..n).rev() {
9377        let mut s = b[i];
9378        for j in (i + 1)..n {
9379            s -= r[[i, j]] * x[j];
9380        }
9381        if r[[i, i]].abs() < 1e-14 {
9382            return Err(
9383                "polyfit: Vandermonde matrix is rank-deficient; reduce polynomial degree"
9384                    .to_string(),
9385            );
9386        }
9387        x[i] = s / r[[i, i]];
9388    }
9389    Ok(x)
9390}
9391
9392/// Evaluates `interp1` at a single query point `xi` using the given `method`.
9393///
9394/// Returns `NaN` for queries outside `[x[0], x[n-1]]`.
9395fn interp1_at(x: &[f64], y: &[f64], xi: f64, method: &str) -> f64 {
9396    let n = x.len();
9397    if xi < x[0] || xi > x[n - 1] {
9398        return f64::NAN;
9399    }
9400    // Index of the leftmost knot ≤ xi (in [0, n-1])
9401    let lo = x.partition_point(|&xk| xk <= xi).saturating_sub(1);
9402    // For methods that need a right neighbour, clamp to n-2
9403    let lo2 = lo.min(n - 2);
9404    match method {
9405        "nearest" => {
9406            if lo == n - 1 {
9407                return y[n - 1];
9408            }
9409            if (xi - x[lo2]) <= (x[lo2 + 1] - xi) {
9410                y[lo2]
9411            } else {
9412                y[lo2 + 1]
9413            }
9414        }
9415        "previous" => y[lo],
9416        "next" => {
9417            if lo == n - 1 || xi == x[lo] {
9418                y[lo]
9419            } else {
9420                y[lo2 + 1]
9421            }
9422        }
9423        _ => {
9424            // "linear" (default)
9425            if lo == n - 1 {
9426                return y[n - 1];
9427            }
9428            let t = (xi - x[lo2]) / (x[lo2 + 1] - x[lo2]);
9429            y[lo2] + t * (y[lo2 + 1] - y[lo2])
9430        }
9431    }
9432}
9433
9434#[cfg(test)]
9435#[path = "eval_tests.rs"]
9436mod tests;