Skip to main content

ccalc_engine/
eval.rs

1use std::cell::{Cell, RefCell};
2use std::collections::{HashMap, HashSet};
3
4use indexmap::IndexMap;
5use ndarray::Array2;
6use num_complex::Complex;
7use rand::{Rng, SeedableRng, rngs::SmallRng};
8
9use crate::env::{Env, LambdaFn, Value};
10use crate::io::IoContext;
11
12// ── User function call hook ──────────────────────────────────────────────────
13
14/// Signature for the hook that executes named user-defined functions.
15///
16/// Registered once by `exec::init()` before the REPL loop starts.
17/// Called by `eval_inner` when a `Value::Function` is invoked.
18/// `name` is the function name (from the call site); `caller_env` is passed so
19/// the function body can access other user-defined functions (enabling recursion
20/// and mutual recursion).
21pub type FnCallHook = fn(
22    name: &str,
23    func: &Value,
24    args: &[Value],
25    caller_env: &Env,
26    io: &mut IoContext,
27) -> Result<Value, String>;
28
29thread_local! {
30    static FN_CALL_HOOK: Cell<Option<FnCallHook>> = const { Cell::new(None) };
31}
32
33/// Registers the hook that executes named user-defined functions.
34///
35/// Must be called by `exec::init()` before any user function can be called.
36pub fn set_fn_call_hook(f: FnCallHook) {
37    FN_CALL_HOOK.with(|c| c.set(Some(f)));
38}
39
40// ── Autoload hook ───────────────────────────────────────────────────────────
41
42/// Signature for the hook that auto-loads a function file by name.
43///
44/// Called by `eval_inner` when a name is not found in the environment and not
45/// a built-in. The hook searches for `<name>.calc` / `<name>.m` on the path
46/// and, if found, inserts the primary function into the autoload cache via
47/// [`autoload_cache_insert`]. Returns `true` if the function was loaded.
48pub type AutoloadHook = fn(name: &str) -> bool;
49
50thread_local! {
51    static AUTOLOAD_HOOK: Cell<Option<AutoloadHook>> = const { Cell::new(None) };
52    /// Cache of autoloaded functions — populated by the autoload hook, read by eval_inner.
53    static AUTOLOAD_CACHE: RefCell<Env> = RefCell::new(Env::new());
54    /// Names that were searched and NOT found on the path — avoids repeated filesystem
55    /// stat() calls for built-in names (sin, cos, …) inside tight loops.
56    static AUTOLOAD_MISS_CACHE: RefCell<HashSet<String>> = RefCell::new(HashSet::new());
57}
58
59/// Registers the autoload hook. Called by `exec::init()`.
60pub fn set_autoload_hook(f: AutoloadHook) {
61    AUTOLOAD_HOOK.with(|c| c.set(Some(f)));
62}
63
64/// Inserts a function into the autoload cache. Called by `exec::try_autoload`.
65pub fn autoload_cache_insert(name: String, val: Value) {
66    AUTOLOAD_CACHE.with(|c| c.borrow_mut().insert(name, val));
67}
68
69/// Clears the negative-autoload miss cache.
70///
71/// Call this when the session path changes so that names previously not found
72/// on the old path can be re-searched on the updated path.
73pub fn clear_autoload_miss_cache() {
74    AUTOLOAD_MISS_CACHE.with(|c| c.borrow_mut().clear());
75}
76
77/// Returns an autoloaded function by name, triggering the autoload hook if needed.
78///
79/// Checks the positive cache first, then the negative (miss) cache, and only fires
80/// the filesystem hook when the name has not been tried before.  Returns `None` when
81/// neither the cache nor the hook can resolve the name.
82pub fn resolve_autoloaded(name: &str) -> Option<Value> {
83    let cached = AUTOLOAD_CACHE.with(|c| c.borrow().get(name).cloned());
84    if cached.is_some() {
85        return cached;
86    }
87    if AUTOLOAD_MISS_CACHE.with(|c| c.borrow().contains(name)) {
88        return None;
89    }
90    let hook = AUTOLOAD_HOOK.with(|c| c.get());
91    if let Some(f) = hook {
92        f(name);
93    }
94    let found = AUTOLOAD_CACHE.with(|c| c.borrow().get(name).cloned());
95    if found.is_none() {
96        AUTOLOAD_MISS_CACHE.with(|c| c.borrow_mut().insert(name.to_string()));
97    }
98    found
99}
100
101// ── Eval-string hook (set by exec::init) ────────────────────────────────────
102
103/// Executes a code string against an immutable snapshot of the env and returns `ans`.
104///
105/// Used by `call_builtin` when `eval()` appears in expression context
106/// (e.g. `y = eval('2+2')`). Env mutations inside the string do **not** persist —
107/// the hook executes against a clone. For env-mutating eval, use `eval()` as a
108/// standalone statement.
109pub type EvalStrHook = fn(code: &str, env: &Env) -> Result<Value, String>;
110
111thread_local! {
112    static EVAL_STR_HOOK: Cell<Option<EvalStrHook>> = const { Cell::new(None) };
113}
114
115/// Registers the hook that executes a code string in expression context.
116///
117/// Must be called by `exec::init()` before any `eval()` expression-context call.
118pub fn set_eval_str_hook(f: EvalStrHook) {
119    EVAL_STR_HOOK.with(|c| c.set(Some(f)));
120}
121
122fn call_eval_str_hook(code: &str, env: &Env) -> Result<Value, String> {
123    match EVAL_STR_HOOK.with(|c| c.get()) {
124        Some(hook) => hook(code, env),
125        None => Err("eval: exec::init() not called".to_string()),
126    }
127}
128
129// ── Tic timer (thread-local) ─────────────────────────────────────────────────
130
131thread_local! {
132    /// Start time set by the most recent `tic` call.
133    static TIC_TIME: Cell<Option<std::time::Instant>> = const { Cell::new(None) };
134}
135
136// ── Last error (thread-local) ────────────────────────────────────────────────
137
138thread_local! {
139    static LAST_ERR: RefCell<String> = const { RefCell::new(String::new()) };
140}
141
142/// Sets the last-error string (called on every caught runtime error).
143pub fn set_last_err(msg: &str) {
144    LAST_ERR.with(|e| *e.borrow_mut() = msg.to_string());
145}
146
147/// Returns the last-error string.
148pub fn get_last_err() -> String {
149    LAST_ERR.with(|e| e.borrow().clone())
150}
151
152// ── Nargout (number of expected outputs, set by exec_stmts) ─────────────────
153
154thread_local! {
155    static NARGOUT: Cell<usize> = const { Cell::new(1) };
156}
157
158/// Sets the number of output values requested by the calling assignment statement.
159///
160/// Called by `exec_stmts` before evaluating the RHS expression, so that
161/// multi-output built-ins (`eig`, `svd`, `lu`, `qr`) can determine whether to
162/// return a full `Value::Tuple` or a single value.
163pub fn set_nargout(n: usize) {
164    NARGOUT.with(|c| c.set(n));
165}
166
167fn get_nargout() -> usize {
168    NARGOUT.with(|c| c.get())
169}
170
171// ── Display context (thread-local, set by exec_stmts) ────────────────────────
172
173thread_local! {
174    static DISPLAY_FMT:     RefCell<FormatMode> = const { RefCell::new(FormatMode::Short) };
175    static DISPLAY_BASE:    Cell<Base>           = const { Cell::new(Base::Dec) };
176    static DISPLAY_COMPACT: Cell<bool>           = const { Cell::new(false) };
177}
178
179/// Sets the display context used when executing function bodies.
180///
181/// Called at the start of `exec_stmts` so that named functions called from
182/// within a block inherit the caller's display settings.
183pub fn set_display_ctx(fmt: &FormatMode, base: Base, compact: bool) {
184    DISPLAY_FMT.with(|f| *f.borrow_mut() = fmt.clone());
185    DISPLAY_BASE.with(|b| b.set(base));
186    DISPLAY_COMPACT.with(|c| c.set(compact));
187}
188
189/// Returns the current display format mode stored in the thread-local context.
190pub fn get_display_fmt() -> FormatMode {
191    DISPLAY_FMT.with(|f| f.borrow().clone())
192}
193
194/// Returns the current numeric base stored in the thread-local context.
195pub fn get_display_base() -> Base {
196    DISPLAY_BASE.with(|b| b.get())
197}
198
199/// Returns the current compact flag stored in the thread-local context.
200pub fn get_display_compact() -> bool {
201    DISPLAY_COMPACT.with(|c| c.get())
202}
203
204// ── Global variable store ────────────────────────────────────────────────────
205
206thread_local! {
207    /// Shared global workspace — variables declared `global` in any scope live here.
208    ///
209    /// Persists for the lifetime of the process. Each call to `global x` in any scope
210    /// makes `x` refer to this store rather than the local environment.
211    static GLOBAL_ENV: RefCell<Env> = RefCell::new(Env::new());
212
213    /// Stack of per-scope global name sets.
214    ///
215    /// Frame 0 = top level / script scope; each `call_user_function` call pushes a new frame
216    /// and pops it on return. `global x` in a scope adds `x` to the current (top) frame.
217    static GLOBAL_NAMES_STACK: RefCell<Vec<HashSet<String>>> =
218        RefCell::new(vec![HashSet::new()]);
219}
220
221/// Pushes an empty global-names frame (called on function entry by `exec.rs`).
222pub fn global_frame_push() {
223    GLOBAL_NAMES_STACK.with(|s| s.borrow_mut().push(HashSet::new()));
224}
225
226/// Pops the top global-names frame (called on function exit by `exec.rs`).
227pub fn global_frame_pop() {
228    GLOBAL_NAMES_STACK.with(|s| {
229        s.borrow_mut().pop();
230    });
231}
232
233/// Declares `name` as global in the current scope.
234pub fn global_declare(name: &str) {
235    GLOBAL_NAMES_STACK.with(|s| {
236        if let Some(frame) = s.borrow_mut().last_mut() {
237            frame.insert(name.to_string());
238        }
239    });
240}
241
242/// Returns `true` if `name` is declared global in the innermost active scope.
243pub fn is_global(name: &str) -> bool {
244    GLOBAL_NAMES_STACK.with(|s| s.borrow().last().is_some_and(|f| f.contains(name)))
245}
246
247/// Gets a value from the shared global store.
248pub fn global_get(name: &str) -> Option<Value> {
249    GLOBAL_ENV.with(|e| e.borrow().get(name).cloned())
250}
251
252/// Sets a value in the shared global store.
253pub fn global_set(name: &str, val: Value) {
254    GLOBAL_ENV.with(|e| e.borrow_mut().insert(name.to_string(), val));
255}
256
257/// Initialises `name` in the global store to `Scalar(0.0)` if not already present.
258pub fn global_init_if_absent(name: &str) {
259    GLOBAL_ENV.with(|e| {
260        e.borrow_mut()
261            .entry(name.to_string())
262            .or_insert(Value::Scalar(0.0));
263    });
264}
265
266/// Refreshes all names declared global in the current scope from `GLOBAL_ENV` into `env`.
267///
268/// Called at the end of `exec_stmts` to ensure that modifications made to global variables
269/// inside called functions are visible to the current scope's environment.
270pub fn global_refresh_into_env(env: &mut crate::env::Env) {
271    GLOBAL_NAMES_STACK.with(|s| {
272        GLOBAL_ENV.with(|ge| {
273            if let Some(frame) = s.borrow().last() {
274                let store = ge.borrow();
275                for name in frame {
276                    if let Some(val) = store.get(name) {
277                        env.insert(name.clone(), val.clone());
278                    }
279                }
280            }
281        });
282    });
283}
284
285// ── Persistent variable store ────────────────────────────────────────────────
286
287thread_local! {
288    /// Persistent variable values — keyed by `"funcname\x00varname"`.
289    ///
290    /// Values survive individual function calls and are restored on the next call
291    /// to the same function.
292    static PERSISTENT_STORE: RefCell<HashMap<String, Value>> =
293        RefCell::new(HashMap::new());
294
295    /// Stack of function names for constructing persistent-store keys.
296    ///
297    /// Empty string = top-level scope. `call_user_function` pushes the function name
298    /// before executing the body and pops it on return.
299    static FUNC_NAME_STACK: RefCell<Vec<String>> =
300        RefCell::new(vec![String::new()]);
301
302    /// Stack of per-scope persistent name sets — mirrors `GLOBAL_NAMES_STACK`.
303    static PERSISTENT_NAMES_STACK: RefCell<Vec<HashSet<String>>> =
304        RefCell::new(vec![HashSet::new()]);
305}
306
307/// Pushes a function scope for persistent tracking (called on function entry).
308pub fn persistent_frame_push(func_name: &str) {
309    FUNC_NAME_STACK.with(|s| s.borrow_mut().push(func_name.to_string()));
310    PERSISTENT_NAMES_STACK.with(|s| s.borrow_mut().push(HashSet::new()));
311}
312
313/// Pops the persistent frame and returns `(func_name, declared_persistent_names)`.
314pub fn persistent_frame_pop() -> (String, HashSet<String>) {
315    let func_name = FUNC_NAME_STACK.with(|s| s.borrow_mut().pop().unwrap_or_default());
316    let names = PERSISTENT_NAMES_STACK.with(|s| s.borrow_mut().pop().unwrap_or_default());
317    (func_name, names)
318}
319
320/// Declares `name` as persistent in the current function scope.
321pub fn persistent_declare(name: &str) {
322    PERSISTENT_NAMES_STACK.with(|s| {
323        if let Some(frame) = s.borrow_mut().last_mut() {
324            frame.insert(name.to_string());
325        }
326    });
327}
328
329/// Gets a saved persistent value for `(func_name, var_name)`.
330pub fn persistent_load(func_name: &str, var_name: &str) -> Option<Value> {
331    let key = format!("{func_name}\x00{var_name}");
332    PERSISTENT_STORE.with(|s| s.borrow().get(&key).cloned())
333}
334
335/// Saves a persistent value for `(func_name, var_name)`.
336pub fn persistent_save(func_name: &str, var_name: &str, val: Value) {
337    let key = format!("{func_name}\x00{var_name}");
338    PERSISTENT_STORE.with(|s| s.borrow_mut().insert(key, val));
339}
340
341/// Returns the name of the currently executing function (top of `FUNC_NAME_STACK`).
342///
343/// Returns an empty string when executing at the top level (REPL / script scope).
344pub fn current_func_name() -> String {
345    FUNC_NAME_STACK.with(|s| s.borrow().last().cloned().unwrap_or_default())
346}
347
348/// Returns `true` if `name` is declared `persistent` in the current function frame.
349pub fn is_persistent(name: &str) -> bool {
350    PERSISTENT_NAMES_STACK.with(|s| s.borrow().last().is_some_and(|frame| frame.contains(name)))
351}
352
353// ── Random-number state ──────────────────────────────────────────────────────
354
355thread_local! {
356    /// Per-thread PRNG used by `rand`, `randn`, and `randi`.
357    ///
358    /// Seeded from OS entropy on first use. Reseed with `rng(seed)` or `rng('shuffle')`.
359    static RNG: RefCell<SmallRng> = RefCell::new(SmallRng::from_entropy());
360}
361
362/// Reseeds the thread-local RNG with the given 64-bit seed.
363pub fn rng_seed(seed: u64) {
364    RNG.with(|r| *r.borrow_mut() = SmallRng::seed_from_u64(seed));
365}
366
367/// Reseeds the thread-local RNG from OS entropy.
368pub fn rng_shuffle() {
369    RNG.with(|r| *r.borrow_mut() = SmallRng::from_entropy());
370}
371
372/// Generates one uniform [0, 1) sample.
373fn rand_uniform() -> f64 {
374    RNG.with(|r| r.borrow_mut().gen_range(0.0_f64..1.0))
375}
376
377/// Generates one standard-normal sample via the Box-Muller transform.
378fn rand_normal() -> f64 {
379    let u1 = rand_uniform().max(f64::EPSILON);
380    let u2 = rand_uniform();
381    (-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos()
382}
383
384// ── AST types ────────────────────────────────────────────────────────────────
385
386/// An expression node in the AST.
387///
388/// Produced by the parser and consumed by [`eval`] / [`eval_with_io`].
389#[derive(Debug, Clone)]
390pub enum Expr {
391    /// A numeric literal (e.g. `3`, `2.5`, `1e-3`).
392    Number(f64),
393    /// A variable or constant reference (e.g. `x`, `pi`, `ans`).
394    Var(String),
395    /// Arithmetic negation: `-expr`.
396    UnaryMinus(Box<Expr>),
397    /// Logical NOT: `~expr`. Result is 1.0 if expr == 0.0, else 0.0.
398    UnaryNot(Box<Expr>),
399    /// Binary operation: `lhs op rhs`.
400    BinOp(Box<Expr>, Op, Box<Expr>),
401    /// Function call or variable indexing: `name(arg1, arg2, ...)`.
402    ///
403    /// Disambiguation happens at eval time: if `name` exists in the environment
404    /// it is treated as indexing, otherwise as a built-in or user function call.
405    Call(String, Vec<Expr>),
406    /// Matrix literal: `[row1; row2; ...]` where each row is a list of expressions.
407    Matrix(Vec<Vec<Expr>>),
408    /// Conjugate transpose: `A'`. For complex scalars, returns the conjugate.
409    Transpose(Box<Expr>),
410    /// Range expression: `start:stop` or `start:step:stop`.
411    /// Evaluates to a 1×N row vector.
412    Range(Box<Expr>, Option<Box<Expr>>, Box<Expr>),
413    /// Bare `:` used as an all-elements index in `A(:,j)` or `A(i,:)`.
414    /// Only valid as an argument inside an indexing expression.
415    Colon,
416    /// Single-quoted char array literal.
417    StrLiteral(String),
418    /// Double-quoted string object literal.
419    StringObjLiteral(String),
420    /// Anonymous function: `@(params) body_expr`.
421    ///
422    /// At evaluation time this is converted to `Value::Lambda`, capturing the
423    /// current environment as a lexical closure.
424    Lambda {
425        /// Parameter names in declaration order (e.g. `["x", "n"]`).
426        params: Vec<String>,
427        /// Body expression evaluated when the lambda is called.
428        body: Box<Expr>,
429        /// Source text for display (e.g. `@(x) x.^2 + 1`), stored at parse time.
430        source: String,
431    },
432    /// Non-conjugate (plain) transpose: `A.'`.
433    ///
434    /// Transposes without complex conjugation. For real matrices, identical to `A'`.
435    /// For complex: `z.'` returns `z` unchanged (no sign flip on imaginary part).
436    PlainTranspose(Box<Expr>),
437    /// Cell array literal: `{e1, e2, e3}`.
438    ///
439    /// Evaluates each element and produces `Value::Cell`.
440    CellLiteral(Vec<Expr>),
441    /// Cell array brace-indexing: `c{i}`.
442    ///
443    /// The first expression must evaluate to `Value::Cell`; the second is the
444    /// 1-based integer index.
445    CellIndex(Box<Expr>, Box<Expr>),
446    /// Function handle: `@funcname`.
447    ///
448    /// Produces a `Value::Lambda` that forwards its arguments to the named
449    /// built-in or user function.
450    FuncHandle(String),
451    /// Struct field read: `s.field` or chained `s.a.b` (parsed as `FieldGet(FieldGet(s,"a"),"b")`).
452    ///
453    /// At eval time the base expression must evaluate to `Value::Struct`.
454    FieldGet(Box<Expr>, String),
455    /// Dynamic struct field read: `s.(fname)` where `fname` evaluates to a string.
456    ///
457    /// At eval time the field expression must evaluate to `Value::Str` or `Value::StringObj`.
458    DynFieldGet(Box<Expr>, Box<Expr>),
459    /// Package-qualified function call: `pkg.func(args)` or `pkg.sub.func(args)`.
460    ///
461    /// `segments` holds the dot-separated name components, e.g. `["utils", "my_function"]`.
462    /// At eval time:
463    /// - If `segments[0]` is in the environment (a struct or callable), the chain is followed
464    ///   as field accesses and the final value is called with the given arguments.
465    /// - Otherwise, the segments are treated as a package call: the autoload hook searches
466    ///   for `+utils/my_function.calc` (or `+utils/+sub/func.calc` for nested packages)
467    ///   on the session path and loads the function on demand.
468    DotCall(Vec<String>, Vec<Expr>),
469    /// Not-a-Time sentinel: `NaT`. Evaluates to `Value::DateTime(f64::NAN)`.
470    NaT,
471}
472
473/// A binary operator used in [`Expr::BinOp`].
474#[derive(Debug, Clone)]
475pub enum Op {
476    /// Addition: `a + b` or element-wise matrix addition.
477    Add,
478    /// Subtraction: `a - b` or element-wise matrix subtraction.
479    Sub,
480    /// Multiplication: scalar `a * b` or matrix product `A * B`.
481    Mul,
482    /// Division: scalar `a / b` or matrix right-division `A / B` (solves `X * B = A`).
483    Div,
484    /// Exponentiation: scalar `a ^ b` or matrix power `A ^ n`.
485    Pow,
486    /// Element-wise multiplication: `A .* B`.
487    ElemMul,
488    /// Element-wise division: `A ./ B`.
489    ElemDiv,
490    /// Element-wise exponentiation: `A .^ B`.
491    ElemPow,
492    // --- Comparison (element-wise, return 0.0/1.0) ---
493    /// Equality comparison: `a == b`. Returns 1.0 if equal, 0.0 otherwise.
494    Eq,
495    /// Inequality comparison: `a ~= b`. Returns 1.0 if not equal, 0.0 otherwise.
496    NotEq,
497    /// Less-than comparison: `a < b`.
498    Lt,
499    /// Greater-than comparison: `a > b`.
500    Gt,
501    /// Less-than-or-equal comparison: `a <= b`.
502    LtEq,
503    /// Greater-than-or-equal comparison: `a >= b`.
504    GtEq,
505    // --- Short-circuit logical (scalars only) ---
506    /// Short-circuit logical AND: `a && b`. Only evaluates `b` if `a` is truthy.
507    And,
508    /// Short-circuit logical OR: `a || b`. Only evaluates `b` if `a` is falsy.
509    Or,
510    // --- Element-wise logical (matrices allowed, no short-circuit) ---
511    /// Element-wise logical AND: `A & B`. Evaluates both sides; works on matrices.
512    ElemAnd,
513    /// Element-wise logical OR: `A | B`. Evaluates both sides; works on matrices.
514    ElemOr,
515    /// Left division: `A \ b` solves `A*x = b`. Scalar: `a \ b = b / a`.
516    LDiv,
517}
518
519/// The numeric base used when displaying integer-valued scalars.
520#[derive(Debug, Clone, Copy, PartialEq, Default)]
521pub enum Base {
522    /// Decimal (base 10) — the default.
523    #[default]
524    Dec,
525    /// Hexadecimal (base 16), prefix `0x` (e.g. `0xff`).
526    Hex,
527    /// Binary (base 2), prefix `0b` (e.g. `0b1010`).
528    Bin,
529    /// Octal (base 8), prefix `0o` (e.g. `0o17`).
530    Oct,
531}
532
533/// Controls how numbers are displayed (MATLAB-compatible format modes).
534#[derive(Debug, Clone, PartialEq)]
535pub enum FormatMode {
536    /// 5 significant digits, auto fixed/scientific (MATLAB `format short`).
537    Short,
538    /// 15 significant digits, auto fixed/scientific (MATLAB `format long`).
539    Long,
540    /// Always scientific notation, 4 decimal places — 5 sig digits.
541    ShortE,
542    /// Always scientific notation, 14 decimal places — 15 sig digits.
543    LongE,
544    /// Same as `Short` for scalars (MATLAB `format shortG`).
545    ShortG,
546    /// Same as `Long` for scalars (MATLAB `format longG`).
547    LongG,
548    /// Fixed 2 decimal places — currency (MATLAB `format bank`).
549    Bank,
550    /// Rational approximation `p/q` (MATLAB `format rat`).
551    Rat,
552    /// IEEE 754 hexadecimal bit pattern, 16 uppercase hex digits (MATLAB `format hex`).
553    Hex,
554    /// Sign character only: `+`, `-`, or ` ` for zero (MATLAB `format +`).
555    Plus,
556    /// N decimal places, auto fixed/scientific — legacy precision= setting.
557    Custom(usize),
558}
559
560impl Default for FormatMode {
561    fn default() -> Self {
562        FormatMode::Custom(10)
563    }
564}
565
566impl FormatMode {
567    /// Human-readable name for display in `config` / status messages.
568    pub fn name(&self) -> String {
569        match self {
570            FormatMode::Short => "short".to_string(),
571            FormatMode::Long => "long".to_string(),
572            FormatMode::ShortE => "shortE".to_string(),
573            FormatMode::LongE => "longE".to_string(),
574            FormatMode::ShortG => "shortG".to_string(),
575            FormatMode::LongG => "longG".to_string(),
576            FormatMode::Bank => "bank".to_string(),
577            FormatMode::Rat => "rat".to_string(),
578            FormatMode::Hex => "hex".to_string(),
579            FormatMode::Plus => "+".to_string(),
580            FormatMode::Custom(n) => format!("custom({n})"),
581        }
582    }
583}
584
585/// Evaluates an expression without file I/O context.
586/// This is the public API used by tests and non-I/O evaluation paths.
587pub fn eval(expr: &Expr, env: &Env) -> Result<Value, String> {
588    eval_inner(expr, env, None)
589}
590
591/// Evaluates an expression with an I/O context (file descriptor table).
592/// Used by the REPL to support `fopen`/`fclose`/`fgetl`/`fgets`/`fprintf(fd,...)`.
593pub fn eval_with_io(expr: &Expr, env: &Env, io: &mut IoContext) -> Result<Value, String> {
594    eval_inner(expr, env, Some(io))
595}
596
597fn eval_inner(expr: &Expr, env: &Env, mut io: Option<&mut IoContext>) -> Result<Value, String> {
598    match expr {
599        Expr::Number(n) => Ok(Value::Scalar(*n)),
600        Expr::Var(name) => env.get(name).cloned().ok_or(()).or_else(|_| {
601            // Check the shared global store when the name is declared global in this scope.
602            if is_global(name)
603                && let Some(val) = global_get(name)
604            {
605                return Ok(val);
606            }
607            // 'e' falls back to Euler's number if not defined in env
608            if name == "e" {
609                return Ok(Value::Scalar(std::f64::consts::E));
610            }
611            // Try as a zero-argument built-in call (e.g., `tic`, `toc` written without parens).
612            if let Ok(val) = call_builtin(name, &[], env, io.as_deref_mut()) {
613                return Ok(val);
614            }
615            let hint = suggest_similar(name, env);
616            match hint {
617                Some(s) => Err(format!("Undefined variable '{name}'; did you mean '{s}'?")),
618                None => Err(format!("Undefined variable: '{name}'")),
619            }
620        }),
621        Expr::UnaryMinus(e) => match eval_inner(e, env, io)? {
622            Value::Void => Err("Unary minus is not applicable to void".to_string()),
623            Value::Scalar(n) => Ok(Value::Scalar(-n)),
624            Value::Matrix(m) => Ok(Value::Matrix(Box::new(m.mapv(|x| -x)))),
625            Value::Complex(re, im) => Ok(Value::Complex(-re, -im)),
626            Value::ComplexMatrix(m) => Ok(Value::ComplexMatrix(Box::new(m.mapv(|c| -c)))),
627            Value::Str(s) => match str_to_numeric(&s) {
628                Value::Scalar(n) => Ok(Value::Scalar(-n)),
629                Value::Matrix(m) => Ok(Value::Matrix(Box::new(m.mapv(|x| -x)))),
630                _ => unreachable!(),
631            },
632            Value::StringObj(_) => {
633                Err("Unary minus is not applicable to string objects".to_string())
634            }
635            Value::Lambda(_)
636            | Value::Function(_)
637            | Value::Tuple(_)
638            | Value::Cell(_)
639            | Value::Struct(_)
640            | Value::StructArray(_)
641            | Value::DateTime(_)
642            | Value::Duration(_)
643            | Value::DateTimeArray(_)
644            | Value::DurationArray(_)
645            | Value::Map(_) => Err("Unary minus is not applicable to this type".to_string()),
646        },
647        Expr::UnaryNot(e) => match eval_inner(e, env, io)? {
648            Value::Void => Err("Logical NOT is not applicable to void".to_string()),
649            Value::Scalar(n) => Ok(Value::Scalar(if n == 0.0 { 1.0 } else { 0.0 })),
650            Value::Matrix(m) => Ok(Value::Matrix(Box::new(
651                m.mapv(|x| if x == 0.0 { 1.0 } else { 0.0 }),
652            ))),
653            Value::Complex(re, im) => Ok(Value::Scalar(if re == 0.0 && im == 0.0 {
654                1.0
655            } else {
656                0.0
657            })),
658            Value::ComplexMatrix(m) => {
659                Ok(Value::Matrix(Box::new(m.mapv(|c| {
660                    if c.re == 0.0 && c.im == 0.0 { 1.0 } else { 0.0 }
661                }))))
662            }
663            Value::Str(s) => match str_to_numeric(&s) {
664                Value::Scalar(n) => Ok(Value::Scalar(if n == 0.0 { 1.0 } else { 0.0 })),
665                Value::Matrix(m) => Ok(Value::Matrix(Box::new(
666                    m.mapv(|x| if x == 0.0 { 1.0 } else { 0.0 }),
667                ))),
668                _ => unreachable!(),
669            },
670            Value::StringObj(_) => {
671                Err("Logical NOT is not applicable to string objects".to_string())
672            }
673            Value::Lambda(_)
674            | Value::Function(_)
675            | Value::Tuple(_)
676            | Value::Cell(_)
677            | Value::Struct(_)
678            | Value::StructArray(_)
679            | Value::DateTime(_)
680            | Value::Duration(_)
681            | Value::DateTimeArray(_)
682            | Value::DurationArray(_)
683            | Value::Map(_) => Err("Logical NOT is not applicable to this type".to_string()),
684        },
685        Expr::BinOp(left, op, right) => {
686            let l = eval_inner(left, env, io.as_deref_mut())?;
687            let r = eval_inner(right, env, io)?;
688            eval_binop(l, op, r)
689        }
690        Expr::Call(name, args) => {
691            // try(expr, default) — special form: evaluate expr; on error evaluate default.
692            // Arguments are NOT pre-evaluated; lazy semantics.
693            if name == "try" && args.len() == 2 {
694                return match eval_inner(&args[0], env, io.as_deref_mut()) {
695                    Ok(v) => Ok(v),
696                    Err(msg) => {
697                        set_last_err(&msg);
698                        eval_inner(&args[1], env, io.as_deref_mut())
699                    }
700                };
701            }
702
703            // If the name resolves to a variable in env, check its type.
704            // User functions (Lambda, Function) are called; other values are indexed.
705            // Variables shadow built-in function names (Octave semantics).
706            //
707            // Non-function variables are forwarded via borrow (no clone) to avoid
708            // copying large matrix values on every indexed read (e.g. x(k) in a loop).
709            if let Some(env_val) = env.get(name) {
710                if !matches!(env_val, Value::Lambda(_) | Value::Function(_)) {
711                    return eval_index(env_val, args, env);
712                }
713                // Lambda/Function: clone is cheap (Rc for Lambda, Strings for Function).
714                let val = env_val.clone();
715                match &val {
716                    Value::Lambda(f) => {
717                        // Evaluate arguments and call the closure directly.
718                        // Empty call → inject ans (convenience: sq() = sq(ans)).
719                        let mut evaled = Vec::with_capacity(args.len().max(1));
720                        for a in args {
721                            evaled.push(eval_inner(a, env, io.as_deref_mut())?);
722                        }
723                        if evaled.is_empty() {
724                            evaled.push(env.get("ans").cloned().unwrap_or(Value::Scalar(0.0)));
725                        }
726                        let f = f.clone();
727                        return f.0(&evaled, io);
728                    }
729                    Value::Function(_) => {
730                        // Evaluate arguments and dispatch to the registered hook in exec.rs.
731                        // User functions receive the raw arg list — NO ans injection. Empty call
732                        // means no arguments (varargin = {}), matching MATLAB semantics.
733                        let mut evaled = Vec::with_capacity(args.len());
734                        for a in args {
735                            evaled.push(eval_inner(a, env, io.as_deref_mut())?);
736                        }
737                        return match io.as_deref_mut() {
738                            Some(io_ref) => FN_CALL_HOOK.with(|c| match c.get() {
739                                Some(hook) => hook(name, &val, &evaled, env, io_ref),
740                                None => Err(format!(
741                                    "'{name}': user function execution not initialized \
742                                         (call exec::init() first)"
743                                )),
744                            }),
745                            None => {
746                                // No I/O context — create a temporary one (functions that do
747                                // file I/O in this path will silently fail to open files).
748                                let mut tmp_io = IoContext::new();
749                                FN_CALL_HOOK.with(|c| match c.get() {
750                                    Some(hook) => hook(name, &val, &evaled, env, &mut tmp_io),
751                                    None => Err(format!(
752                                        "'{name}': user function execution not initialized"
753                                    )),
754                                })
755                            }
756                        };
757                    }
758                    _ => unreachable!(),
759                }
760            }
761            // Autoload: search for <name>.calc / <name>.m if not in env.
762            // Check positive cache → negative (miss) cache → fire filesystem hook.
763            // Names that fail the hook are recorded in the miss cache so the
764            // filesystem is not searched again within the same session.
765            let autoloaded_val = AUTOLOAD_CACHE
766                .with(|c| c.borrow().get(name).cloned())
767                .or_else(|| {
768                    if AUTOLOAD_MISS_CACHE.with(|c| c.borrow().contains(name.as_str())) {
769                        return None;
770                    }
771                    let loaded = AUTOLOAD_HOOK
772                        .with(|c| c.get())
773                        .is_some_and(|hook| hook(name));
774                    if loaded {
775                        AUTOLOAD_CACHE.with(|c| c.borrow().get(name).cloned())
776                    } else {
777                        AUTOLOAD_MISS_CACHE.with(|c| c.borrow_mut().insert(name.to_string()));
778                        None
779                    }
780                });
781            if let Some(val) = autoloaded_val {
782                let mut evaled = Vec::with_capacity(args.len());
783                for a in args {
784                    evaled.push(eval_inner(a, env, io.as_deref_mut())?);
785                }
786                return match io.as_deref_mut() {
787                    Some(io_ref) => FN_CALL_HOOK.with(|c| match c.get() {
788                        Some(hook) => hook(name, &val, &evaled, env, io_ref),
789                        None => Err(format!("'{name}': exec::init() not called")),
790                    }),
791                    None => {
792                        let mut tmp_io = IoContext::new();
793                        FN_CALL_HOOK.with(|c| match c.get() {
794                            Some(hook) => hook(name, &val, &evaled, env, &mut tmp_io),
795                            None => Err(format!("'{name}': exec::init() not called")),
796                        })
797                    }
798                };
799            }
800
801            // Builtin path: empty call → inject ans (sqrt() = sqrt(ans)).
802            let mut evaled = Vec::with_capacity(args.len().max(1));
803            for a in args {
804                evaled.push(eval_inner(a, env, io.as_deref_mut())?);
805            }
806            // Don't inject ans for functions that take explicit struct/cell args
807            // or constructors where zero args is meaningful.
808            let no_ans_inject = matches!(
809                name.as_str(),
810                "struct"
811                    | "fieldnames"
812                    | "isfield"
813                    | "rmfield"
814                    | "isstruct"
815                    | "cell"
816                    | "iscell"
817                    | "isempty"
818                    | "cellfun"
819                    | "error"
820                    | "warning"
821                    | "lasterr"
822                    | "pcall"
823                    | "rand"
824                    | "randn"
825                    | "rng"
826                    | "tic"
827                    | "toc"
828            );
829            if evaled.is_empty() && !no_ans_inject {
830                evaled.push(env.get("ans").cloned().unwrap_or(Value::Scalar(0.0)));
831            }
832            call_builtin(name, &evaled, env, io)
833        }
834
835        Expr::Lambda {
836            params,
837            body,
838            source,
839        } => {
840            // Capture the current environment and body expression at definition time.
841            // The resulting Value::Lambda is a closure that binds params on each call.
842            let captured_env = env.clone();
843            let captured_params = params.clone();
844            let captured_body = *body.clone();
845            let src = source.clone();
846            let lambda = LambdaFn(
847                std::rc::Rc::new(move |args: &[Value], io: Option<&mut IoContext>| {
848                    // Allow up to params.len()+1 args: the parser injects `ans` for empty f() calls.
849                    let effective = if args.len() > captured_params.len() {
850                        if args.len() > captured_params.len() + 1 {
851                            return Err(format!(
852                                "Lambda: too many arguments (expected at most {}, got {})",
853                                captured_params.len(),
854                                args.len()
855                            ));
856                        }
857                        &args[..captured_params.len()]
858                    } else {
859                        args
860                    };
861                    let mut local_env = captured_env.clone();
862                    for (p, a) in captured_params.iter().zip(effective.iter()) {
863                        local_env.insert(p.clone(), a.clone());
864                    }
865                    local_env.insert("nargin".to_string(), Value::Scalar(effective.len() as f64));
866                    eval_inner(&captured_body, &local_env, io)
867                }),
868                src,
869            );
870            Ok(Value::Lambda(Box::new(lambda)))
871        }
872        Expr::CellLiteral(elems) => {
873            let mut vals = Vec::with_capacity(elems.len());
874            for e in elems {
875                vals.push(eval_inner(e, env, io.as_deref_mut())?);
876            }
877            Ok(Value::Cell(Box::new(vals)))
878        }
879        Expr::CellIndex(cell_expr, idx_expr) => {
880            let cell = eval_inner(cell_expr, env, io.as_deref_mut())?;
881            let idx = eval_inner(idx_expr, env, io)?;
882            match (cell, idx) {
883                (Value::Cell(v), Value::Scalar(i)) => {
884                    let i = i as isize;
885                    if i < 1 || i as usize > v.len() {
886                        Err(format!("Cell index {} out of range (1..{})", i, v.len()))
887                    } else {
888                        Ok(v[(i - 1) as usize].clone())
889                    }
890                }
891                (Value::Cell(_), _) => Err("Cell index must be a scalar integer".to_string()),
892                _ => Err("Brace indexing '{}' is only valid on cell arrays".to_string()),
893            }
894        }
895        Expr::DynFieldGet(base_expr, field_expr) => {
896            let base_val = eval_inner(base_expr, env, io.as_deref_mut())?;
897            let field_val = eval_inner(field_expr, env, io)?;
898            let field = match &field_val {
899                Value::Str(s) | Value::StringObj(s) => s.clone(),
900                _ => return Err("Dynamic field name must be a string".to_string()),
901            };
902            match base_val {
903                Value::Struct(map) => map
904                    .get(&field)
905                    .cloned()
906                    .ok_or_else(|| format!("No field '{field}' in struct")),
907                _ => Err(format!(
908                    "Cannot access field '{field}' on a non-struct value"
909                )),
910            }
911        }
912        Expr::FieldGet(base_expr, field) => {
913            let base_val = eval_inner(base_expr, env, io)?;
914            match base_val {
915                Value::Map(ref map) if field == "Count" => Ok(Value::Scalar(map.len() as f64)),
916                Value::Map(_) => Err(format!(
917                    "Map has no property '{field}'; use 'Count', isKey(), keys(), values()"
918                )),
919                Value::Struct(map) => map
920                    .get(field)
921                    .cloned()
922                    .ok_or_else(|| format!("No field '{field}' in struct")),
923                // s.field on a struct array — collect field values across all elements
924                Value::StructArray(arr) => {
925                    let mut values: Vec<Value> = Vec::with_capacity(arr.len());
926                    for (idx, elem) in arr.iter().enumerate() {
927                        let v = elem.get(field).cloned().ok_or_else(|| {
928                            format!("No field '{field}' in struct array element {}", idx + 1)
929                        })?;
930                        values.push(v);
931                    }
932                    // If all values are scalars, return a 1×N matrix; otherwise a cell.
933                    let all_scalar = values.iter().all(|v| matches!(v, Value::Scalar(_)));
934                    if all_scalar {
935                        let nums: Vec<f64> = values
936                            .into_iter()
937                            .map(|v| {
938                                if let Value::Scalar(n) = v {
939                                    n
940                                } else {
941                                    unreachable!()
942                                }
943                            })
944                            .collect();
945                        let n = nums.len();
946                        Ok(Value::Matrix(Box::new(
947                            Array2::from_shape_vec((1, n), nums).unwrap(),
948                        )))
949                    } else {
950                        Ok(Value::Cell(Box::new(values)))
951                    }
952                }
953                _ => Err(format!(
954                    "Cannot access field '{field}' on a non-struct value"
955                )),
956            }
957        }
958        Expr::DotCall(segs, args) => {
959            let qualified = segs.join(".");
960            // containers.Map({'k1','k2'}, {v1,v2}) constructor.
961            if segs == &["containers", "Map"] {
962                return make_containers_map(args, env, io);
963            }
964            // If the head segment is a variable, follow the field chain and call the result.
965            if let Some(head_val) = env.get(&segs[0]).cloned() {
966                let mut val = head_val;
967                for field in &segs[1..] {
968                    val = match val {
969                        Value::Struct(ref map) => map
970                            .get(field)
971                            .cloned()
972                            .ok_or_else(|| format!("No field '{field}' in struct"))?,
973                        _ => {
974                            return Err(format!(
975                                "Cannot access field '{field}' on a non-struct value"
976                            ));
977                        }
978                    };
979                }
980                let mut evaled = Vec::with_capacity(args.len());
981                for a in args {
982                    evaled.push(eval_inner(a, env, io.as_deref_mut())?);
983                }
984                return match val {
985                    Value::Lambda(f) => {
986                        if evaled.is_empty() {
987                            evaled.push(env.get("ans").cloned().unwrap_or(Value::Scalar(0.0)));
988                        }
989                        f.0(&evaled, io)
990                    }
991                    Value::Function(_) => match io.as_deref_mut() {
992                        Some(io_ref) => FN_CALL_HOOK.with(|c| match c.get() {
993                            Some(hook) => hook(&qualified, &val, &evaled, env, io_ref),
994                            None => Err(format!("'{qualified}': exec::init() not called")),
995                        }),
996                        None => {
997                            let mut tmp_io = IoContext::new();
998                            FN_CALL_HOOK.with(|c| match c.get() {
999                                Some(hook) => hook(&qualified, &val, &evaled, env, &mut tmp_io),
1000                                None => Err(format!("'{qualified}': exec::init() not called")),
1001                            })
1002                        }
1003                    },
1004                    _ => Err(format!("'{qualified}': not a callable")),
1005                };
1006            }
1007            // Package call: autoload from +pkg/func.calc then invoke.
1008            let cached = AUTOLOAD_CACHE.with(|c| c.borrow().get(&qualified).cloned());
1009            let autoloaded_val = cached.or_else(|| {
1010                let loaded = AUTOLOAD_HOOK
1011                    .with(|c| c.get())
1012                    .is_some_and(|hook| hook(&qualified));
1013                if loaded {
1014                    AUTOLOAD_CACHE.with(|c| c.borrow().get(&qualified).cloned())
1015                } else {
1016                    None
1017                }
1018            });
1019            if let Some(val) = autoloaded_val {
1020                let mut evaled = Vec::with_capacity(args.len());
1021                for a in args {
1022                    evaled.push(eval_inner(a, env, io.as_deref_mut())?);
1023                }
1024                return match io.as_deref_mut() {
1025                    Some(io_ref) => FN_CALL_HOOK.with(|c| match c.get() {
1026                        Some(hook) => hook(&qualified, &val, &evaled, env, io_ref),
1027                        None => Err(format!("'{qualified}': exec::init() not called")),
1028                    }),
1029                    None => {
1030                        let mut tmp_io = IoContext::new();
1031                        FN_CALL_HOOK.with(|c| match c.get() {
1032                            Some(hook) => hook(&qualified, &val, &evaled, env, &mut tmp_io),
1033                            None => Err(format!("'{qualified}': exec::init() not called")),
1034                        })
1035                    }
1036                };
1037            }
1038            Err(format!("Unknown package function: '{qualified}'"))
1039        }
1040        Expr::FuncHandle(name) => {
1041            let name = name.clone();
1042            let captured_env = env.clone();
1043            let src = format!("@{name}");
1044            let lambda = LambdaFn(
1045                std::rc::Rc::new(move |args: &[Value], io: Option<&mut IoContext>| {
1046                    // First try the environment (user-defined function), then fall back to builtin.
1047                    if let Some(f) = captured_env.get(&name) {
1048                        let f = f.clone();
1049                        call_function_value(&f, args, io)
1050                    } else {
1051                        call_builtin(&name, args, &captured_env, io)
1052                    }
1053                }),
1054                src,
1055            );
1056            Ok(Value::Lambda(Box::new(lambda)))
1057        }
1058        Expr::PlainTranspose(e) => match eval_inner(e, env, io)? {
1059            Value::Void => Err("Transpose is not applicable to void".to_string()),
1060            Value::Scalar(n) => Ok(Value::Scalar(n)),
1061            Value::Matrix(m) => Ok(Value::Matrix(Box::new(m.t().to_owned()))),
1062            // Plain transpose: no conjugation — imaginary part unchanged
1063            Value::Complex(re, im) => Ok(Value::Complex(re, im)),
1064            // Plain transpose of complex matrix: swap axes only, no conjugation
1065            Value::ComplexMatrix(m) => Ok(Value::ComplexMatrix(Box::new(m.t().to_owned()))),
1066            Value::Str(s) => Ok(Value::Str(s)),
1067            Value::StringObj(s) => Ok(Value::StringObj(s)),
1068            // Arrays: orientation is ignored (Vec<f64> is always 1-D), return as-is.
1069            v @ (Value::DateTimeArray(_) | Value::DurationArray(_)) => Ok(v),
1070            Value::Lambda(_)
1071            | Value::Function(_)
1072            | Value::Tuple(_)
1073            | Value::Cell(_)
1074            | Value::Struct(_)
1075            | Value::StructArray(_)
1076            | Value::DateTime(_)
1077            | Value::Duration(_)
1078            | Value::Map(_) => Err("Transpose is not applicable to this type".to_string()),
1079        },
1080        Expr::Colon => Err("':' is only valid inside index expressions".to_string()),
1081        Expr::NaT => Ok(Value::DateTime(f64::NAN)),
1082        Expr::Matrix(rows) => {
1083            if rows.is_empty() {
1084                return Ok(Value::Matrix(Box::new(Array2::<f64>::zeros((0, 0)))));
1085            }
1086
1087            // Pass 1: evaluate all elements, skipping empty rows.
1088            let mut evaluated: Vec<Vec<Value>> = Vec::with_capacity(rows.len());
1089            for row in rows {
1090                if row.is_empty() {
1091                    continue;
1092                }
1093                let mut ev_row: Vec<Value> = Vec::with_capacity(row.len());
1094                for elem_expr in row {
1095                    ev_row.push(eval_inner(elem_expr, env, io.as_deref_mut())?);
1096                }
1097                evaluated.push(ev_row);
1098            }
1099            if evaluated.is_empty() {
1100                return Ok(Value::Matrix(Box::new(Array2::<f64>::zeros((0, 0)))));
1101            }
1102
1103            // Pass 2: detect if any element is complex (scan the entire evaluated grid).
1104            let has_complex = evaluated
1105                .iter()
1106                .flat_map(|row| row.iter())
1107                .any(|v| matches!(v, Value::Complex(_, _) | Value::ComplexMatrix(_)));
1108
1109            // Dispatch on element kind (complex takes priority over numeric).
1110            enum MatKind {
1111                ComplexNumeric,
1112                Numeric,
1113                DateTime,
1114                Duration,
1115                Str,
1116            }
1117            let kind = if has_complex {
1118                MatKind::ComplexNumeric
1119            } else {
1120                match &evaluated[0][0] {
1121                    Value::Scalar(_) | Value::Matrix(_) => MatKind::Numeric,
1122                    Value::DateTime(_) | Value::DateTimeArray(_) => MatKind::DateTime,
1123                    Value::Duration(_) | Value::DurationArray(_) => MatKind::Duration,
1124                    Value::Str(_) | Value::StringObj(_) => MatKind::Str,
1125                    Value::Void => {
1126                        return Err("Void value cannot be used in matrix literal".to_string());
1127                    }
1128                    Value::Lambda(_)
1129                    | Value::Function(_)
1130                    | Value::Tuple(_)
1131                    | Value::Cell(_)
1132                    | Value::Struct(_)
1133                    | Value::StructArray(_)
1134                    | Value::Map(_) => {
1135                        return Err("This type cannot be used in matrix literals".to_string());
1136                    }
1137                    // Cannot reach here — has_complex covers this
1138                    Value::Complex(_, _) | Value::ComplexMatrix(_) => unreachable!(),
1139                }
1140            };
1141
1142            match kind {
1143                MatKind::ComplexNumeric => {
1144                    // Build a ComplexMatrix by upcasting all elements.
1145                    // Each element can be Scalar, Complex, Matrix (real), or ComplexMatrix.
1146                    let mut row_blocks: Vec<Array2<Complex<f64>>> =
1147                        Vec::with_capacity(evaluated.len());
1148                    for ev_row in &evaluated {
1149                        let mut elem_mats: Vec<Array2<Complex<f64>>> =
1150                            Vec::with_capacity(ev_row.len());
1151                        for val in ev_row {
1152                            let block: Array2<Complex<f64>> = match val {
1153                                Value::Scalar(n) => {
1154                                    Array2::from_elem((1, 1), Complex::new(*n, 0.0))
1155                                }
1156                                Value::Complex(re, im) => {
1157                                    Array2::from_elem((1, 1), Complex::new(*re, *im))
1158                                }
1159                                Value::Matrix(m) => cm_from_real(m),
1160                                Value::ComplexMatrix(m) => (**m).clone(),
1161                                _ => {
1162                                    return Err(
1163                                        "This type cannot be used in a complex matrix literal"
1164                                            .to_string(),
1165                                    );
1166                                }
1167                            };
1168                            elem_mats.push(block);
1169                        }
1170                        let nrows = elem_mats[0].nrows();
1171                        for (i, m) in elem_mats.iter().enumerate().skip(1) {
1172                            if m.nrows() != nrows {
1173                                return Err(format!(
1174                                    "Matrix row height mismatch: expected {} rows, element {} has {} rows",
1175                                    nrows,
1176                                    i + 1,
1177                                    m.nrows()
1178                                ));
1179                            }
1180                        }
1181                        let ncols: usize = elem_mats.iter().map(|m| m.ncols()).sum();
1182                        let mut flat: Vec<Complex<f64>> = Vec::with_capacity(nrows * ncols);
1183                        for r in 0..nrows {
1184                            for m in &elem_mats {
1185                                flat.extend(m.row(r).iter().copied());
1186                            }
1187                        }
1188                        row_blocks.push(
1189                            Array2::from_shape_vec((nrows, ncols), flat)
1190                                .map_err(|e| format!("Matrix shape error: {e}"))?,
1191                        );
1192                    }
1193                    if row_blocks.is_empty() {
1194                        return Ok(Value::ComplexMatrix(Box::new(Array2::zeros((0, 0)))));
1195                    }
1196                    let ncols = row_blocks[0].ncols();
1197                    for (i, blk) in row_blocks.iter().enumerate().skip(1) {
1198                        if blk.ncols() != ncols {
1199                            return Err(format!(
1200                                "Matrix column count mismatch: expected {} columns, row {} has {} columns",
1201                                ncols,
1202                                i + 1,
1203                                blk.ncols()
1204                            ));
1205                        }
1206                    }
1207                    let total_rows: usize = row_blocks.iter().map(|b| b.nrows()).sum();
1208                    let mut flat: Vec<Complex<f64>> = Vec::with_capacity(total_rows * ncols);
1209                    for blk in &row_blocks {
1210                        flat.extend(blk.iter().copied());
1211                    }
1212                    let m = Array2::from_shape_vec((total_rows, ncols), flat)
1213                        .map_err(|e| format!("Matrix shape error: {e}"))?;
1214                    Ok(Value::ComplexMatrix(Box::new(m)))
1215                }
1216                MatKind::DateTime => {
1217                    let mut ts: Vec<f64> = Vec::new();
1218                    for ev_row in &evaluated {
1219                        for val in ev_row {
1220                            match val {
1221                                Value::DateTime(t) => ts.push(*t),
1222                                Value::DateTimeArray(v) => ts.extend_from_slice(v),
1223                                _ => {
1224                                    return Err(
1225                                        "Matrix literal: cannot mix datetime with other types"
1226                                            .to_string(),
1227                                    );
1228                                }
1229                            }
1230                        }
1231                    }
1232                    Ok(Value::DateTimeArray(ts))
1233                }
1234                MatKind::Duration => {
1235                    let mut sv: Vec<f64> = Vec::new();
1236                    for ev_row in &evaluated {
1237                        for val in ev_row {
1238                            match val {
1239                                Value::Duration(s) => sv.push(*s),
1240                                Value::DurationArray(v) => sv.extend_from_slice(v),
1241                                _ => {
1242                                    return Err(
1243                                        "Matrix literal: cannot mix duration with other types"
1244                                            .to_string(),
1245                                    );
1246                                }
1247                            }
1248                        }
1249                    }
1250                    Ok(Value::DurationArray(sv))
1251                }
1252                MatKind::Numeric => {
1253                    // Each row is horizontally concatenated into an Array2 block;
1254                    // blocks are then vertically concatenated.
1255                    let mut row_blocks: Vec<Array2<f64>> = Vec::with_capacity(evaluated.len());
1256                    for ev_row in &evaluated {
1257                        let mut elem_mats: Vec<Array2<f64>> = Vec::with_capacity(ev_row.len());
1258                        for val in ev_row {
1259                            match val {
1260                                Value::Scalar(n) => {
1261                                    elem_mats.push(Array2::from_elem((1, 1), *n));
1262                                }
1263                                Value::Matrix(m) => elem_mats.push((**m).clone()),
1264                                Value::Void => {
1265                                    return Err(
1266                                        "Void value cannot be used in matrix literal".to_string()
1267                                    );
1268                                }
1269                                // In numeric context, char arrays contribute their
1270                                // Unicode code values — MATLAB compatible: [65 'b'] = [65 98]
1271                                Value::Str(s) | Value::StringObj(s) => {
1272                                    let codes: Vec<f64> =
1273                                        s.chars().map(|c| c as u32 as f64).collect();
1274                                    let mat = if codes.is_empty() {
1275                                        Array2::<f64>::zeros((1, 0))
1276                                    } else {
1277                                        Array2::from_shape_vec((1, codes.len()), codes)
1278                                            .map_err(|e| format!("Matrix shape error: {e}"))?
1279                                    };
1280                                    elem_mats.push(mat);
1281                                }
1282                                _ => {
1283                                    return Err(
1284                                        "This type cannot be used in matrix literals".to_string()
1285                                    );
1286                                }
1287                            }
1288                        }
1289                        let nrows = elem_mats[0].nrows();
1290                        for (i, m) in elem_mats.iter().enumerate().skip(1) {
1291                            if m.nrows() != nrows {
1292                                return Err(format!(
1293                                    "Matrix row height mismatch: expected {} rows, element {} has {} rows",
1294                                    nrows,
1295                                    i + 1,
1296                                    m.nrows()
1297                                ));
1298                            }
1299                        }
1300                        let ncols: usize = elem_mats.iter().map(|m| m.ncols()).sum();
1301                        let mut flat: Vec<f64> = Vec::with_capacity(nrows * ncols);
1302                        for r in 0..nrows {
1303                            for m in &elem_mats {
1304                                flat.extend(m.row(r).iter().copied());
1305                            }
1306                        }
1307                        row_blocks.push(
1308                            Array2::from_shape_vec((nrows, ncols), flat)
1309                                .map_err(|e| format!("Matrix shape error: {e}"))?,
1310                        );
1311                    }
1312                    if row_blocks.is_empty() {
1313                        return Ok(Value::Matrix(Box::new(Array2::<f64>::zeros((0, 0)))));
1314                    }
1315                    let ncols = row_blocks[0].ncols();
1316                    if ncols == 0 {
1317                        let total_rows: usize = row_blocks.iter().map(|b| b.nrows()).sum();
1318                        return Ok(Value::Matrix(Box::new(Array2::zeros((total_rows, 0)))));
1319                    }
1320                    for (i, blk) in row_blocks.iter().enumerate().skip(1) {
1321                        if blk.ncols() != ncols {
1322                            return Err(format!(
1323                                "Matrix column count mismatch: expected {} columns, row {} has {} columns",
1324                                ncols,
1325                                i + 1,
1326                                blk.ncols()
1327                            ));
1328                        }
1329                    }
1330                    let total_rows: usize = row_blocks.iter().map(|b| b.nrows()).sum();
1331                    let mut flat: Vec<f64> = Vec::with_capacity(total_rows * ncols);
1332                    for blk in &row_blocks {
1333                        flat.extend(blk.iter().copied());
1334                    }
1335                    let m = Array2::from_shape_vec((total_rows, ncols), flat)
1336                        .map_err(|e| format!("Matrix shape error: {e}"))?;
1337                    Ok(Value::Matrix(Box::new(m)))
1338                }
1339                MatKind::Str => {
1340                    if evaluated.len() > 1 {
1341                        return Err("Multi-row char-array literals are not supported".to_string());
1342                    }
1343                    let mut out = String::new();
1344                    for val in &evaluated[0] {
1345                        match val {
1346                            Value::Str(s) | Value::StringObj(s) => out.push_str(s),
1347                            Value::Scalar(n) => {
1348                                let code = n.round();
1349                                out.push(
1350                                    char::from_u32(code as u32)
1351                                        .ok_or_else(|| format!("char: invalid code {n}"))?,
1352                                );
1353                            }
1354                            Value::Matrix(m) => {
1355                                for &n in m.iter() {
1356                                    out.push(
1357                                        char::from_u32(n.round() as u32)
1358                                            .ok_or_else(|| format!("char: invalid code {n}"))?,
1359                                    );
1360                                }
1361                            }
1362                            _ => {
1363                                return Err(
1364                                    "This type cannot be used in a char-array literal".to_string()
1365                                );
1366                            }
1367                        }
1368                    }
1369                    Ok(Value::Str(out))
1370                }
1371            }
1372        }
1373        Expr::Transpose(e) => match eval_inner(e, env, io)? {
1374            Value::Void => Err("Transpose is not applicable to void".to_string()),
1375            Value::Scalar(n) => Ok(Value::Scalar(n)),
1376            Value::Matrix(m) => Ok(Value::Matrix(Box::new(m.t().to_owned()))),
1377            Value::Complex(re, im) => Ok(Value::Complex(re, -im)),
1378            // Conjugate transpose (Hermitian): transpose axes + conjugate each element
1379            Value::ComplexMatrix(m) => Ok(Value::ComplexMatrix(Box::new(m.t().mapv(|c| c.conj())))),
1380            // Transpose of a char array or string object: return as-is (1×N not fully supported)
1381            Value::Str(s) => Ok(Value::Str(s)),
1382            Value::StringObj(s) => Ok(Value::StringObj(s)),
1383            // Arrays: orientation is ignored (Vec<f64> is always 1-D), return as-is.
1384            v @ (Value::DateTimeArray(_) | Value::DurationArray(_)) => Ok(v),
1385            Value::Lambda(_)
1386            | Value::Function(_)
1387            | Value::Tuple(_)
1388            | Value::Cell(_)
1389            | Value::Struct(_)
1390            | Value::StructArray(_)
1391            | Value::DateTime(_)
1392            | Value::Duration(_)
1393            | Value::Map(_) => Err("Transpose is not applicable to this type".to_string()),
1394        },
1395        Expr::StrLiteral(s) => Ok(Value::Str(s.clone())),
1396        Expr::StringObjLiteral(s) => Ok(Value::StringObj(s.clone())),
1397        Expr::Range(start_expr, step_expr, stop_expr) => {
1398            let start = match eval_inner(start_expr, env, io.as_deref_mut())? {
1399                Value::Scalar(n) => n,
1400                _ => return Err("Range bounds must be real scalars".to_string()),
1401            };
1402            let stop = match eval_inner(stop_expr, env, io.as_deref_mut())? {
1403                Value::Scalar(n) => n,
1404                _ => return Err("Range bounds must be real scalars".to_string()),
1405            };
1406            let step = match step_expr {
1407                None => 1.0,
1408                Some(s) => match eval_inner(s, env, io)? {
1409                    Value::Scalar(n) => n,
1410                    _ => return Err("Range step must be a real scalar".to_string()),
1411                },
1412            };
1413            if step == 0.0 {
1414                return Err("Range step cannot be zero".to_string());
1415            }
1416            let n_float = (stop - start) / step;
1417            if n_float < -1e-10 {
1418                // Empty range: step points in the wrong direction
1419                return Ok(Value::Matrix(Box::new(Array2::zeros((1, 0)))));
1420            }
1421            let n = (n_float + 1e-10).floor() as usize + 1;
1422            let vals: Vec<f64> = (0..n).map(|i| start + i as f64 * step).collect();
1423            let m =
1424                Array2::from_shape_vec((1, n), vals).map_err(|e| format!("Range error: {e}"))?;
1425            Ok(Value::Matrix(Box::new(m)))
1426        }
1427    }
1428}
1429
1430fn eval_binop(l: Value, op: &Op, r: Value) -> Result<Value, String> {
1431    match (l, r) {
1432        (Value::Void, _) | (_, Value::Void) => {
1433            Err("Cannot apply operator to void value".to_string())
1434        }
1435        // --- String object operations ---
1436        (Value::StringObj(a), Value::StringObj(b)) => match op {
1437            Op::Add => Ok(Value::StringObj(a + &b)),
1438            Op::Eq => Ok(Value::Scalar(bool_to_f64(a == b))),
1439            Op::NotEq => Ok(Value::Scalar(bool_to_f64(a != b))),
1440            _ => Err("Operator not supported on string objects".to_string()),
1441        },
1442        // Char array: convert to numeric, re-dispatch
1443        (Value::Str(s), r) => eval_binop(str_to_numeric(&s), op, r),
1444        (l, Value::Str(s)) => eval_binop(l, op, str_to_numeric(&s)),
1445        // String object mixed with other types: error
1446        (Value::StringObj(_), _) | (_, Value::StringObj(_)) => {
1447            Err("String object cannot be combined with non-string values".to_string())
1448        }
1449        // Functions, tuples, cell arrays, structs, struct arrays, and maps are not numeric
1450        (Value::Lambda(_), _)
1451        | (_, Value::Lambda(_))
1452        | (Value::Function(_), _)
1453        | (_, Value::Function(_))
1454        | (Value::Tuple(_), _)
1455        | (_, Value::Tuple(_))
1456        | (Value::Cell(_), _)
1457        | (_, Value::Cell(_))
1458        | (Value::Struct(_), _)
1459        | (_, Value::Struct(_))
1460        | (Value::StructArray(_), _)
1461        | (_, Value::StructArray(_))
1462        | (Value::Map(_), _)
1463        | (_, Value::Map(_)) => Err("Cannot apply operator to a Map value".to_string()),
1464        // --- DateTime / Duration arithmetic ---
1465        // datetime + duration → datetime
1466        (Value::DateTime(t), Value::Duration(d)) => match op {
1467            Op::Add => Ok(Value::DateTime(t + d)),
1468            Op::Sub => Ok(Value::DateTime(t - d)),
1469            _ => Err("Unsupported operator between datetime and duration".to_string()),
1470        },
1471        // duration + datetime → datetime (commutative add only)
1472        (Value::Duration(d), Value::DateTime(t)) => match op {
1473            Op::Add => Ok(Value::DateTime(t + d)),
1474            _ => Err("Unsupported operator between duration and datetime".to_string()),
1475        },
1476        // datetime - datetime → duration
1477        (Value::DateTime(t1), Value::DateTime(t2)) => match op {
1478            Op::Sub => Ok(Value::Duration(t1 - t2)),
1479            Op::Eq => Ok(Value::Scalar(bool_to_f64(
1480                (t1 - t2).abs() < 1e-9 || (t1.is_nan() && t2.is_nan()),
1481            ))),
1482            Op::NotEq => Ok(Value::Scalar(bool_to_f64(
1483                (t1 - t2).abs() >= 1e-9 && !(t1.is_nan() && t2.is_nan()),
1484            ))),
1485            Op::Lt => Ok(Value::Scalar(bool_to_f64(t1 < t2))),
1486            Op::Gt => Ok(Value::Scalar(bool_to_f64(t1 > t2))),
1487            Op::LtEq => Ok(Value::Scalar(bool_to_f64(t1 <= t2))),
1488            Op::GtEq => Ok(Value::Scalar(bool_to_f64(t1 >= t2))),
1489            _ => Err("Unsupported operator between two datetimes".to_string()),
1490        },
1491        // duration ± duration → duration; duration */ scalar → duration; duration / duration → scalar
1492        (Value::Duration(d1), Value::Duration(d2)) => match op {
1493            Op::Add => Ok(Value::Duration(d1 + d2)),
1494            Op::Sub => Ok(Value::Duration(d1 - d2)),
1495            Op::Div | Op::ElemDiv => Ok(Value::Scalar(d1 / d2)),
1496            Op::Eq => Ok(Value::Scalar(bool_to_f64((d1 - d2).abs() < 1e-9))),
1497            Op::NotEq => Ok(Value::Scalar(bool_to_f64((d1 - d2).abs() >= 1e-9))),
1498            Op::Lt => Ok(Value::Scalar(bool_to_f64(d1 < d2))),
1499            Op::Gt => Ok(Value::Scalar(bool_to_f64(d1 > d2))),
1500            Op::LtEq => Ok(Value::Scalar(bool_to_f64(d1 <= d2))),
1501            Op::GtEq => Ok(Value::Scalar(bool_to_f64(d1 >= d2))),
1502            _ => Err("Unsupported operator between two durations".to_string()),
1503        },
1504        (Value::Duration(d), Value::Scalar(s)) => match op {
1505            Op::Mul | Op::ElemMul => Ok(Value::Duration(d * s)),
1506            Op::Div | Op::ElemDiv => Ok(Value::Duration(d / s)),
1507            _ => Err("Unsupported operator between duration and scalar".to_string()),
1508        },
1509        (Value::Scalar(s), Value::Duration(d)) => match op {
1510            Op::Mul | Op::ElemMul => Ok(Value::Duration(s * d)),
1511            _ => Err("Unsupported operator between scalar and duration".to_string()),
1512        },
1513        // DateTime/Duration + arrays
1514        (Value::DateTime(t), Value::DurationArray(dv)) => match op {
1515            Op::Add => Ok(Value::DateTimeArray(dv.iter().map(|d| t + d).collect())),
1516            Op::Sub => Ok(Value::DateTimeArray(dv.iter().map(|d| t - d).collect())),
1517            _ => Err("Unsupported operator between datetime and duration array".to_string()),
1518        },
1519        (Value::DurationArray(dv), Value::DateTime(t)) => match op {
1520            Op::Add => Ok(Value::DateTimeArray(dv.iter().map(|d| t + d).collect())),
1521            _ => Err("Unsupported operator between duration array and datetime".to_string()),
1522        },
1523        (Value::DateTimeArray(tv), Value::Duration(d)) => match op {
1524            Op::Add => Ok(Value::DateTimeArray(tv.iter().map(|t| t + d).collect())),
1525            Op::Sub => Ok(Value::DateTimeArray(tv.iter().map(|t| t - d).collect())),
1526            _ => Err("Unsupported operator between datetime array and duration".to_string()),
1527        },
1528        (Value::DateTimeArray(tv), Value::DurationArray(dv)) => match op {
1529            Op::Add if tv.len() == dv.len() => Ok(Value::DateTimeArray(
1530                tv.iter().zip(&dv).map(|(t, d)| t + d).collect(),
1531            )),
1532            Op::Sub if tv.len() == dv.len() => Ok(Value::DateTimeArray(
1533                tv.iter().zip(&dv).map(|(t, d)| t - d).collect(),
1534            )),
1535            _ => Err("Unsupported or mismatched datetime/duration array operation".to_string()),
1536        },
1537        (Value::DateTimeArray(tv1), Value::DateTimeArray(tv2)) => match op {
1538            Op::Sub if tv1.len() == tv2.len() => Ok(Value::DurationArray(
1539                tv1.iter().zip(&tv2).map(|(a, b)| a - b).collect(),
1540            )),
1541            _ => Err("Unsupported operator between two datetime arrays".to_string()),
1542        },
1543        (Value::DurationArray(dv), Value::Scalar(s)) => match op {
1544            Op::Mul | Op::ElemMul => Ok(Value::DurationArray(dv.iter().map(|d| d * s).collect())),
1545            Op::Div | Op::ElemDiv => Ok(Value::DurationArray(dv.iter().map(|d| d / s).collect())),
1546            _ => Err("Unsupported operator between duration array and scalar".to_string()),
1547        },
1548        (Value::Scalar(s), Value::DurationArray(dv)) => match op {
1549            Op::Mul | Op::ElemMul => Ok(Value::DurationArray(dv.iter().map(|d| s * d).collect())),
1550            _ => Err("Unsupported operator between scalar and duration array".to_string()),
1551        },
1552        // Catch-all: DateTime/Duration mixed with unsupported types
1553        (Value::DateTime(_), _)
1554        | (_, Value::DateTime(_))
1555        | (Value::Duration(_), _)
1556        | (_, Value::Duration(_))
1557        | (Value::DateTimeArray(_), _)
1558        | (_, Value::DateTimeArray(_))
1559        | (Value::DurationArray(_), _)
1560        | (_, Value::DurationArray(_)) => {
1561            Err("Unsupported operation on datetime or duration value".to_string())
1562        }
1563        // --- Complex arithmetic ---
1564        (Value::Complex(re1, im1), Value::Complex(re2, im2)) => {
1565            complex_binop(re1, im1, op, re2, im2)
1566        }
1567        (Value::Complex(re, im), Value::Scalar(s)) => complex_binop(re, im, op, s, 0.0),
1568        (Value::Scalar(s), Value::Complex(re, im)) => complex_binop(s, 0.0, op, re, im),
1569        // Complex scalar × real matrix → upcast matrix to ComplexMatrix, broadcast scalar
1570        (Value::Complex(re, im), Value::Matrix(m)) => {
1571            complex_binop_cm(re, im, op, cm_from_real(&m))
1572        }
1573        (Value::Matrix(m), Value::Complex(re, im)) => {
1574            cm_binop_complex(cm_from_real(&m), op, re, im)
1575        }
1576        // ComplexMatrix combinations
1577        (Value::ComplexMatrix(a), Value::ComplexMatrix(b)) => complex_matrix_binop(*a, op, *b),
1578        (Value::ComplexMatrix(cm), Value::Matrix(m)) => {
1579            complex_matrix_binop(*cm, op, cm_from_real(&m))
1580        }
1581        (Value::Matrix(m), Value::ComplexMatrix(cm)) => {
1582            complex_matrix_binop(cm_from_real(&m), op, *cm)
1583        }
1584        (Value::ComplexMatrix(cm), Value::Scalar(s)) => cm_binop_scalar(*cm, op, s),
1585        (Value::Scalar(s), Value::ComplexMatrix(cm)) => scalar_binop_cm(s, op, *cm),
1586        (Value::ComplexMatrix(cm), Value::Complex(re, im)) => cm_binop_complex(*cm, op, re, im),
1587        (Value::Complex(re, im), Value::ComplexMatrix(cm)) => complex_binop_cm(re, im, op, *cm),
1588        (Value::Scalar(lv), Value::Scalar(rv)) => {
1589            let result = match op {
1590                Op::Add => lv + rv,
1591                Op::Sub => lv - rv,
1592                Op::Mul | Op::ElemMul => lv * rv,
1593                Op::Div | Op::ElemDiv => lv / rv,
1594                Op::LDiv => rv / lv,
1595                Op::Pow | Op::ElemPow => lv.powf(rv),
1596                Op::Eq => bool_to_f64(lv == rv),
1597                Op::NotEq => bool_to_f64(lv != rv),
1598                Op::Lt => bool_to_f64(lv < rv),
1599                Op::Gt => bool_to_f64(lv > rv),
1600                Op::LtEq => bool_to_f64(lv <= rv),
1601                Op::GtEq => bool_to_f64(lv >= rv),
1602                Op::And | Op::ElemAnd => bool_to_f64(lv != 0.0 && rv != 0.0),
1603                Op::Or | Op::ElemOr => bool_to_f64(lv != 0.0 || rv != 0.0),
1604            };
1605            Ok(Value::Scalar(result))
1606        }
1607        (Value::Matrix(lm), Value::Matrix(rm)) => match op {
1608            Op::Add => {
1609                check_same_shape(&lm, &rm)?;
1610                Ok(Value::Matrix(Box::new(&*lm + &*rm)))
1611            }
1612            Op::Sub => {
1613                check_same_shape(&lm, &rm)?;
1614                Ok(Value::Matrix(Box::new(&*lm - &*rm)))
1615            }
1616            Op::Mul => {
1617                if lm.ncols() != rm.nrows() {
1618                    return Err(format!(
1619                        "Inner dimensions must agree: {}x{} * {}x{}",
1620                        lm.nrows(),
1621                        lm.ncols(),
1622                        rm.nrows(),
1623                        rm.ncols()
1624                    ));
1625                }
1626                Ok(Value::Matrix(Box::new(lm.dot(&*rm))))
1627            }
1628            Op::ElemMul => {
1629                check_same_shape(&lm, &rm)?;
1630                Ok(Value::Matrix(Box::new(&*lm * &*rm)))
1631            }
1632            Op::ElemDiv => {
1633                check_same_shape(&lm, &rm)?;
1634                Ok(Value::Matrix(Box::new(&*lm / &*rm)))
1635            }
1636            Op::ElemPow => {
1637                check_same_shape(&lm, &rm)?;
1638                Ok(Value::Matrix(Box::new(
1639                    ndarray::Zip::from(&*lm)
1640                        .and(&*rm)
1641                        .map_collect(|a, b| a.powf(*b)),
1642                )))
1643            }
1644            Op::Eq | Op::NotEq | Op::Lt | Op::Gt | Op::LtEq | Op::GtEq => {
1645                check_same_shape(&lm, &rm)?;
1646                Ok(Value::Matrix(Box::new(
1647                    ndarray::Zip::from(&*lm)
1648                        .and(&*rm)
1649                        .map_collect(|a, b| bool_to_f64(cmp_op(op, *a, *b))),
1650                )))
1651            }
1652            Op::And | Op::Or | Op::ElemAnd | Op::ElemOr => {
1653                check_same_shape(&lm, &rm)?;
1654                Ok(Value::Matrix(Box::new(
1655                    ndarray::Zip::from(&*lm)
1656                        .and(&*rm)
1657                        .map_collect(|a, b| bool_to_f64(cmp_op(op, *a, *b))),
1658                )))
1659            }
1660            Op::Div => Err("Matrix / Matrix: use inv(B)*A or A*inv(B)".to_string()),
1661            Op::LDiv => Ok(Value::Matrix(Box::new(solve_linear(&lm, &rm)?))),
1662            Op::Pow => Err("Matrix ^ Matrix: not supported".to_string()),
1663        },
1664        (Value::Scalar(s), Value::Matrix(m)) => match op {
1665            Op::Add => Ok(Value::Matrix(Box::new(s + &*m))),
1666            Op::Sub => Ok(Value::Matrix(Box::new(m.mapv(|x| s - x)))),
1667            Op::Mul | Op::ElemMul => Ok(Value::Matrix(Box::new(s * &*m))),
1668            Op::Div => Err("Scalar / Matrix: not supported".to_string()),
1669            Op::ElemDiv => Err("Scalar ./ Matrix: not supported".to_string()),
1670            Op::LDiv => {
1671                if s == 0.0 {
1672                    return Err("Left division by zero (a \\ B requires a ≠ 0)".to_string());
1673                }
1674                Ok(Value::Matrix(Box::new(m.mapv(|x| x / s))))
1675            }
1676            Op::Pow | Op::ElemPow => Ok(Value::Matrix(Box::new(m.mapv(|x| s.powf(x))))),
1677            Op::Eq
1678            | Op::NotEq
1679            | Op::Lt
1680            | Op::Gt
1681            | Op::LtEq
1682            | Op::GtEq
1683            | Op::And
1684            | Op::Or
1685            | Op::ElemAnd
1686            | Op::ElemOr => Ok(Value::Matrix(Box::new(
1687                m.mapv(|x| bool_to_f64(cmp_op(op, s, x))),
1688            ))),
1689        },
1690        (Value::Matrix(m), Value::Scalar(s)) => match op {
1691            Op::Add => Ok(Value::Matrix(Box::new(&*m + s))),
1692            Op::Sub => Ok(Value::Matrix(Box::new(&*m - s))),
1693            Op::Mul | Op::ElemMul => Ok(Value::Matrix(Box::new(&*m * s))),
1694            Op::Div | Op::ElemDiv => Ok(Value::Matrix(Box::new(m.mapv(|x| x / s)))),
1695            Op::LDiv => {
1696                let b = Array2::from_elem((m.nrows(), 1), s);
1697                Ok(Value::Matrix(Box::new(solve_linear(&m, &b)?)))
1698            }
1699            Op::Pow | Op::ElemPow => Ok(Value::Matrix(Box::new(m.mapv(|x| x.powf(s))))),
1700            Op::Eq
1701            | Op::NotEq
1702            | Op::Lt
1703            | Op::Gt
1704            | Op::LtEq
1705            | Op::GtEq
1706            | Op::And
1707            | Op::Or
1708            | Op::ElemAnd
1709            | Op::ElemOr => Ok(Value::Matrix(Box::new(
1710                m.mapv(|x| bool_to_f64(cmp_op(op, x, s))),
1711            ))),
1712        },
1713    }
1714}
1715
1716#[inline]
1717fn bool_to_f64(b: bool) -> f64 {
1718    if b { 1.0 } else { 0.0 }
1719}
1720
1721/// Applies a comparison or logical op to two scalar values.
1722fn cmp_op(op: &Op, a: f64, b: f64) -> bool {
1723    match op {
1724        Op::Eq => a == b,
1725        Op::NotEq => a != b,
1726        Op::Lt => a < b,
1727        Op::Gt => a > b,
1728        Op::LtEq => a <= b,
1729        Op::GtEq => a >= b,
1730        Op::And | Op::ElemAnd => a != 0.0 && b != 0.0,
1731        Op::Or | Op::ElemOr => a != 0.0 || b != 0.0,
1732        _ => unreachable!(),
1733    }
1734}
1735
1736/// Performs binary operations on two complex numbers `(re1+im1*i) OP (re2+im2*i)`.
1737fn complex_binop(re1: f64, im1: f64, op: &Op, re2: f64, im2: f64) -> Result<Value, String> {
1738    match op {
1739        Op::Add => Ok(make_complex(re1 + re2, im1 + im2)),
1740        Op::Sub => Ok(make_complex(re1 - re2, im1 - im2)),
1741        Op::Mul | Op::ElemMul => {
1742            // (a+bi)(c+di) = (ac-bd) + (ad+bc)i
1743            Ok(make_complex(re1 * re2 - im1 * im2, re1 * im2 + im1 * re2))
1744        }
1745        Op::Div | Op::ElemDiv => {
1746            // (a+bi)/(c+di) = ((ac+bd) + (bc-ad)i) / (c²+d²)
1747            // When denom == 0 let IEEE 754 produce Inf/NaN naturally.
1748            let denom = re2 * re2 + im2 * im2;
1749            if denom == 0.0 {
1750                return Ok(make_complex(re1 / 0.0_f64, im1 / 0.0_f64));
1751            }
1752            Ok(make_complex(
1753                (re1 * re2 + im1 * im2) / denom,
1754                (im1 * re2 - re1 * im2) / denom,
1755            ))
1756        }
1757        Op::Pow | Op::ElemPow => {
1758            let r1 = (re1 * re1 + im1 * im1).sqrt();
1759            if r1 == 0.0 {
1760                if re2 > 0.0 {
1761                    return Ok(Value::Scalar(0.0));
1762                }
1763                return Ok(Value::Complex(f64::NAN, f64::NAN));
1764            }
1765            // For integer exponents with zero imaginary part, use repeated multiplication
1766            // to avoid polar-form floating-point error (e.g. i^2 = -1 exactly).
1767            if im2 == 0.0 && re2.fract() == 0.0 && re2.abs() < 1_000_000.0 {
1768                let n = re2 as i64;
1769                if n == 0 {
1770                    return Ok(Value::Scalar(1.0));
1771                }
1772                // positive power: repeated squaring
1773                let abs_n = n.unsigned_abs();
1774                let (mut rr, mut ri) = (1.0_f64, 0.0_f64);
1775                let (mut br, mut bi) = (re1, im1);
1776                let mut exp = abs_n;
1777                while exp > 0 {
1778                    if exp & 1 == 1 {
1779                        let nr = rr * br - ri * bi;
1780                        let ni = rr * bi + ri * br;
1781                        rr = nr;
1782                        ri = ni;
1783                    }
1784                    let nr = br * br - bi * bi;
1785                    let ni = 2.0 * br * bi;
1786                    br = nr;
1787                    bi = ni;
1788                    exp >>= 1;
1789                }
1790                if n < 0 {
1791                    // invert: 1/(rr+ri*i)
1792                    let denom = rr * rr + ri * ri;
1793                    return Ok(make_complex(rr / denom, -ri / denom));
1794                }
1795                return Ok(make_complex(rr, ri));
1796            }
1797            // General case: via polar form exp((c+di) * ln(a+bi))
1798            let theta1 = im1.atan2(re1);
1799            let ln_r1 = r1.ln();
1800            let exp_re = re2 * ln_r1 - im2 * theta1;
1801            let exp_im = im2 * ln_r1 + re2 * theta1;
1802            let mag = exp_re.exp();
1803            Ok(make_complex(mag * exp_im.cos(), mag * exp_im.sin()))
1804        }
1805        Op::Eq => Ok(Value::Scalar(bool_to_f64(re1 == re2 && im1 == im2))),
1806        Op::NotEq => Ok(Value::Scalar(bool_to_f64(re1 != re2 || im1 != im2))),
1807        Op::Lt | Op::Gt | Op::LtEq | Op::GtEq => {
1808            Err("Ordering is not defined for complex numbers".to_string())
1809        }
1810        Op::And | Op::ElemAnd => Ok(Value::Scalar(bool_to_f64(
1811            (re1 != 0.0 || im1 != 0.0) && (re2 != 0.0 || im2 != 0.0),
1812        ))),
1813        Op::Or | Op::ElemOr => Ok(Value::Scalar(bool_to_f64(
1814            re1 != 0.0 || im1 != 0.0 || re2 != 0.0 || im2 != 0.0,
1815        ))),
1816        Op::LDiv => Err("Left division (\\) is not supported for complex numbers".to_string()),
1817    }
1818}
1819
1820/// Constructs a `Value::Complex` or collapses to `Value::Scalar` when `im` is exactly zero.
1821#[inline]
1822fn make_complex(re: f64, im: f64) -> Value {
1823    if im == 0.0 {
1824        Value::Scalar(re)
1825    } else {
1826        Value::Complex(re, im)
1827    }
1828}
1829
1830/// Upcasts a real matrix to a complex matrix by setting all imaginary parts to zero.
1831#[inline]
1832fn cm_from_real(m: &Array2<f64>) -> Array2<Complex<f64>> {
1833    m.mapv(|x| Complex::new(x, 0.0))
1834}
1835
1836/// Performs binary operations between two `Array2<Complex<f64>>` matrices.
1837fn complex_matrix_binop(
1838    a: Array2<Complex<f64>>,
1839    op: &Op,
1840    b: Array2<Complex<f64>>,
1841) -> Result<Value, String> {
1842    let same_shape = || {
1843        if a.shape() != b.shape() {
1844            Err(format!(
1845                "Matrix dimensions must agree: {}×{} vs {}×{}",
1846                a.nrows(),
1847                a.ncols(),
1848                b.nrows(),
1849                b.ncols()
1850            ))
1851        } else {
1852            Ok(())
1853        }
1854    };
1855    match op {
1856        Op::Add => {
1857            same_shape()?;
1858            Ok(Value::ComplexMatrix(Box::new(a + b)))
1859        }
1860        Op::Sub => {
1861            same_shape()?;
1862            Ok(Value::ComplexMatrix(Box::new(a - b)))
1863        }
1864        Op::Mul => {
1865            if a.ncols() != b.nrows() {
1866                return Err(format!(
1867                    "Inner dimensions must agree: {}×{} * {}×{}",
1868                    a.nrows(),
1869                    a.ncols(),
1870                    b.nrows(),
1871                    b.ncols()
1872                ));
1873            }
1874            Ok(Value::ComplexMatrix(Box::new(a.dot(&b))))
1875        }
1876        Op::ElemMul => {
1877            same_shape()?;
1878            Ok(Value::ComplexMatrix(Box::new(a * b)))
1879        }
1880        Op::ElemDiv => {
1881            same_shape()?;
1882            Ok(Value::ComplexMatrix(Box::new(a / b)))
1883        }
1884        Op::ElemPow => {
1885            same_shape()?;
1886            Ok(Value::ComplexMatrix(Box::new(
1887                ndarray::Zip::from(&a)
1888                    .and(&b)
1889                    .map_collect(|x, y| x.powc(*y)),
1890            )))
1891        }
1892        Op::Pow => Err(
1893            "ComplexMatrix ^ ComplexMatrix: not supported; use .^ for element-wise power"
1894                .to_string(),
1895        ),
1896        Op::Div | Op::LDiv => {
1897            Err("Complex matrix / and \\ not supported; use inv(A)*B".to_string())
1898        }
1899        Op::Eq => {
1900            same_shape()?;
1901            Ok(Value::Matrix(Box::new(
1902                ndarray::Zip::from(&a)
1903                    .and(&b)
1904                    .map_collect(|x, y| bool_to_f64(x == y)),
1905            )))
1906        }
1907        Op::NotEq => {
1908            same_shape()?;
1909            Ok(Value::Matrix(Box::new(
1910                ndarray::Zip::from(&a)
1911                    .and(&b)
1912                    .map_collect(|x, y| bool_to_f64(x != y)),
1913            )))
1914        }
1915        Op::Lt | Op::Gt | Op::LtEq | Op::GtEq => {
1916            Err("Ordering comparison not defined for complex matrices".to_string())
1917        }
1918        Op::And | Op::ElemAnd => {
1919            same_shape()?;
1920            Ok(Value::Matrix(Box::new(
1921                ndarray::Zip::from(&a).and(&b).map_collect(|x, y| {
1922                    bool_to_f64((x.re != 0.0 || x.im != 0.0) && (y.re != 0.0 || y.im != 0.0))
1923                }),
1924            )))
1925        }
1926        Op::Or | Op::ElemOr => {
1927            same_shape()?;
1928            Ok(Value::Matrix(Box::new(
1929                ndarray::Zip::from(&a).and(&b).map_collect(|x, y| {
1930                    bool_to_f64(x.re != 0.0 || x.im != 0.0 || y.re != 0.0 || y.im != 0.0)
1931                }),
1932            )))
1933        }
1934    }
1935}
1936
1937/// Broadcasts a scalar to every element of a complex matrix.
1938fn cm_binop_scalar(cm: Array2<Complex<f64>>, op: &Op, s: f64) -> Result<Value, String> {
1939    let c = Complex::new(s, 0.0);
1940    match op {
1941        Op::Add => Ok(Value::ComplexMatrix(Box::new(cm.mapv(|x| x + c)))),
1942        Op::Sub => Ok(Value::ComplexMatrix(Box::new(cm.mapv(|x| x - c)))),
1943        Op::Mul | Op::ElemMul => Ok(Value::ComplexMatrix(Box::new(cm.mapv(|x| x * c)))),
1944        Op::Div | Op::ElemDiv => Ok(Value::ComplexMatrix(Box::new(cm.mapv(|x| x / c)))),
1945        Op::Pow | Op::ElemPow => Ok(Value::ComplexMatrix(Box::new(cm.mapv(|x| x.powf(s))))),
1946        Op::Eq => Ok(Value::Matrix(Box::new(cm.mapv(|x| bool_to_f64(x == c))))),
1947        Op::NotEq => Ok(Value::Matrix(Box::new(cm.mapv(|x| bool_to_f64(x != c))))),
1948        _ => Err("Unsupported operator between complex matrix and scalar".to_string()),
1949    }
1950}
1951
1952/// Broadcasts a scalar to every element of a complex matrix (scalar on the left).
1953fn scalar_binop_cm(s: f64, op: &Op, cm: Array2<Complex<f64>>) -> Result<Value, String> {
1954    let c = Complex::new(s, 0.0);
1955    match op {
1956        Op::Add => Ok(Value::ComplexMatrix(Box::new(cm.mapv(|x| c + x)))),
1957        Op::Sub => Ok(Value::ComplexMatrix(Box::new(cm.mapv(|x| c - x)))),
1958        Op::Mul | Op::ElemMul => Ok(Value::ComplexMatrix(Box::new(cm.mapv(|x| c * x)))),
1959        Op::Pow | Op::ElemPow => Ok(Value::ComplexMatrix(Box::new(cm.mapv(|x| c.powc(x))))),
1960        Op::Eq => Ok(Value::Matrix(Box::new(cm.mapv(|x| bool_to_f64(c == x))))),
1961        Op::NotEq => Ok(Value::Matrix(Box::new(cm.mapv(|x| bool_to_f64(c != x))))),
1962        _ => Err("Unsupported operator between scalar and complex matrix".to_string()),
1963    }
1964}
1965
1966/// Broadcasts a complex scalar to every element of a complex matrix.
1967fn cm_binop_complex(cm: Array2<Complex<f64>>, op: &Op, re: f64, im: f64) -> Result<Value, String> {
1968    let c = Complex::new(re, im);
1969    match op {
1970        Op::Add => Ok(Value::ComplexMatrix(Box::new(cm.mapv(|x| x + c)))),
1971        Op::Sub => Ok(Value::ComplexMatrix(Box::new(cm.mapv(|x| x - c)))),
1972        Op::Mul | Op::ElemMul => Ok(Value::ComplexMatrix(Box::new(cm.mapv(|x| x * c)))),
1973        Op::Div | Op::ElemDiv => Ok(Value::ComplexMatrix(Box::new(cm.mapv(|x| x / c)))),
1974        Op::Pow | Op::ElemPow => Ok(Value::ComplexMatrix(Box::new(cm.mapv(|x| x.powc(c))))),
1975        Op::Eq => Ok(Value::Matrix(Box::new(cm.mapv(|x| bool_to_f64(x == c))))),
1976        Op::NotEq => Ok(Value::Matrix(Box::new(cm.mapv(|x| bool_to_f64(x != c))))),
1977        _ => Err("Unsupported operator between complex matrix and complex scalar".to_string()),
1978    }
1979}
1980
1981/// Broadcasts a complex scalar (on the left) to every element of a complex matrix.
1982fn complex_binop_cm(re: f64, im: f64, op: &Op, cm: Array2<Complex<f64>>) -> Result<Value, String> {
1983    let c = Complex::new(re, im);
1984    match op {
1985        Op::Add => Ok(Value::ComplexMatrix(Box::new(cm.mapv(|x| c + x)))),
1986        Op::Sub => Ok(Value::ComplexMatrix(Box::new(cm.mapv(|x| c - x)))),
1987        Op::Mul | Op::ElemMul => Ok(Value::ComplexMatrix(Box::new(cm.mapv(|x| c * x)))),
1988        Op::Pow | Op::ElemPow => Ok(Value::ComplexMatrix(Box::new(cm.mapv(|x| c.powc(x))))),
1989        Op::Eq => Ok(Value::Matrix(Box::new(cm.mapv(|x| bool_to_f64(c == x))))),
1990        Op::NotEq => Ok(Value::Matrix(Box::new(cm.mapv(|x| bool_to_f64(c != x))))),
1991        _ => Err("Unsupported operator between complex scalar and complex matrix".to_string()),
1992    }
1993}
1994
1995/// Converts a char array string to its numeric representation.
1996/// Single char → Scalar(code), multi-char → 1×N Matrix, empty → 1×0 Matrix.
1997fn str_to_numeric(s: &str) -> Value {
1998    let codes: Vec<f64> = s.chars().map(|c| c as u32 as f64).collect();
1999    match codes.len() {
2000        0 => Value::Matrix(Box::new(Array2::zeros((1, 0)))),
2001        1 => Value::Scalar(codes[0]),
2002        n => Value::Matrix(Box::new(Array2::from_shape_vec((1, n), codes).unwrap())),
2003    }
2004}
2005
2006/// Extracts a string slice from a Str or StringObj value.
2007fn string_arg<'a>(v: &'a Value, fname: &str, pos: usize) -> Result<&'a str, String> {
2008    match v {
2009        Value::Str(s) | Value::StringObj(s) => Ok(s.as_str()),
2010        _ => Err(format!(
2011            "Function '{fname}' argument {pos} must be a string"
2012        )),
2013    }
2014}
2015
2016fn check_same_shape(lm: &Array2<f64>, rm: &Array2<f64>) -> Result<(), String> {
2017    if lm.shape() != rm.shape() {
2018        return Err(format!(
2019            "Matrix size mismatch: {}x{} vs {}x{}",
2020            lm.nrows(),
2021            lm.ncols(),
2022            rm.nrows(),
2023            rm.ncols()
2024        ));
2025    }
2026    Ok(())
2027}
2028
2029fn scalar_arg(v: &Value, fname: &str, pos: usize) -> Result<f64, String> {
2030    match v {
2031        Value::Void => Err(format!(
2032            "Function '{fname}' argument {pos} must be a scalar, got void"
2033        )),
2034        Value::Scalar(n) => Ok(*n),
2035        Value::Complex(re, im) if *im == 0.0 => Ok(*re),
2036        Value::Complex(_, _) => Err(format!(
2037            "Function '{fname}' argument {pos} must be real, got a complex number"
2038        )),
2039        Value::Matrix(_) => Err(format!(
2040            "Function '{fname}' argument {pos} must be a scalar, got a matrix"
2041        )),
2042        Value::ComplexMatrix(_) => Err(format!(
2043            "Function '{fname}' argument {pos} must be a scalar, got a complex matrix"
2044        )),
2045        Value::Str(s) if s.chars().count() == 1 => Ok(s.chars().next().unwrap() as u32 as f64),
2046        Value::Str(_) | Value::StringObj(_) => Err(format!(
2047            "Function '{fname}' argument {pos} must be a scalar, got a string"
2048        )),
2049        Value::Lambda(_)
2050        | Value::Function(_)
2051        | Value::Tuple(_)
2052        | Value::Cell(_)
2053        | Value::Struct(_)
2054        | Value::StructArray(_)
2055        | Value::DateTime(_)
2056        | Value::Duration(_)
2057        | Value::DateTimeArray(_)
2058        | Value::DurationArray(_)
2059        | Value::Map(_) => Err(format!(
2060            "Function '{fname}' argument {pos} must be a scalar, got a non-numeric value"
2061        )),
2062    }
2063}
2064
2065/// Interprets a single size argument for matrix constructors like `zeros`, `ones`,
2066/// `rand`, `randn`, and `nan`.
2067///
2068/// Accepts:
2069/// - `Scalar(n)` → `(n, n)` square matrix
2070/// - 1×1 `Matrix([n])` → `(n, n)` square matrix
2071/// - 1×2 or 2×1 `Matrix([r, c])` → `(r, c)` — enables `zeros(size(A))`
2072///
2073/// Returns `Err` for all other shapes.
2074fn size_arg(v: &Value, fname: &str) -> Result<(usize, usize), String> {
2075    match v {
2076        Value::Scalar(n) => Ok((*n as usize, *n as usize)),
2077        Value::Matrix(m) => {
2078            let elems: Vec<f64> = m.iter().copied().collect();
2079            match elems.as_slice() {
2080                [n] => Ok((*n as usize, *n as usize)),
2081                [r, c] => Ok((*r as usize, *c as usize)),
2082                _ => Err(format!(
2083                    "{fname}: size argument must be a scalar or a 1×2 vector, \
2084                     got a {}×{} matrix",
2085                    m.nrows(),
2086                    m.ncols()
2087                )),
2088            }
2089        }
2090        _ => Err(format!(
2091            "{fname}: size argument must be a scalar or a [rows cols] vector"
2092        )),
2093    }
2094}
2095
2096/// Applies a scalar function element-wise to a scalar or matrix.
2097/// Parses the first argument of `randi` into an inclusive `[lo, hi]` integer range.
2098///
2099/// Accepts either a scalar `max` (→ `[1, max]`) or a 1×2 / 2×1 vector `[min, max]`.
2100fn randi_range(v: &Value) -> Result<(i64, i64), String> {
2101    match v {
2102        Value::Scalar(n) => {
2103            let hi = *n as i64;
2104            if hi < 1 {
2105                return Err("randi: max must be a positive integer".to_string());
2106            }
2107            Ok((1, hi))
2108        }
2109        Value::Matrix(m) if m.len() == 2 => {
2110            let vals: Vec<f64> = m.iter().copied().collect();
2111            let lo = vals[0] as i64;
2112            let hi = vals[1] as i64;
2113            if lo > hi {
2114                return Err("randi: [min, max] range is empty".to_string());
2115            }
2116            Ok((lo, hi))
2117        }
2118        _ => Err("randi: first argument must be a scalar max or a [min, max] vector".to_string()),
2119    }
2120}
2121
2122// ── Descriptive statistics helpers ───────────────────────────────────────────
2123
2124/// Extracts a flat `Vec<f64>` from a `Scalar` or `Matrix` value.
2125fn numeric_vec(v: &Value, fname: &str) -> Result<Vec<f64>, String> {
2126    match v {
2127        Value::Scalar(n) => Ok(vec![*n]),
2128        Value::Matrix(m) => Ok(m.iter().copied().collect()),
2129        _ => Err(format!("{fname}: argument must be numeric")),
2130    }
2131}
2132
2133/// Computes the variance of a slice.  Returns `NaN` for empty, `0.0` for singletons.
2134/// `population = true` divides by N; `false` divides by N-1.
2135fn stat_var_vec(vals: &[f64], population: bool) -> f64 {
2136    let n = vals.len();
2137    if n == 0 {
2138        return f64::NAN;
2139    }
2140    if n == 1 {
2141        return 0.0;
2142    }
2143    let mean = vals.iter().sum::<f64>() / n as f64;
2144    let ss: f64 = vals.iter().map(|&x| (x - mean).powi(2)).sum();
2145    let denom = if population { n as f64 } else { (n - 1) as f64 };
2146    ss / denom
2147}
2148
2149/// Applies a column-wise statistical closure returning one scalar per column.
2150///
2151/// - Scalar → passes `[n]` to `f`.
2152/// - Vector (1×N or N×1) → scalar.
2153/// - M×N matrix (M>1, N>1) → 1×N row vector.
2154fn apply_stat<F>(v: &Value, mut f: F, fname: &str) -> Result<Value, String>
2155where
2156    F: FnMut(&[f64]) -> f64,
2157{
2158    match v {
2159        Value::Scalar(n) => Ok(Value::Scalar(f(&[*n]))),
2160        Value::Matrix(m) => {
2161            if m.nrows() == 1 || m.ncols() == 1 {
2162                let vals: Vec<f64> = m.iter().copied().collect();
2163                Ok(Value::Scalar(f(&vals)))
2164            } else {
2165                let ncols = m.ncols();
2166                let result: Vec<f64> = (0..ncols)
2167                    .map(|c| {
2168                        let col: Vec<f64> = m.column(c).iter().copied().collect();
2169                        f(&col)
2170                    })
2171                    .collect();
2172                Ok(Value::Matrix(Box::new(
2173                    Array2::from_shape_vec((1, ncols), result).unwrap(),
2174                )))
2175            }
2176        }
2177        _ => Err(format!("{fname}: argument must be numeric")),
2178    }
2179}
2180
2181/// Computes the p-th percentile (0–100) of a pre-sorted slice via linear interpolation.
2182fn percentile_sorted(sorted: &[f64], p: f64) -> f64 {
2183    let n = sorted.len();
2184    if n == 0 {
2185        return f64::NAN;
2186    }
2187    if n == 1 {
2188        return sorted[0];
2189    }
2190    let p = p.clamp(0.0, 100.0);
2191    // Octave/MATLAB Type 5 (Hazen): r = n*p/100 + 0.5 (1-indexed), clamped to [1,n].
2192    let idx = (p / 100.0 * n as f64 - 0.5).max(0.0).min((n - 1) as f64);
2193    let lo = idx.floor() as usize;
2194    let hi = idx.ceil() as usize;
2195    let frac = idx - lo as f64;
2196    sorted[lo] * (1.0 - frac) + sorted[hi] * frac
2197}
2198
2199fn apply_elem<F: Fn(f64) -> f64>(v: &Value, f: F) -> Result<Value, String> {
2200    match v {
2201        Value::Void => Err("Element-wise function not applicable to void".to_string()),
2202        Value::Scalar(n) => Ok(Value::Scalar(f(*n))),
2203        Value::Matrix(m) => Ok(Value::Matrix(Box::new(m.mapv(f)))),
2204        Value::Complex(re, im) if *im == 0.0 => Ok(Value::Scalar(f(*re))),
2205        Value::Complex(_, _) => {
2206            Err("Element-wise real function not applicable to complex values".to_string())
2207        }
2208        Value::ComplexMatrix(_) => {
2209            Err("Element-wise real function not applicable to complex matrices".to_string())
2210        }
2211        Value::Str(_) | Value::StringObj(_) => {
2212            Err("Element-wise function not applicable to strings".to_string())
2213        }
2214        Value::Lambda(_)
2215        | Value::Function(_)
2216        | Value::Tuple(_)
2217        | Value::Cell(_)
2218        | Value::Struct(_)
2219        | Value::StructArray(_)
2220        | Value::DateTime(_)
2221        | Value::Duration(_)
2222        | Value::DateTimeArray(_)
2223        | Value::DurationArray(_)
2224        | Value::Map(_) => Err("Element-wise function not applicable to this type".to_string()),
2225    }
2226}
2227
2228/// Reduces a scalar or matrix to a scalar (for vectors) or 1×N row vector (for M×N matrices).
2229///
2230/// - Scalar → apply `f` to `[n]`.
2231/// - Vector (1×N or N×1) → apply `f` to all elements, return scalar.
2232/// - M×N matrix (M>1, N>1) → apply `f` column-wise, return 1×N row vector.
2233fn apply_reduction<F>(v: &Value, f: F) -> Result<Value, String>
2234where
2235    F: Fn(&[f64]) -> f64,
2236{
2237    match v {
2238        Value::Void => Err("Reduction not applicable to void".to_string()),
2239        Value::Scalar(n) => Ok(Value::Scalar(f(&[*n]))),
2240        Value::Complex(_, _) => Err("Reduction not applicable to complex values".to_string()),
2241        Value::ComplexMatrix(_) => Err("Reduction not applicable to complex matrices".to_string()),
2242        Value::Str(_) | Value::StringObj(_) => {
2243            Err("Reduction not applicable to strings".to_string())
2244        }
2245        Value::Lambda(_)
2246        | Value::Function(_)
2247        | Value::Tuple(_)
2248        | Value::Cell(_)
2249        | Value::Struct(_)
2250        | Value::StructArray(_)
2251        | Value::DateTime(_)
2252        | Value::Duration(_)
2253        | Value::DateTimeArray(_)
2254        | Value::DurationArray(_)
2255        | Value::Map(_) => Err("Reduction not applicable to this type".to_string()),
2256        Value::Matrix(m) => {
2257            if m.nrows() == 1 || m.ncols() == 1 {
2258                let vals: Vec<f64> = m.iter().copied().collect();
2259                Ok(Value::Scalar(f(&vals)))
2260            } else {
2261                let ncols = m.ncols();
2262                let result: Vec<f64> = (0..ncols)
2263                    .map(|c| {
2264                        let col: Vec<f64> = m.column(c).iter().copied().collect();
2265                        f(&col)
2266                    })
2267                    .collect();
2268                Ok(Value::Matrix(Box::new(
2269                    Array2::from_shape_vec((1, ncols), result).unwrap(),
2270                )))
2271            }
2272        }
2273    }
2274}
2275
2276/// Column-wise reduction over a `ComplexMatrix` (or any numeric value treated as complex).
2277///
2278/// For vectors (1×N or N×1) returns a `Complex`/`Scalar`; for M×N matrices returns a
2279/// `ComplexMatrix` 1×N row vector.
2280fn apply_cm_reduction<F>(v: &Value, f: F) -> Result<Value, String>
2281where
2282    F: Fn(&[Complex<f64>]) -> Complex<f64>,
2283{
2284    let make_scalar = |c: Complex<f64>| -> Value {
2285        if c.im == 0.0 {
2286            Value::Scalar(c.re)
2287        } else {
2288            Value::Complex(c.re, c.im)
2289        }
2290    };
2291    match v {
2292        Value::Scalar(n) => Ok(make_scalar(f(&[Complex::new(*n, 0.0)]))),
2293        Value::Complex(re, im) => Ok(make_scalar(f(&[Complex::new(*re, *im)]))),
2294        Value::Matrix(m) => {
2295            if m.nrows() == 1 || m.ncols() == 1 {
2296                let vals: Vec<Complex<f64>> = m.iter().map(|&x| Complex::new(x, 0.0)).collect();
2297                Ok(make_scalar(f(&vals)))
2298            } else {
2299                let ncols = m.ncols();
2300                let result: Vec<Complex<f64>> = (0..ncols)
2301                    .map(|c| {
2302                        let col: Vec<Complex<f64>> =
2303                            m.column(c).iter().map(|&x| Complex::new(x, 0.0)).collect();
2304                        f(&col)
2305                    })
2306                    .collect();
2307                if result.iter().all(|c| c.im == 0.0) {
2308                    let reals: Vec<f64> = result.iter().map(|c| c.re).collect();
2309                    Ok(Value::Matrix(Box::new(
2310                        Array2::from_shape_vec((1, ncols), reals).unwrap(),
2311                    )))
2312                } else {
2313                    Ok(Value::ComplexMatrix(Box::new(
2314                        Array2::from_shape_vec((1, ncols), result).unwrap(),
2315                    )))
2316                }
2317            }
2318        }
2319        Value::ComplexMatrix(m) => {
2320            if m.nrows() == 1 || m.ncols() == 1 {
2321                let vals: Vec<Complex<f64>> = m.iter().copied().collect();
2322                Ok(make_scalar(f(&vals)))
2323            } else {
2324                let ncols = m.ncols();
2325                let result: Vec<Complex<f64>> = (0..ncols)
2326                    .map(|c| {
2327                        let col: Vec<Complex<f64>> = m.column(c).iter().copied().collect();
2328                        f(&col)
2329                    })
2330                    .collect();
2331                if result.iter().all(|c| c.im == 0.0) {
2332                    let reals: Vec<f64> = result.iter().map(|c| c.re).collect();
2333                    Ok(Value::Matrix(Box::new(
2334                        Array2::from_shape_vec((1, ncols), reals).unwrap(),
2335                    )))
2336                } else {
2337                    Ok(Value::ComplexMatrix(Box::new(
2338                        Array2::from_shape_vec((1, ncols), result).unwrap(),
2339                    )))
2340                }
2341            }
2342        }
2343        _ => Err("Reduction not applicable to this type".to_string()),
2344    }
2345}
2346
2347/// Computes a cumulative scan (cumsum / cumprod) along a vector or column-wise on a matrix.
2348///
2349/// `combine(accumulator, element) -> new_accumulator` — e.g. `|a, x| a + x` for cumsum.
2350fn apply_cumulative<F>(v: &Value, combine: F) -> Result<Value, String>
2351where
2352    F: Fn(f64, f64) -> f64,
2353{
2354    match v {
2355        Value::Void => Err("Cumulative reduction not applicable to void".to_string()),
2356        Value::Scalar(n) => Ok(Value::Scalar(*n)),
2357        Value::Complex(_, _) => {
2358            Err("Cumulative reduction not applicable to complex values".to_string())
2359        }
2360        Value::ComplexMatrix(_) => {
2361            Err("Cumulative reduction not applicable to complex matrices".to_string())
2362        }
2363        Value::Str(_) | Value::StringObj(_) => {
2364            Err("Cumulative reduction not applicable to strings".to_string())
2365        }
2366        Value::Lambda(_)
2367        | Value::Function(_)
2368        | Value::Tuple(_)
2369        | Value::Cell(_)
2370        | Value::Struct(_)
2371        | Value::StructArray(_)
2372        | Value::DateTime(_)
2373        | Value::Duration(_)
2374        | Value::DateTimeArray(_)
2375        | Value::DurationArray(_)
2376        | Value::Map(_) => Err("Cumulative reduction not applicable to this type".to_string()),
2377        Value::Matrix(m) => {
2378            let initial = combine(0.0, 0.0); // detect identity: 0+0=0 or 0*0=0
2379            // Use 0.0 as additive identity, 1.0 as multiplicative identity.
2380            // We detect the identity from f(1.0, 1.0) vs f(0.0, 0.0).
2381            let identity = if (combine(1.0, 1.0) - 1.0).abs() < 1e-15 && initial == 0.0 {
2382                1.0 // product
2383            } else {
2384                0.0 // sum
2385            };
2386            let (nrows, ncols) = (m.nrows(), m.ncols());
2387            let mut result = m.clone();
2388            if nrows == 1 || ncols == 1 {
2389                // Vector: scan along all elements in order
2390                let mut acc = identity;
2391                for v in result.iter_mut() {
2392                    acc = combine(acc, *v);
2393                    *v = acc;
2394                }
2395            } else {
2396                // Matrix: scan each column independently
2397                for c in 0..ncols {
2398                    let mut acc = identity;
2399                    for r in 0..nrows {
2400                        acc = combine(acc, result[[r, c]]);
2401                        result[[r, c]] = acc;
2402                    }
2403                }
2404            }
2405            Ok(Value::Matrix(result))
2406        }
2407    }
2408}
2409
2410/// Returns column-major 1-based indices of non-zero elements, up to `max_k`.
2411fn find_nonzero(v: &Value, max_k: usize) -> Result<Value, String> {
2412    match v {
2413        Value::Void => Err("find: not applicable to void".to_string()),
2414        Value::ComplexMatrix(_) => Err("find: not applicable to complex matrices".to_string()),
2415        Value::Str(_) | Value::StringObj(_) => Err("find: not applicable to strings".to_string()),
2416        Value::Lambda(_)
2417        | Value::Function(_)
2418        | Value::Tuple(_)
2419        | Value::Cell(_)
2420        | Value::Struct(_)
2421        | Value::StructArray(_)
2422        | Value::DateTime(_)
2423        | Value::Duration(_)
2424        | Value::DateTimeArray(_)
2425        | Value::DurationArray(_)
2426        | Value::Map(_) => Err("find: not applicable to this type".to_string()),
2427        Value::Complex(re, im) => {
2428            if (*re != 0.0 || *im != 0.0) && max_k >= 1 {
2429                Ok(Value::Matrix(Box::new(
2430                    Array2::from_shape_vec((1, 1), vec![1.0]).unwrap(),
2431                )))
2432            } else {
2433                Ok(Value::Matrix(Box::new(Array2::zeros((1, 0)))))
2434            }
2435        }
2436        Value::Scalar(n) => {
2437            if *n != 0.0 && max_k >= 1 {
2438                Ok(Value::Matrix(Box::new(
2439                    Array2::from_shape_vec((1, 1), vec![1.0]).unwrap(),
2440                )))
2441            } else {
2442                Ok(Value::Matrix(Box::new(Array2::zeros((1, 0)))))
2443            }
2444        }
2445        Value::Matrix(m) => {
2446            let nrows = m.nrows();
2447            let total = m.len();
2448            let mut idxs: Vec<f64> = Vec::new();
2449            for i in 0..total {
2450                if idxs.len() >= max_k {
2451                    break;
2452                }
2453                let row = i % nrows;
2454                let col = i / nrows;
2455                if m[[row, col]] != 0.0 {
2456                    idxs.push((i + 1) as f64);
2457                }
2458            }
2459            let n = idxs.len();
2460            if n == 0 {
2461                Ok(Value::Matrix(Box::new(Array2::zeros((1, 0)))))
2462            } else {
2463                Ok(Value::Matrix(Box::new(
2464                    Array2::from_shape_vec((1, n), idxs).unwrap(),
2465                )))
2466            }
2467        }
2468    }
2469}
2470
2471// ---------------------------------------------------------------------------
2472// C-style printf format engine
2473// ---------------------------------------------------------------------------
2474
2475/// Formats `args` using a C-style `fmt` string.
2476///
2477/// Supported specifiers: `%d` `%i` `%f` `%e` `%g` `%s` `%%`.
2478/// Flags: `-` (left-align), `+` (force sign), `0` (zero-pad), ` ` (space sign).
2479/// Width and `.precision` follow standard C `printf` conventions.
2480/// Escape sequences `\n` `\t` `\\` are also processed.
2481///
2482/// Octave behaviour: if `args` is longer than the number of specifiers the
2483/// format string is repeated until all args are consumed.
2484pub fn format_printf(fmt: &str, args: &[Value]) -> Result<String, String> {
2485    let mut result = String::new();
2486    let mut arg_idx = 0;
2487
2488    loop {
2489        let consumed_before = arg_idx;
2490        let mut chars = fmt.chars().peekable();
2491
2492        while let Some(c) = chars.next() {
2493            if c == '\\' {
2494                match chars.next() {
2495                    Some('n') => result.push('\n'),
2496                    Some('t') => result.push('\t'),
2497                    Some('\\') => result.push('\\'),
2498                    Some('\'') => result.push('\''),
2499                    Some('"') => result.push('"'),
2500                    Some(other) => {
2501                        result.push('\\');
2502                        result.push(other);
2503                    }
2504                    None => result.push('\\'),
2505                }
2506                continue;
2507            }
2508
2509            if c != '%' {
2510                result.push(c);
2511                continue;
2512            }
2513
2514            // `%%` → literal `%`
2515            if chars.peek() == Some(&'%') {
2516                chars.next();
2517                result.push('%');
2518                continue;
2519            }
2520
2521            // Parse flags
2522            let mut flag_minus = false;
2523            let mut flag_plus = false;
2524            let mut flag_zero = false;
2525            let mut flag_space = false;
2526            loop {
2527                match chars.peek() {
2528                    Some('-') => {
2529                        flag_minus = true;
2530                        chars.next();
2531                    }
2532                    Some('+') => {
2533                        flag_plus = true;
2534                        chars.next();
2535                    }
2536                    Some('0') => {
2537                        flag_zero = true;
2538                        chars.next();
2539                    }
2540                    Some(' ') => {
2541                        flag_space = true;
2542                        chars.next();
2543                    }
2544                    _ => break,
2545                }
2546            }
2547
2548            // Parse width
2549            let mut width_str = String::new();
2550            while let Some(&d) = chars.peek() {
2551                if d.is_ascii_digit() {
2552                    width_str.push(d);
2553                    chars.next();
2554                } else {
2555                    break;
2556                }
2557            }
2558            let width: usize = width_str.parse().unwrap_or(0);
2559
2560            // Parse precision
2561            let mut precision: Option<usize> = None;
2562            if chars.peek() == Some(&'.') {
2563                chars.next();
2564                let mut p = String::new();
2565                while let Some(&d) = chars.peek() {
2566                    if d.is_ascii_digit() {
2567                        p.push(d);
2568                        chars.next();
2569                    } else {
2570                        break;
2571                    }
2572                }
2573                precision = Some(p.parse().unwrap_or(0));
2574            }
2575
2576            // Specifier character
2577            let spec = match chars.next() {
2578                Some(s) => s,
2579                None => {
2580                    return Err("fprintf: incomplete format specifier at end of string".to_string());
2581                }
2582            };
2583
2584            // No more args — silently skip remaining specifiers
2585            if arg_idx >= args.len() {
2586                continue;
2587            }
2588
2589            let arg = &args[arg_idx];
2590            arg_idx += 1;
2591
2592            let formatted = match spec {
2593                'd' | 'i' => {
2594                    let n = printf_scalar(arg, spec)?;
2595                    let i = n.trunc() as i64;
2596                    let s = printf_sign_str(i >= 0, flag_plus, flag_space, format!("{}", i.abs()));
2597                    printf_pad(s, width, flag_minus, flag_zero)
2598                }
2599                'f' => {
2600                    let n = printf_scalar(arg, spec)?;
2601                    let prec = precision.unwrap_or(6);
2602                    let s = printf_sign_str(
2603                        n >= 0.0,
2604                        flag_plus,
2605                        flag_space,
2606                        format!("{:.prec$}", n.abs(), prec = prec),
2607                    );
2608                    printf_pad(s, width, flag_minus, flag_zero)
2609                }
2610                'e' | 'E' => {
2611                    let n = printf_scalar(arg, spec)?;
2612                    let prec = precision.unwrap_or(6);
2613                    let s = printf_format_sci(n, prec, flag_plus, flag_space, spec == 'E');
2614                    printf_pad(s, width, flag_minus, flag_zero)
2615                }
2616                'g' | 'G' => {
2617                    let n = printf_scalar(arg, spec)?;
2618                    let prec = precision.unwrap_or(6).max(1);
2619                    let s = printf_format_g(n, prec, flag_plus, flag_space, spec == 'G');
2620                    printf_pad(s, width, flag_minus, flag_zero)
2621                }
2622                'x' | 'X' => {
2623                    let n = printf_scalar(arg, spec)?;
2624                    let i = n.trunc() as u64;
2625                    let hex = if spec == 'X' {
2626                        format!("{:X}", i)
2627                    } else {
2628                        format!("{:x}", i)
2629                    };
2630                    printf_pad(hex, width, flag_minus, flag_zero)
2631                }
2632                's' => {
2633                    let s = printf_string(arg)?;
2634                    let s = if let Some(max_len) = precision {
2635                        s.chars().take(max_len).collect::<String>()
2636                    } else {
2637                        s
2638                    };
2639                    printf_pad(s, width, flag_minus, false)
2640                }
2641                other => return Err(format!("fprintf: unknown format specifier '%{other}'")),
2642            };
2643
2644            result.push_str(&formatted);
2645        }
2646
2647        // Stop if all args consumed or no specifiers were found (infinite loop guard)
2648        if arg_idx >= args.len() || arg_idx == consumed_before {
2649            break;
2650        }
2651    }
2652
2653    Ok(result)
2654}
2655
2656/// Extracts a scalar f64 from a Value for use in numeric printf specifiers.
2657fn printf_scalar(v: &Value, spec: char) -> Result<f64, String> {
2658    match v {
2659        Value::Scalar(n) => Ok(*n),
2660        Value::Complex(re, im) if *im == 0.0 => Ok(*re),
2661        Value::Str(s) if s.chars().count() == 1 => Ok(s.chars().next().unwrap() as u32 as f64),
2662        _ => Err(format!(
2663            "fprintf: expected numeric argument for '%{spec}', got {:?}",
2664            std::mem::discriminant(v)
2665        )),
2666    }
2667}
2668
2669/// Extracts a string from a Value for use in `%s`.
2670fn printf_string(v: &Value) -> Result<String, String> {
2671    match v {
2672        Value::Str(s) | Value::StringObj(s) => Ok(s.clone()),
2673        Value::Scalar(n) => Ok(format_number(*n)),
2674        Value::Complex(re, im) => Ok(format_complex(*re, *im, &FormatMode::Custom(6))),
2675        Value::Void => Err("fprintf: cannot format void as string".to_string()),
2676        Value::Matrix(_) => Err("fprintf: cannot format matrix as string".to_string()),
2677        Value::ComplexMatrix(_) => {
2678            Err("fprintf: cannot format complex matrix as string".to_string())
2679        }
2680        Value::DateTime(ts) => Ok(crate::datetime::format_datetime(*ts)),
2681        Value::Duration(s) => Ok(crate::datetime::format_duration(*s)),
2682        Value::Lambda(_)
2683        | Value::Function(_)
2684        | Value::Tuple(_)
2685        | Value::Cell(_)
2686        | Value::Struct(_)
2687        | Value::StructArray(_)
2688        | Value::DateTimeArray(_)
2689        | Value::DurationArray(_)
2690        | Value::Map(_) => Err("fprintf: cannot format this type as string".to_string()),
2691    }
2692}
2693
2694/// Builds a sign-prefixed string: `+n`, ` n`, `-n`, or bare `n`.
2695fn printf_sign_str(positive: bool, flag_plus: bool, flag_space: bool, digits: String) -> String {
2696    if positive {
2697        if flag_plus {
2698            format!("+{digits}")
2699        } else if flag_space {
2700            format!(" {digits}")
2701        } else {
2702            digits
2703        }
2704    } else {
2705        format!("-{digits}")
2706    }
2707}
2708
2709/// Right- or left-pads `s` to at least `width` chars, optionally zero-pads.
2710fn printf_pad(s: String, width: usize, left_align: bool, zero_pad: bool) -> String {
2711    if s.len() >= width {
2712        return s;
2713    }
2714    let pad_len = width - s.len();
2715    if left_align {
2716        format!("{s}{}", " ".repeat(pad_len))
2717    } else if zero_pad {
2718        // Insert zeros after optional sign
2719        let (prefix, rest) = if s.starts_with(['+', '-', ' ']) {
2720            s.split_at(1)
2721        } else {
2722            ("", s.as_str())
2723        };
2724        format!("{prefix}{}{rest}", "0".repeat(pad_len))
2725    } else {
2726        format!("{}{s}", " ".repeat(pad_len))
2727    }
2728}
2729
2730/// Formats `n` in scientific notation matching C `%e` / `%E`.
2731/// Always produces at least 2 exponent digits with an explicit sign: `1.23e+04`.
2732fn printf_format_sci(
2733    n: f64,
2734    prec: usize,
2735    flag_plus: bool,
2736    flag_space: bool,
2737    upper: bool,
2738) -> String {
2739    if n == 0.0 {
2740        let zeros = "0".repeat(prec);
2741        let sep = if prec > 0 {
2742            format!(".{zeros}")
2743        } else {
2744            String::new()
2745        };
2746        let e_char = if upper { 'E' } else { 'e' };
2747        let sign = if flag_plus {
2748            "+"
2749        } else if flag_space {
2750            " "
2751        } else {
2752            ""
2753        };
2754        return format!("{sign}0{sep}{e_char}+00");
2755    }
2756
2757    let neg = n < 0.0;
2758    let abs_n = n.abs();
2759    let exp = abs_n.log10().floor() as i32;
2760    let mantissa = abs_n / 10f64.powi(exp);
2761    let man_str = format!("{:.prec$}", mantissa, prec = prec);
2762
2763    let e_char = if upper { 'E' } else { 'e' };
2764    let exp_sign = if exp >= 0 { '+' } else { '-' };
2765    let exp_abs = exp.unsigned_abs();
2766    let exp_str = if exp_abs < 10 {
2767        format!("{e_char}{exp_sign}0{exp_abs}")
2768    } else {
2769        format!("{e_char}{exp_sign}{exp_abs}")
2770    };
2771
2772    let sign_str = if neg {
2773        "-"
2774    } else if flag_plus {
2775        "+"
2776    } else if flag_space {
2777        " "
2778    } else {
2779        ""
2780    };
2781    format!("{sign_str}{man_str}{exp_str}")
2782}
2783
2784/// Formats `n` using `%g` / `%G` rules:
2785/// uses `%e` if exponent < -4 or >= prec, otherwise `%f`; trims trailing zeros.
2786fn printf_format_g(n: f64, prec: usize, flag_plus: bool, flag_space: bool, upper: bool) -> String {
2787    if n == 0.0 {
2788        let sign = if flag_plus {
2789            "+"
2790        } else if flag_space {
2791            " "
2792        } else {
2793            ""
2794        };
2795        return format!("{sign}0");
2796    }
2797    let abs_n = n.abs();
2798    let exp = abs_n.log10().floor() as i32;
2799    if exp < -4 || exp >= prec as i32 {
2800        let s = printf_format_sci(n, prec.saturating_sub(1), flag_plus, flag_space, upper);
2801        trim_g_sci(s, upper)
2802    } else {
2803        let decimal_places = (prec as i32 - 1 - exp).max(0) as usize;
2804        let neg = n < 0.0;
2805        let s = format!("{:.prec$}", abs_n, prec = decimal_places);
2806        let s = if s.contains('.') {
2807            s.trim_end_matches('0').trim_end_matches('.').to_string()
2808        } else {
2809            s
2810        };
2811        let sign = if neg {
2812            "-"
2813        } else if flag_plus {
2814            "+"
2815        } else if flag_space {
2816            " "
2817        } else {
2818            ""
2819        };
2820        format!("{sign}{s}")
2821    }
2822}
2823
2824/// Trims trailing zeros from the mantissa of a scientific-notation string `1.230e+04` → `1.23e+04`.
2825fn trim_g_sci(s: String, upper: bool) -> String {
2826    let e_char = if upper { 'E' } else { 'e' };
2827    if let Some(e_pos) = s.find(e_char) {
2828        let mantissa = &s[..e_pos];
2829        let exp_part = &s[e_pos..];
2830        let trimmed = if mantissa.contains('.') {
2831            mantissa.trim_end_matches('0').trim_end_matches('.')
2832        } else {
2833            mantissa
2834        };
2835        format!("{trimmed}{exp_part}")
2836    } else {
2837        s
2838    }
2839}
2840
2841/// Calls a `Lambda` or `Function` value with the given arguments.
2842///
2843/// Used by `cellfun` and `arrayfun` to apply a function to each element
2844/// without going through the name-lookup path.
2845fn call_function_value(
2846    f: &Value,
2847    args: &[Value],
2848    io: Option<&mut IoContext>,
2849) -> Result<Value, String> {
2850    match f {
2851        Value::Lambda(lf) => {
2852            let lf = lf.clone();
2853            lf.0(args, io)
2854        }
2855        Value::Function(_) => {
2856            // Named function called via cellfun/arrayfun — name is unknown at this point.
2857            // Use a minimal env that doesn't export any user variables to avoid
2858            // polluting the caller's scope. Functions see their own scope via exec.
2859            let empty_env = Env::new();
2860            match io {
2861                Some(io_ref) => FN_CALL_HOOK.with(|c| match c.get() {
2862                    Some(hook) => hook("<anonymous>", f, args, &empty_env, io_ref),
2863                    None => Err("User function execution not initialized".to_string()),
2864                }),
2865                None => {
2866                    let mut tmp_io = IoContext::new();
2867                    FN_CALL_HOOK.with(|c| match c.get() {
2868                        Some(hook) => hook("<anonymous>", f, args, &empty_env, &mut tmp_io),
2869                        None => Err("User function execution not initialized".to_string()),
2870                    })
2871                }
2872            }
2873        }
2874        _ => Err("cellfun/arrayfun: first argument must be a function or lambda (@fn)".to_string()),
2875    }
2876}
2877
2878/// Names of all built-in functions recognized by `call_builtin`.
2879///
2880/// Used for REPL tab completion and "did you mean?" suggestions.
2881pub fn builtin_names() -> &'static [&'static str] {
2882    &[
2883        "abs",
2884        "acos",
2885        "all",
2886        "angle",
2887        "any",
2888        "arrayfun",
2889        "asin",
2890        "assert",
2891        "atan",
2892        "atan2",
2893        "bitand",
2894        "bitnot",
2895        "bitor",
2896        "bitshift",
2897        "bitxor",
2898        "ceil",
2899        "cell",
2900        "cellfun",
2901        "chol",
2902        "complex",
2903        "cond",
2904        "cross",
2905        "conj",
2906        "contains",
2907        "conv",
2908        "cos",
2909        "cov",
2910        "cumprod",
2911        "cumsum",
2912        "datenum",
2913        "datestr",
2914        "datevec",
2915        "datetime",
2916        "day",
2917        "days",
2918        "deconv",
2919        "det",
2920        "diag",
2921        "diff",
2922        "dir",
2923        "dot",
2924        "disp",
2925        "dlmread",
2926        "dlmwrite",
2927        "eig",
2928        "endsWith",
2929        "erf",
2930        "eval",
2931        "erfc",
2932        "exist",
2933        "exp",
2934        "eye",
2935        "fclose",
2936        "fft",
2937        "fftfreq",
2938        "fftshift",
2939        "fgetl",
2940        "fgets",
2941        "fieldnames",
2942        "ifft",
2943        "ifftshift",
2944        "figure",
2945        "find",
2946        "fliplr",
2947        "flipud",
2948        "floor",
2949        "fopen",
2950        "fprintf",
2951        "genpath",
2952        "histc",
2953        "hour",
2954        "hours",
2955        "hypot",
2956        "imag",
2957        "ind2sub",
2958        "int2str",
2959        "interp1",
2960        "intersect",
2961        "inv",
2962        "iqr",
2963        "iscell",
2964        "ismember",
2965        "ischar",
2966        "isdatetime",
2967        "isduration",
2968        "isempty",
2969        "isfield",
2970        "isfile",
2971        "isfinite",
2972        "isfolder",
2973        "isinf",
2974        "isnan",
2975        "isnat",
2976        "isreal",
2977        "isKey",
2978        "isstring",
2979        "isstruct",
2980        "jsonencode",
2981        "jsondecode",
2982        "keys",
2983        "kron",
2984        "kurtosis",
2985        "lasterr",
2986        "length",
2987        "linspace",
2988        "load",
2989        "log",
2990        "log10",
2991        "log2",
2992        "lower",
2993        "lu",
2994        "mat2str",
2995        "max",
2996        "mean",
2997        "median",
2998        "meshgrid",
2999        "milliseconds",
3000        "min",
3001        "minute",
3002        "minutes",
3003        "mod",
3004        "mode",
3005        "month",
3006        "nan",
3007        "norm",
3008        "normcdf",
3009        "normpdf",
3010        "not",
3011        "null",
3012        "num2str",
3013        "numel",
3014        "ones",
3015        "orth",
3016        "pinv",
3017        "poly",
3018        "polyfit",
3019        "polyval",
3020        "posixtime",
3021        "prctile",
3022        "prod",
3023        "qr",
3024        "rand",
3025        "randi",
3026        "randn",
3027        "rank",
3028        "readmatrix",
3029        "readtable",
3030        "real",
3031        "regexp",
3032        "regexpi",
3033        "regexprep",
3034        "rem",
3035        "repelem",
3036        "remove",
3037        "repmat",
3038        "reshape",
3039        "rmfield",
3040        "rng",
3041        "roots",
3042        "round",
3043        "setdiff",
3044        "second",
3045        "seconds",
3046        "sign",
3047        "sin",
3048        "size",
3049        "skewness",
3050        "sort",
3051        "sprintf",
3052        "sqrt",
3053        "startsWith",
3054        "std",
3055        "str2double",
3056        "str2num",
3057        "strcmp",
3058        "strcmpi",
3059        "strjoin",
3060        "strrep",
3061        "strsplit",
3062        "strtrim",
3063        "sub2ind",
3064        "sum",
3065        "svd",
3066        "tan",
3067        "tic",
3068        "toc",
3069        "trace",
3070        "tril",
3071        "triu",
3072        "union",
3073        "unique",
3074        "upper",
3075        "values",
3076        "var",
3077        "writetable",
3078        "xor",
3079        "year",
3080        "years",
3081        "zeros",
3082        "zscore",
3083    ]
3084}
3085
3086/// Computes the Levenshtein edit distance between two strings.
3087fn levenshtein(a: &str, b: &str) -> usize {
3088    let a: Vec<char> = a.chars().collect();
3089    let b: Vec<char> = b.chars().collect();
3090    let (m, n) = (a.len(), b.len());
3091    let mut row: Vec<usize> = (0..=n).collect();
3092    for i in 1..=m {
3093        let mut prev = row[0];
3094        row[0] = i;
3095        for j in 1..=n {
3096            let next = if a[i - 1] == b[j - 1] {
3097                prev
3098            } else {
3099                1 + prev.min(row[j]).min(row[j - 1])
3100            };
3101            prev = row[j];
3102            row[j] = next;
3103        }
3104    }
3105    row[n]
3106}
3107
3108/// Finds the closest name in `env` keys and built-in names within Levenshtein distance 2.
3109fn suggest_similar(name: &str, env: &Env) -> Option<String> {
3110    const MAX_DIST: usize = 2;
3111    let mut best: Option<(String, usize)> = None;
3112    let mut update = |candidate: &str| {
3113        let d = levenshtein(name, candidate);
3114        if d <= MAX_DIST && best.as_ref().is_none_or(|(_, bd)| d < *bd) {
3115            best = Some((candidate.to_string(), d));
3116        }
3117    };
3118    for key in env.keys() {
3119        update(key);
3120    }
3121    for &bname in builtin_names() {
3122        update(bname);
3123    }
3124    best.map(|(s, _)| s)
3125}
3126
3127/// Checks equality of two values for `assert(a, b[, tol])`.
3128fn assert_values_equal(a: &Value, b: &Value, tol: Option<f64>) -> Result<Value, String> {
3129    match (a, b) {
3130        (Value::Scalar(x), Value::Scalar(y)) => {
3131            let ok = match tol {
3132                None => x == y,
3133                Some(t) => (x - y).abs() <= t,
3134            };
3135            if ok {
3136                Ok(Value::Void)
3137            } else if let Some(t) = tol {
3138                Err(format!(
3139                    "assert: |{x} - {y}| = {} exceeds tolerance {t}",
3140                    (x - y).abs()
3141                ))
3142            } else {
3143                Err(format!("assert: {x} ~= {y}"))
3144            }
3145        }
3146        (Value::Matrix(ma), Value::Matrix(mb)) => {
3147            if ma.shape() != mb.shape() {
3148                return Err(format!(
3149                    "assert: size mismatch [{}×{}] vs [{}×{}]",
3150                    ma.nrows(),
3151                    ma.ncols(),
3152                    mb.nrows(),
3153                    mb.ncols()
3154                ));
3155            }
3156            for (x, y) in ma.iter().zip(mb.iter()) {
3157                let ok = match tol {
3158                    None => x == y,
3159                    Some(t) => (x - y).abs() <= t,
3160                };
3161                if !ok {
3162                    if let Some(t) = tol {
3163                        return Err(format!(
3164                            "assert: difference {} exceeds tolerance {t}",
3165                            (x - y).abs()
3166                        ));
3167                    } else {
3168                        return Err(format!("assert: {x} ~= {y}"));
3169                    }
3170                }
3171            }
3172            Ok(Value::Void)
3173        }
3174        _ => {
3175            if tol.is_some() {
3176                return Err("assert: tolerance requires numeric arguments".to_string());
3177            }
3178            if a == b {
3179                Ok(Value::Void)
3180            } else {
3181                Err("assert: values not equal".to_string())
3182            }
3183        }
3184    }
3185}
3186
3187pub(crate) fn call_builtin(
3188    name: &str,
3189    args: &[Value],
3190    env: &Env,
3191    mut io: Option<&mut IoContext>,
3192) -> Result<Value, String> {
3193    // Plugins are checked first so they can shadow built-ins.
3194    if let Some(result) = crate::plugin::call_plugin(name, args, env) {
3195        return result;
3196    }
3197
3198    match (name, args.len()) {
3199        // --- 1-argument scalar functions ---
3200        ("sqrt", 1) => match &args[0] {
3201            Value::Scalar(x) if *x < 0.0 => Ok(make_complex(0.0, (-x).sqrt())),
3202            Value::Complex(re, im) => {
3203                let mag = (*re * *re + *im * *im).sqrt();
3204                let sqrt_mag = mag.sqrt();
3205                let arg = (*im).atan2(*re) / 2.0;
3206                Ok(make_complex(sqrt_mag * arg.cos(), sqrt_mag * arg.sin()))
3207            }
3208            _ => apply_elem(&args[0], |x| x.sqrt()),
3209        },
3210        ("floor", 1) => apply_elem(&args[0], |x| x.floor()),
3211        ("ceil", 1) => apply_elem(&args[0], |x| x.ceil()),
3212        ("round", 1) => apply_elem(&args[0], |x| x.round()),
3213        ("sign", 1) => apply_elem(&args[0], |x| x.signum()),
3214        ("log", 1) => apply_elem(&args[0], |x| x.ln()),
3215        ("log2", 1) => apply_elem(&args[0], |x| x.log2()),
3216        ("log10", 1) => apply_elem(&args[0], |x| x.log10()),
3217        ("exp", 1) => match &args[0] {
3218            Value::Complex(re, im) => {
3219                let e = re.exp();
3220                Ok(make_complex(e * im.cos(), e * im.sin()))
3221            }
3222            _ => apply_elem(&args[0], |x| x.exp()),
3223        },
3224        ("sin", 1) => apply_elem(&args[0], |x| x.sin()),
3225        ("cos", 1) => apply_elem(&args[0], |x| x.cos()),
3226        ("tan", 1) => apply_elem(&args[0], |x| x.tan()),
3227        ("asin", 1) => apply_elem(&args[0], |x| x.asin()),
3228        ("acos", 1) => apply_elem(&args[0], |x| x.acos()),
3229        ("atan", 1) => apply_elem(&args[0], |x| x.atan()),
3230        // --- Special functions (erf, normal distribution) ---
3231        ("erf", 1) => apply_elem(&args[0], libm::erf),
3232        ("erfc", 1) => apply_elem(&args[0], libm::erfc),
3233        ("normcdf", 1) => apply_elem(&args[0], |x| {
3234            0.5 * (1.0 + libm::erf(x / std::f64::consts::SQRT_2))
3235        }),
3236        ("normcdf", 3) => {
3237            let mu = scalar_arg(&args[1], name, 2)?;
3238            let s = scalar_arg(&args[2], name, 3)?;
3239            if s <= 0.0 {
3240                return Err("normcdf: sigma must be positive".to_string());
3241            }
3242            apply_elem(&args[0], move |x| {
3243                0.5 * (1.0 + libm::erf((x - mu) / (s * std::f64::consts::SQRT_2)))
3244            })
3245        }
3246        ("normpdf", 1) => apply_elem(&args[0], |x| {
3247            (-0.5 * x * x).exp() / (2.0 * std::f64::consts::PI).sqrt()
3248        }),
3249        ("normpdf", 3) => {
3250            let mu = scalar_arg(&args[1], name, 2)?;
3251            let s = scalar_arg(&args[2], name, 3)?;
3252            if s <= 0.0 {
3253                return Err("normpdf: sigma must be positive".to_string());
3254            }
3255            apply_elem(&args[0], move |x| {
3256                let z = (x - mu) / s;
3257                (-0.5 * z * z).exp() / (s * (2.0 * std::f64::consts::PI).sqrt())
3258            })
3259        }
3260        // --- 2-argument scalar functions ---
3261        ("atan2", 2) => Ok(Value::Scalar(
3262            scalar_arg(&args[0], name, 1)?.atan2(scalar_arg(&args[1], name, 2)?),
3263        )),
3264        ("mod", 2) => {
3265            let a = scalar_arg(&args[0], name, 1)?;
3266            let b = scalar_arg(&args[1], name, 2)?;
3267            Ok(Value::Scalar(a - b * (a / b).floor()))
3268        }
3269        ("rem", 2) => {
3270            let a = scalar_arg(&args[0], name, 1)?;
3271            let b = scalar_arg(&args[1], name, 2)?;
3272            Ok(Value::Scalar(a - b * (a / b).trunc()))
3273        }
3274        ("max", 2) => Ok(Value::Scalar(
3275            scalar_arg(&args[0], name, 1)?.max(scalar_arg(&args[1], name, 2)?),
3276        )),
3277        ("min", 2) => Ok(Value::Scalar(
3278            scalar_arg(&args[0], name, 1)?.min(scalar_arg(&args[1], name, 2)?),
3279        )),
3280        ("hypot", 2) => Ok(Value::Scalar(
3281            scalar_arg(&args[0], name, 1)?.hypot(scalar_arg(&args[1], name, 2)?),
3282        )),
3283        ("log", 2) => Ok(Value::Scalar(
3284            scalar_arg(&args[0], name, 1)?.log(scalar_arg(&args[1], name, 2)?),
3285        )),
3286        // --- Matrix constructors ---
3287        ("zeros", 1) => {
3288            let (r, c) = size_arg(&args[0], name)?;
3289            Ok(Value::Matrix(Box::new(Array2::zeros((r, c)))))
3290        }
3291        ("zeros", 2) => {
3292            let r = scalar_arg(&args[0], name, 1)? as usize;
3293            let c = scalar_arg(&args[1], name, 2)? as usize;
3294            Ok(Value::Matrix(Box::new(Array2::zeros((r, c)))))
3295        }
3296        ("ones", 1) => {
3297            let (r, c) = size_arg(&args[0], name)?;
3298            Ok(Value::Matrix(Box::new(Array2::ones((r, c)))))
3299        }
3300        ("ones", 2) => {
3301            let r = scalar_arg(&args[0], name, 1)? as usize;
3302            let c = scalar_arg(&args[1], name, 2)? as usize;
3303            Ok(Value::Matrix(Box::new(Array2::ones((r, c)))))
3304        }
3305        ("eye", 1) => {
3306            let n = scalar_arg(&args[0], name, 1)? as usize;
3307            let mut m = Array2::<f64>::zeros((n, n));
3308            for i in 0..n {
3309                m[[i, i]] = 1.0;
3310            }
3311            Ok(Value::Matrix(Box::new(m)))
3312        }
3313        // --- Matrix properties ---
3314        ("size", 1) => match &args[0] {
3315            Value::Void => Err("size: not applicable to void".to_string()),
3316            Value::Scalar(_) | Value::Complex(_, _) | Value::Struct(_) => Ok(Value::Matrix(
3317                Box::new(Array2::from_shape_vec((1, 2), vec![1.0, 1.0]).unwrap()),
3318            )),
3319            Value::Matrix(m) => Ok(Value::Matrix(Box::new(
3320                Array2::from_shape_vec((1, 2), vec![m.nrows() as f64, m.ncols() as f64]).unwrap(),
3321            ))),
3322            Value::ComplexMatrix(m) => Ok(Value::Matrix(Box::new(
3323                Array2::from_shape_vec((1, 2), vec![m.nrows() as f64, m.ncols() as f64]).unwrap(),
3324            ))),
3325            Value::Str(s) => Ok(Value::Matrix(Box::new(
3326                Array2::from_shape_vec((1, 2), vec![1.0, s.chars().count() as f64]).unwrap(),
3327            ))),
3328            Value::StringObj(s) => Ok(Value::Matrix(Box::new(
3329                Array2::from_shape_vec((1, 2), vec![1.0, s.chars().count() as f64]).unwrap(),
3330            ))),
3331            Value::Cell(v) => Ok(Value::Matrix(Box::new(
3332                Array2::from_shape_vec((1, 2), vec![1.0, v.len() as f64]).unwrap(),
3333            ))),
3334            Value::StructArray(arr) => Ok(Value::Matrix(Box::new(
3335                Array2::from_shape_vec((1, 2), vec![1.0, arr.len() as f64]).unwrap(),
3336            ))),
3337            Value::Lambda(_)
3338            | Value::Function(_)
3339            | Value::Tuple(_)
3340            | Value::DateTime(_)
3341            | Value::Duration(_)
3342            | Value::DateTimeArray(_)
3343            | Value::DurationArray(_)
3344            | Value::Map(_) => Err("size: not applicable to this type".to_string()),
3345        },
3346        ("size", 2) => {
3347            let dim = scalar_arg(&args[1], name, 2)? as usize;
3348            match &args[0] {
3349                Value::Void => Err("size: not applicable to void".to_string()),
3350                Value::Scalar(_) | Value::Complex(_, _) | Value::Struct(_) => {
3351                    Ok(Value::Scalar(1.0))
3352                }
3353                Value::Matrix(m) => match dim {
3354                    1 => Ok(Value::Scalar(m.nrows() as f64)),
3355                    2 => Ok(Value::Scalar(m.ncols() as f64)),
3356                    _ => Err(format!("size: invalid dimension {dim}, must be 1 or 2")),
3357                },
3358                Value::ComplexMatrix(m) => match dim {
3359                    1 => Ok(Value::Scalar(m.nrows() as f64)),
3360                    2 => Ok(Value::Scalar(m.ncols() as f64)),
3361                    _ => Err(format!("size: invalid dimension {dim}, must be 1 or 2")),
3362                },
3363                Value::Str(s) => match dim {
3364                    1 => Ok(Value::Scalar(1.0)),
3365                    2 => Ok(Value::Scalar(s.chars().count() as f64)),
3366                    _ => Err(format!("size: invalid dimension {dim}")),
3367                },
3368                Value::StringObj(s) => match dim {
3369                    1 => Ok(Value::Scalar(1.0)),
3370                    2 => Ok(Value::Scalar(s.chars().count() as f64)),
3371                    _ => Err(format!("size: invalid dimension {dim}")),
3372                },
3373                Value::Cell(v) => match dim {
3374                    1 => Ok(Value::Scalar(1.0)),
3375                    2 => Ok(Value::Scalar(v.len() as f64)),
3376                    _ => Err(format!("size: invalid dimension {dim}")),
3377                },
3378                Value::StructArray(arr) => match dim {
3379                    1 => Ok(Value::Scalar(1.0)),
3380                    2 => Ok(Value::Scalar(arr.len() as f64)),
3381                    _ => Err(format!("size: invalid dimension {dim}")),
3382                },
3383                Value::Lambda(_)
3384                | Value::Function(_)
3385                | Value::Tuple(_)
3386                | Value::DateTime(_)
3387                | Value::Duration(_)
3388                | Value::DateTimeArray(_)
3389                | Value::DurationArray(_)
3390                | Value::Map(_) => Err("size: not applicable to this type".to_string()),
3391            }
3392        }
3393        ("length", 1) => match &args[0] {
3394            Value::Void => Err("length: not applicable to void".to_string()),
3395            Value::Scalar(_) | Value::Complex(_, _) | Value::Struct(_) => Ok(Value::Scalar(1.0)),
3396            Value::Matrix(m) => Ok(Value::Scalar(m.nrows().max(m.ncols()) as f64)),
3397            Value::ComplexMatrix(m) => Ok(Value::Scalar(m.nrows().max(m.ncols()) as f64)),
3398            Value::Str(s) => Ok(Value::Scalar(s.chars().count() as f64)),
3399            Value::StringObj(s) => Ok(Value::Scalar(s.chars().count() as f64)),
3400            Value::Cell(v) => Ok(Value::Scalar(v.len() as f64)),
3401            Value::StructArray(arr) => Ok(Value::Scalar(arr.len() as f64)),
3402            Value::DateTimeArray(v) | Value::DurationArray(v) => Ok(Value::Scalar(v.len() as f64)),
3403            Value::DateTime(_) | Value::Duration(_) => Ok(Value::Scalar(1.0)),
3404            Value::Map(m) => Ok(Value::Scalar(m.len() as f64)),
3405            Value::Lambda(_) | Value::Function(_) | Value::Tuple(_) => {
3406                Err("length: not applicable to function values".to_string())
3407            }
3408        },
3409        ("numel", 1) => match &args[0] {
3410            Value::Void => Err("numel: not applicable to void".to_string()),
3411            Value::Scalar(_) | Value::Complex(_, _) | Value::Struct(_) => Ok(Value::Scalar(1.0)),
3412            Value::Matrix(m) => Ok(Value::Scalar(m.len() as f64)),
3413            Value::ComplexMatrix(m) => Ok(Value::Scalar(m.len() as f64)),
3414            Value::Str(s) => Ok(Value::Scalar(s.chars().count() as f64)),
3415            Value::StringObj(s) => Ok(Value::Scalar(s.chars().count() as f64)),
3416            Value::Cell(v) => Ok(Value::Scalar(v.len() as f64)),
3417            Value::StructArray(arr) => Ok(Value::Scalar(arr.len() as f64)),
3418            Value::DateTimeArray(v) | Value::DurationArray(v) => Ok(Value::Scalar(v.len() as f64)),
3419            Value::DateTime(_) | Value::Duration(_) => Ok(Value::Scalar(1.0)),
3420            Value::Map(m) => Ok(Value::Scalar(m.len() as f64)),
3421            Value::Lambda(_) | Value::Function(_) | Value::Tuple(_) => {
3422                Err("numel: not applicable to function values".to_string())
3423            }
3424        },
3425        ("trace", 1) => match &args[0] {
3426            Value::Void => Err("trace: not applicable to void".to_string()),
3427            Value::Scalar(n) => Ok(Value::Scalar(*n)),
3428            Value::Complex(re, _) => Ok(Value::Scalar(*re)),
3429            Value::Matrix(m) => {
3430                let n = m.nrows().min(m.ncols());
3431                Ok(Value::Scalar((0..n).map(|i| m[[i, i]]).sum()))
3432            }
3433            Value::ComplexMatrix(m) => {
3434                let n = m.nrows().min(m.ncols());
3435                let s: Complex<f64> = (0..n).map(|i| m[[i, i]]).sum();
3436                Ok(if s.im == 0.0 {
3437                    Value::Scalar(s.re)
3438                } else {
3439                    Value::Complex(s.re, s.im)
3440                })
3441            }
3442            Value::Str(_)
3443            | Value::StringObj(_)
3444            | Value::Lambda(_)
3445            | Value::Function(_)
3446            | Value::Tuple(_)
3447            | Value::Cell(_)
3448            | Value::Struct(_)
3449            | Value::StructArray(_)
3450            | Value::DateTime(_)
3451            | Value::Duration(_)
3452            | Value::DateTimeArray(_)
3453            | Value::DurationArray(_)
3454            | Value::Map(_) => Err("trace: not applicable to non-numeric values".to_string()),
3455        },
3456        ("det", 1) => match &args[0] {
3457            Value::Void => Err("det: not applicable to void".to_string()),
3458            Value::Scalar(n) => Ok(Value::Scalar(*n)),
3459            Value::Complex(_, _) => Err("det: not applicable to complex scalars".to_string()),
3460            Value::ComplexMatrix(_) => Err("det: not supported for complex matrices".to_string()),
3461            Value::Matrix(m) => Ok(Value::Scalar(det_matrix(m)?)),
3462            Value::Str(_)
3463            | Value::StringObj(_)
3464            | Value::Lambda(_)
3465            | Value::Function(_)
3466            | Value::Tuple(_)
3467            | Value::Cell(_)
3468            | Value::Struct(_)
3469            | Value::StructArray(_)
3470            | Value::DateTime(_)
3471            | Value::Duration(_)
3472            | Value::DateTimeArray(_)
3473            | Value::DurationArray(_)
3474            | Value::Map(_) => Err("det: not applicable to non-numeric values".to_string()),
3475        },
3476        ("inv", 1) => match &args[0] {
3477            Value::Void => Err("inv: not applicable to void".to_string()),
3478            Value::Scalar(n) => {
3479                if *n == 0.0 {
3480                    Err("inv: singular (zero scalar)".to_string())
3481                } else {
3482                    Ok(Value::Scalar(1.0 / n))
3483                }
3484            }
3485            Value::Complex(re, im) => {
3486                // 1/(a+bi) = (a-bi)/(a²+b²)
3487                let denom = re * re + im * im;
3488                if denom == 0.0 {
3489                    Err("inv: singular (zero complex)".to_string())
3490                } else {
3491                    Ok(make_complex(re / denom, -im / denom))
3492                }
3493            }
3494            Value::Matrix(m) => Ok(Value::Matrix(Box::new(inv_matrix(m)?))),
3495            Value::ComplexMatrix(_) => Err("inv: not supported for complex matrices".to_string()),
3496            Value::Str(_)
3497            | Value::StringObj(_)
3498            | Value::Lambda(_)
3499            | Value::Function(_)
3500            | Value::Tuple(_)
3501            | Value::Cell(_)
3502            | Value::Struct(_)
3503            | Value::StructArray(_)
3504            | Value::DateTime(_)
3505            | Value::Duration(_)
3506            | Value::DateTimeArray(_)
3507            | Value::DurationArray(_)
3508            | Value::Map(_) => Err("inv: not applicable to non-numeric values".to_string()),
3509        },
3510        // --- Range / linspace ---
3511        ("linspace", 3) => {
3512            let a = scalar_arg(&args[0], name, 1)?;
3513            let b = scalar_arg(&args[1], name, 2)?;
3514            let n = scalar_arg(&args[2], name, 3)? as usize;
3515            if n == 0 {
3516                return Ok(Value::Matrix(Box::new(Array2::zeros((1, 0)))));
3517            }
3518            if n == 1 {
3519                return Ok(Value::Matrix(Box::new(
3520                    Array2::from_shape_vec((1, 1), vec![b]).unwrap(),
3521                )));
3522            }
3523            let vals: Vec<f64> = (0..n)
3524                .map(|i| a + (b - a) * i as f64 / (n - 1) as f64)
3525                .collect();
3526            Ok(Value::Matrix(Box::new(
3527                Array2::from_shape_vec((1, n), vals).unwrap(),
3528            )))
3529        }
3530        // --- Bitwise functions ---
3531        // All operands are truncated to i64. Results are non-negative integers
3532        // returned as f64.  For bitnot the bit-width defines the mask.
3533        ("bitand", 2) => {
3534            let a = to_bits(scalar_arg(&args[0], name, 1)?, name, 1)?;
3535            let b = to_bits(scalar_arg(&args[1], name, 2)?, name, 2)?;
3536            Ok(Value::Scalar((a & b) as f64))
3537        }
3538        ("bitor", 2) => {
3539            let a = to_bits(scalar_arg(&args[0], name, 1)?, name, 1)?;
3540            let b = to_bits(scalar_arg(&args[1], name, 2)?, name, 2)?;
3541            Ok(Value::Scalar((a | b) as f64))
3542        }
3543        ("bitxor", 2) => {
3544            let a = to_bits(scalar_arg(&args[0], name, 1)?, name, 1)?;
3545            let b = to_bits(scalar_arg(&args[1], name, 2)?, name, 2)?;
3546            Ok(Value::Scalar((a ^ b) as f64))
3547        }
3548        // bitshift(a, n): n > 0 → left shift; n < 0 → logical right shift.
3549        // Shifts of 64 or more return 0.
3550        ("bitshift", 2) => {
3551            let a = to_bits(scalar_arg(&args[0], name, 1)?, name, 1)?;
3552            let n = scalar_arg(&args[1], name, 2)?;
3553            if n.fract() != 0.0 {
3554                return Err("bitshift: shift amount must be an integer".to_string());
3555            }
3556            let n = n as i64;
3557            let result: u64 = if n >= 64 || n <= -64 {
3558                0
3559            } else if n >= 0 {
3560                a.wrapping_shl(n as u32)
3561            } else {
3562                a.wrapping_shr((-n) as u32)
3563            };
3564            Ok(Value::Scalar(result as f64))
3565        }
3566        // bitnot(a)        — NOT within 32-bit window (Octave uint32 default)
3567        // bitnot(a, bits)  — NOT within explicit bit-width window (1–53)
3568        ("bitnot", 1) => {
3569            let a = to_bits(scalar_arg(&args[0], name, 1)?, name, 1)?;
3570            let mask: u64 = 0xFFFF_FFFF;
3571            Ok(Value::Scalar(((a ^ mask) & mask) as f64))
3572        }
3573        ("bitnot", 2) => {
3574            let a = to_bits(scalar_arg(&args[0], name, 1)?, name, 1)?;
3575            let bits = scalar_arg(&args[1], name, 2)?;
3576            if bits.fract() != 0.0 || !(1.0..=53.0).contains(&bits) {
3577                return Err(format!(
3578                    "bitnot: bit-width must be an integer in [1, 53], got {bits}"
3579                ));
3580            }
3581            let mask: u64 = (1u64 << bits as u32) - 1;
3582            Ok(Value::Scalar(((a ^ mask) & mask) as f64))
3583        }
3584        // --- Special constant predicates (element-wise) ---
3585        ("isnan", 1) => apply_elem(&args[0], |x| if x.is_nan() { 1.0 } else { 0.0 }),
3586        ("isinf", 1) => apply_elem(&args[0], |x| if x.is_infinite() { 1.0 } else { 0.0 }),
3587        ("isfinite", 1) => apply_elem(&args[0], |x| if x.is_finite() { 1.0 } else { 0.0 }),
3588        // --- NaN matrix constructors ---
3589        ("nan", 1) => {
3590            let (r, c) = size_arg(&args[0], name)?;
3591            Ok(Value::Matrix(Box::new(Array2::from_elem((r, c), f64::NAN))))
3592        }
3593        ("nan", 2) => {
3594            let r = scalar_arg(&args[0], name, 1)? as usize;
3595            let c = scalar_arg(&args[1], name, 2)? as usize;
3596            Ok(Value::Matrix(Box::new(Array2::from_elem((r, c), f64::NAN))))
3597        }
3598        // --- Random number generation ---
3599        ("rand", 0) => Ok(Value::Scalar(rand_uniform())),
3600        ("rand", 1) => {
3601            let (r, c) = size_arg(&args[0], name)?;
3602            let data: Vec<f64> = (0..r * c).map(|_| rand_uniform()).collect();
3603            Ok(Value::Matrix(Box::new(
3604                Array2::from_shape_vec((r, c), data).unwrap(),
3605            )))
3606        }
3607        ("rand", 2) => {
3608            let r = scalar_arg(&args[0], name, 1)? as usize;
3609            let c = scalar_arg(&args[1], name, 2)? as usize;
3610            let data: Vec<f64> = (0..r * c).map(|_| rand_uniform()).collect();
3611            Ok(Value::Matrix(Box::new(
3612                Array2::from_shape_vec((r, c), data).unwrap(),
3613            )))
3614        }
3615        ("randn", 0) => Ok(Value::Scalar(rand_normal())),
3616        ("randn", 1) => {
3617            let (r, c) = size_arg(&args[0], name)?;
3618            let data: Vec<f64> = (0..r * c).map(|_| rand_normal()).collect();
3619            Ok(Value::Matrix(Box::new(
3620                Array2::from_shape_vec((r, c), data).unwrap(),
3621            )))
3622        }
3623        ("randn", 2) => {
3624            let r = scalar_arg(&args[0], name, 1)? as usize;
3625            let c = scalar_arg(&args[1], name, 2)? as usize;
3626            let data: Vec<f64> = (0..r * c).map(|_| rand_normal()).collect();
3627            Ok(Value::Matrix(Box::new(
3628                Array2::from_shape_vec((r, c), data).unwrap(),
3629            )))
3630        }
3631        ("randi", 1) => {
3632            let (lo, hi) = randi_range(&args[0])?;
3633            let v = RNG.with(|r| r.borrow_mut().gen_range(lo..=hi)) as f64;
3634            Ok(Value::Scalar(v))
3635        }
3636        ("randi", 2) => {
3637            let (lo, hi) = randi_range(&args[0])?;
3638            let n = scalar_arg(&args[1], name, 2)? as usize;
3639            let data: Vec<f64> = (0..n * n)
3640                .map(|_| RNG.with(|r| r.borrow_mut().gen_range(lo..=hi)) as f64)
3641                .collect();
3642            Ok(Value::Matrix(Box::new(
3643                Array2::from_shape_vec((n, n), data).unwrap(),
3644            )))
3645        }
3646        ("randi", 3) => {
3647            let (lo, hi) = randi_range(&args[0])?;
3648            let r = scalar_arg(&args[1], name, 2)? as usize;
3649            let c = scalar_arg(&args[2], name, 3)? as usize;
3650            let data: Vec<f64> = (0..r * c)
3651                .map(|_| RNG.with(|rng| rng.borrow_mut().gen_range(lo..=hi)) as f64)
3652                .collect();
3653            Ok(Value::Matrix(Box::new(
3654                Array2::from_shape_vec((r, c), data).unwrap(),
3655            )))
3656        }
3657        ("rng", 1) => match &args[0] {
3658            Value::Scalar(n) => {
3659                rng_seed(*n as u64);
3660                Ok(Value::Void)
3661            }
3662            Value::Str(s) | Value::StringObj(s) if s == "shuffle" => {
3663                rng_shuffle();
3664                Ok(Value::Void)
3665            }
3666            _ => Err("rng: argument must be a numeric seed or 'shuffle'".to_string()),
3667        },
3668        // --- Vector reductions ---
3669        // For vectors (1×N or N×1): reduce all elements to scalar.
3670        // For M×N matrices (M>1, N>1): reduce column-wise, return 1×N row vector.
3671        ("sum", 1) => {
3672            if matches!(&args[0], Value::Complex(_, _) | Value::ComplexMatrix(_)) {
3673                apply_cm_reduction(&args[0], |v| v.iter().copied().sum())
3674            } else {
3675                apply_reduction(&args[0], |v| v.iter().copied().sum())
3676            }
3677        }
3678        ("prod", 1) => {
3679            if matches!(&args[0], Value::Complex(_, _) | Value::ComplexMatrix(_)) {
3680                apply_cm_reduction(&args[0], |v| v.iter().copied().product())
3681            } else {
3682                apply_reduction(&args[0], |v| v.iter().copied().product())
3683            }
3684        }
3685        ("any", 1) => apply_reduction(&args[0], |v| {
3686            if v.iter().any(|&x| x != 0.0) {
3687                1.0
3688            } else {
3689                0.0
3690            }
3691        }),
3692        ("all", 1) => apply_reduction(&args[0], |v| {
3693            if v.iter().all(|&x| x != 0.0) {
3694                1.0
3695            } else {
3696                0.0
3697            }
3698        }),
3699        ("mean", 1) => {
3700            if matches!(&args[0], Value::Complex(_, _) | Value::ComplexMatrix(_)) {
3701                apply_cm_reduction(&args[0], |v| {
3702                    if v.is_empty() {
3703                        Complex::new(f64::NAN, 0.0)
3704                    } else {
3705                        v.iter().copied().sum::<Complex<f64>>() / v.len() as f64
3706                    }
3707                })
3708            } else {
3709                apply_reduction(&args[0], |v| {
3710                    if v.is_empty() {
3711                        f64::NAN
3712                    } else {
3713                        v.iter().copied().sum::<f64>() / v.len() as f64
3714                    }
3715                })
3716            }
3717        }
3718        // 1-arg min/max: reduce to scalar for vectors, column-wise for matrices.
3719        // 2-arg forms (element-wise scalar min/max) are already handled above.
3720        ("min", 1) => apply_reduction(&args[0], |v| {
3721            v.iter().copied().fold(f64::INFINITY, f64::min)
3722        }),
3723        ("max", 1) => apply_reduction(&args[0], |v| {
3724            v.iter().copied().fold(f64::NEG_INFINITY, f64::max)
3725        }),
3726        // --- Norms ---
3727        ("norm", 1) => match &args[0] {
3728            Value::Void => Err("norm: not applicable to void".to_string()),
3729            Value::Scalar(n) => Ok(Value::Scalar(n.abs())),
3730            Value::Complex(re, im) => Ok(Value::Scalar((re * re + im * im).sqrt())),
3731            Value::Matrix(m) => {
3732                if m.nrows() <= 1 || m.ncols() <= 1 {
3733                    // Vector: L2 norm.
3734                    Ok(Value::Scalar(m.iter().map(|x| x * x).sum::<f64>().sqrt()))
3735                } else {
3736                    // Matrix: 2-norm = largest singular value.
3737                    let (_, s, _) = svd_compute(m)?;
3738                    Ok(Value::Scalar(s.first().copied().unwrap_or(0.0)))
3739                }
3740            }
3741            Value::ComplexMatrix(m) => Ok(Value::Scalar(
3742                m.iter().map(|c| c.norm_sqr()).sum::<f64>().sqrt(),
3743            )),
3744            Value::Str(_)
3745            | Value::StringObj(_)
3746            | Value::Lambda(_)
3747            | Value::Function(_)
3748            | Value::Tuple(_)
3749            | Value::Cell(_)
3750            | Value::Struct(_)
3751            | Value::StructArray(_)
3752            | Value::DateTime(_)
3753            | Value::Duration(_)
3754            | Value::DateTimeArray(_)
3755            | Value::DurationArray(_)
3756            | Value::Map(_) => Err("norm: not applicable to non-numeric values".to_string()),
3757        },
3758        ("norm", 2) => match &args[1] {
3759            Value::Str(s) | Value::StringObj(s) => match s.as_str() {
3760                "fro" => match &args[0] {
3761                    Value::Scalar(n) => Ok(Value::Scalar(n.abs())),
3762                    Value::Matrix(m) => {
3763                        Ok(Value::Scalar(m.iter().map(|x| x * x).sum::<f64>().sqrt()))
3764                    }
3765                    _ => Err("norm: first argument must be numeric".to_string()),
3766                },
3767                other => Err(format!("norm: unknown norm type '{other}'")),
3768            },
3769            _ => {
3770                let p = scalar_arg(&args[1], name, 2)?;
3771                match &args[0] {
3772                    Value::Void => Err("norm: not applicable to void".to_string()),
3773                    Value::Scalar(n) => Ok(Value::Scalar(n.abs())),
3774                    Value::Complex(re, im) => Ok(Value::Scalar((re * re + im * im).sqrt().powf(p))),
3775                    Value::Matrix(m) => {
3776                        if m.nrows() > 1 && m.ncols() > 1 {
3777                            // Matrix norms.
3778                            if (p - 2.0).abs() < 1e-15 {
3779                                let (_, s, _) = svd_compute(m)?;
3780                                return Ok(Value::Scalar(s.first().copied().unwrap_or(0.0)));
3781                            } else if (p - 1.0).abs() < 1e-15 {
3782                                // Maximum absolute column sum.
3783                                let v = (0..m.ncols())
3784                                    .map(|j| m.column(j).iter().map(|&x| x.abs()).sum::<f64>())
3785                                    .fold(0.0_f64, f64::max);
3786                                return Ok(Value::Scalar(v));
3787                            } else if p == f64::INFINITY {
3788                                // Maximum absolute row sum.
3789                                let v = (0..m.nrows())
3790                                    .map(|i| m.row(i).iter().map(|&x| x.abs()).sum::<f64>())
3791                                    .fold(0.0_f64, f64::max);
3792                                return Ok(Value::Scalar(v));
3793                            }
3794                        }
3795                        // Vector (or general Lp).
3796                        if p == f64::INFINITY {
3797                            Ok(Value::Scalar(
3798                                m.iter().copied().fold(0.0_f64, |acc, x| acc.max(x.abs())),
3799                            ))
3800                        } else {
3801                            Ok(Value::Scalar(
3802                                m.iter().map(|x| x.abs().powf(p)).sum::<f64>().powf(1.0 / p),
3803                            ))
3804                        }
3805                    }
3806                    Value::ComplexMatrix(m) => Ok(Value::Scalar(
3807                        m.iter().map(|c| c.norm_sqr()).sum::<f64>().sqrt().powf(p),
3808                    )),
3809                    Value::Str(_)
3810                    | Value::StringObj(_)
3811                    | Value::Lambda(_)
3812                    | Value::Function(_)
3813                    | Value::Tuple(_)
3814                    | Value::Cell(_)
3815                    | Value::Struct(_)
3816                    | Value::StructArray(_)
3817                    | Value::DateTime(_)
3818                    | Value::Duration(_)
3819                    | Value::DateTimeArray(_)
3820                    | Value::DurationArray(_)
3821                    | Value::Map(_) => {
3822                        Err("norm: not applicable to non-numeric values".to_string())
3823                    }
3824                }
3825            }
3826        },
3827        // --- Cumulative reductions ---
3828        ("cumsum", 1) => apply_cumulative(&args[0], |acc, x| acc + x),
3829        ("cumprod", 1) => apply_cumulative(&args[0], |acc, x| acc * x),
3830        // --- Sort ---
3831        ("sort", 1) => match &args[0] {
3832            Value::Void => Err("sort: not applicable to void".to_string()),
3833            Value::Scalar(n) => Ok(Value::Scalar(*n)),
3834            Value::Complex(_, _) => Err("sort: not applicable to complex values".to_string()),
3835            Value::ComplexMatrix(_) => Err("sort: not applicable to complex values".to_string()),
3836            Value::Str(_)
3837            | Value::StringObj(_)
3838            | Value::Lambda(_)
3839            | Value::Function(_)
3840            | Value::Tuple(_)
3841            | Value::Cell(_)
3842            | Value::Struct(_)
3843            | Value::StructArray(_)
3844            | Value::DateTime(_)
3845            | Value::Duration(_)
3846            | Value::DateTimeArray(_)
3847            | Value::DurationArray(_)
3848            | Value::Map(_) => Err("sort: not applicable to non-numeric values".to_string()),
3849            Value::Matrix(m) => {
3850                if m.nrows() > 1 && m.ncols() > 1 {
3851                    return Err("sort: input must be a vector".to_string());
3852                }
3853                let mut vals: Vec<f64> = m.iter().copied().collect();
3854                vals.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
3855                Ok(Value::Matrix(Box::new(
3856                    Array2::from_shape_vec(m.raw_dim(), vals).unwrap(),
3857                )))
3858            }
3859        },
3860        // --- Reshape ---
3861        ("reshape", 3) => {
3862            let r = scalar_arg(&args[1], name, 2)? as usize;
3863            let c = scalar_arg(&args[2], name, 3)? as usize;
3864            match &args[0] {
3865                Value::Void => Err("reshape: not applicable to void".to_string()),
3866                Value::Scalar(n) => {
3867                    if r * c != 1 {
3868                        return Err(format!("reshape: cannot reshape 1 element into {r}x{c}"));
3869                    }
3870                    Ok(Value::Matrix(Box::new(
3871                        Array2::from_shape_vec((1, 1), vec![*n]).unwrap(),
3872                    )))
3873                }
3874                Value::Complex(_, _) => {
3875                    Err("reshape: not applicable to complex values".to_string())
3876                }
3877                Value::ComplexMatrix(_) => {
3878                    Err("reshape: not supported for complex matrices".to_string())
3879                }
3880                Value::Str(_)
3881                | Value::StringObj(_)
3882                | Value::Lambda(_)
3883                | Value::Function(_)
3884                | Value::Tuple(_)
3885                | Value::Cell(_)
3886                | Value::Struct(_)
3887                | Value::StructArray(_)
3888                | Value::DateTime(_)
3889                | Value::Duration(_)
3890                | Value::DateTimeArray(_)
3891                | Value::DurationArray(_)
3892                | Value::Map(_) => Err("reshape: not applicable to non-numeric values".to_string()),
3893                Value::Matrix(m) => {
3894                    let total = m.len();
3895                    if r * c != total {
3896                        return Err(format!(
3897                            "reshape: cannot reshape {total} elements into {r}x{c}"
3898                        ));
3899                    }
3900                    // Column-major order (MATLAB convention)
3901                    let flat: Vec<f64> = (0..m.ncols())
3902                        .flat_map(|col| (0..m.nrows()).map(move |row| m[[row, col]]))
3903                        .collect();
3904                    let mut result = Array2::<f64>::zeros((r, c));
3905                    for (i, &v) in flat.iter().enumerate() {
3906                        result[[i % r, i / r]] = v;
3907                    }
3908                    Ok(Value::Matrix(Box::new(result)))
3909                }
3910            }
3911        }
3912        // --- Flip ---
3913        ("fliplr", 1) => match &args[0] {
3914            Value::Void => Err(format!("{name}: not applicable to void")),
3915            Value::Scalar(n) => Ok(Value::Scalar(*n)),
3916            Value::Complex(re, im) => Ok(Value::Complex(*re, *im)),
3917            Value::ComplexMatrix(_) => Err(format!("{name}: not supported for complex matrices")),
3918            Value::Str(_)
3919            | Value::StringObj(_)
3920            | Value::Lambda(_)
3921            | Value::Function(_)
3922            | Value::Tuple(_)
3923            | Value::Cell(_)
3924            | Value::Struct(_)
3925            | Value::StructArray(_)
3926            | Value::DateTime(_)
3927            | Value::Duration(_)
3928            | Value::DateTimeArray(_)
3929            | Value::DurationArray(_)
3930            | Value::Map(_) => Err(format!("{name}: not applicable to non-numeric values")),
3931            Value::Matrix(m) => {
3932                let (nrows, ncols) = (m.nrows(), m.ncols());
3933                let mut result = m.clone();
3934                for r in 0..nrows {
3935                    for c in 0..ncols / 2 {
3936                        let tmp = result[[r, c]];
3937                        result[[r, c]] = result[[r, ncols - 1 - c]];
3938                        result[[r, ncols - 1 - c]] = tmp;
3939                    }
3940                }
3941                Ok(Value::Matrix(result))
3942            }
3943        },
3944        ("flipud", 1) => match &args[0] {
3945            Value::Void => Err(format!("{name}: not applicable to void")),
3946            Value::Scalar(n) => Ok(Value::Scalar(*n)),
3947            Value::Complex(re, im) => Ok(Value::Complex(*re, *im)),
3948            Value::ComplexMatrix(_) => Err(format!("{name}: not supported for complex matrices")),
3949            Value::Str(_)
3950            | Value::StringObj(_)
3951            | Value::Lambda(_)
3952            | Value::Function(_)
3953            | Value::Tuple(_)
3954            | Value::Cell(_)
3955            | Value::Struct(_)
3956            | Value::StructArray(_)
3957            | Value::DateTime(_)
3958            | Value::Duration(_)
3959            | Value::DateTimeArray(_)
3960            | Value::DurationArray(_)
3961            | Value::Map(_) => Err(format!("{name}: not applicable to non-numeric values")),
3962            Value::Matrix(m) => {
3963                let (nrows, ncols) = (m.nrows(), m.ncols());
3964                let mut result = m.clone();
3965                for c in 0..ncols {
3966                    for r in 0..nrows / 2 {
3967                        let tmp = result[[r, c]];
3968                        result[[r, c]] = result[[nrows - 1 - r, c]];
3969                        result[[nrows - 1 - r, c]] = tmp;
3970                    }
3971                }
3972                Ok(Value::Matrix(result))
3973            }
3974        },
3975        // --- Find ---
3976        ("find", 1) => find_nonzero(&args[0], usize::MAX),
3977        ("find", 2) => {
3978            let k = scalar_arg(&args[1], name, 2)?;
3979            if k < 0.0 {
3980                return Err("find: k must be non-negative".to_string());
3981            }
3982            find_nonzero(&args[0], k as usize)
3983        }
3984        // --- Unique ---
3985        ("unique", 1) => match &args[0] {
3986            Value::Void => Err("unique: not applicable to void".to_string()),
3987            Value::Scalar(n) => Ok(Value::Scalar(*n)),
3988            Value::Matrix(m) => {
3989                let mut vals: Vec<f64> = m.iter().copied().collect();
3990                vals.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
3991                let mut unique: Vec<f64> = Vec::new();
3992                for v in vals {
3993                    if unique.last().is_none_or(|&last| last != v) {
3994                        unique.push(v);
3995                    }
3996                }
3997                let n = unique.len();
3998                Ok(Value::Matrix(Box::new(
3999                    Array2::from_shape_vec((1, n), unique).unwrap(),
4000                )))
4001            }
4002            Value::Complex(_, _) => Err("unique: not applicable to complex values".to_string()),
4003            Value::ComplexMatrix(_) => Err("unique: not applicable to complex values".to_string()),
4004            Value::Str(_)
4005            | Value::StringObj(_)
4006            | Value::Lambda(_)
4007            | Value::Function(_)
4008            | Value::Tuple(_)
4009            | Value::Cell(_)
4010            | Value::Struct(_)
4011            | Value::StructArray(_)
4012            | Value::DateTime(_)
4013            | Value::Duration(_)
4014            | Value::DateTimeArray(_)
4015            | Value::DurationArray(_)
4016            | Value::Map(_) => Err("unique: not applicable to non-numeric values".to_string()),
4017        },
4018        // --- Descriptive statistics ---
4019        ("std", 1) => apply_stat(&args[0], |s| stat_var_vec(s, false).sqrt(), "std"),
4020        ("std", 2) => {
4021            let w = scalar_arg(&args[1], name, 2)?;
4022            let population = w != 0.0;
4023            apply_stat(&args[0], |s| stat_var_vec(s, population).sqrt(), "std")
4024        }
4025        ("var", 1) => apply_stat(&args[0], |s| stat_var_vec(s, false), "var"),
4026        ("var", 2) => {
4027            let w = scalar_arg(&args[1], name, 2)?;
4028            let population = w != 0.0;
4029            apply_stat(&args[0], |s| stat_var_vec(s, population), "var")
4030        }
4031        ("cov", 1) => match &args[0] {
4032            Value::Scalar(_) => Ok(Value::Scalar(0.0)),
4033            Value::Matrix(m) => {
4034                if m.nrows() == 1 || m.ncols() == 1 {
4035                    let vals: Vec<f64> = m.iter().copied().collect();
4036                    Ok(Value::Scalar(stat_var_vec(&vals, false)))
4037                } else {
4038                    let (nobs, nvars) = (m.nrows(), m.ncols());
4039                    if nobs < 2 {
4040                        return Err("cov: need at least 2 observations".to_string());
4041                    }
4042                    let mut centered = m.clone();
4043                    for c in 0..nvars {
4044                        let col_mean: f64 = m.column(c).iter().sum::<f64>() / nobs as f64;
4045                        for r in 0..nobs {
4046                            centered[[r, c]] -= col_mean;
4047                        }
4048                    }
4049                    let denom = (nobs - 1) as f64;
4050                    let mut cov_mat = Array2::<f64>::zeros((nvars, nvars));
4051                    for i in 0..nvars {
4052                        for j in 0..nvars {
4053                            let dot: f64 =
4054                                (0..nobs).map(|r| centered[[r, i]] * centered[[r, j]]).sum();
4055                            cov_mat[[i, j]] = dot / denom;
4056                        }
4057                    }
4058                    Ok(Value::Matrix(Box::new(cov_mat)))
4059                }
4060            }
4061            _ => Err("cov: argument must be numeric".to_string()),
4062        },
4063        ("median", 1) => apply_stat(
4064            &args[0],
4065            |s| {
4066                if s.is_empty() {
4067                    return f64::NAN;
4068                }
4069                let mut v = s.to_vec();
4070                v.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
4071                let n = v.len();
4072                if n % 2 == 0 {
4073                    (v[n / 2 - 1] + v[n / 2]) / 2.0
4074                } else {
4075                    v[n / 2]
4076                }
4077            },
4078            "median",
4079        ),
4080        ("mode", 1) => apply_stat(
4081            &args[0],
4082            |s| {
4083                if s.is_empty() {
4084                    return f64::NAN;
4085                }
4086                let mut counts: std::collections::HashMap<u64, usize> =
4087                    std::collections::HashMap::new();
4088                for &x in s {
4089                    *counts.entry(x.to_bits()).or_insert(0) += 1;
4090                }
4091                let max_count = counts.values().copied().max().unwrap_or(0);
4092                let mut candidates: Vec<f64> = counts
4093                    .iter()
4094                    .filter(|&(_, &c)| c == max_count)
4095                    .map(|(&bits, _)| f64::from_bits(bits))
4096                    .collect();
4097                candidates.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
4098                candidates[0]
4099            },
4100            "mode",
4101        ),
4102        ("skewness", 1) => apply_stat(
4103            &args[0],
4104            |s| {
4105                let n = s.len();
4106                if n == 0 {
4107                    return f64::NAN;
4108                }
4109                if n == 1 {
4110                    return 0.0;
4111                }
4112                let mean = s.iter().sum::<f64>() / n as f64;
4113                let m2 = s.iter().map(|&x| (x - mean).powi(2)).sum::<f64>() / n as f64;
4114                if m2 == 0.0 {
4115                    return f64::NAN;
4116                }
4117                let m3 = s.iter().map(|&x| (x - mean).powi(3)).sum::<f64>() / n as f64;
4118                m3 / m2.powf(1.5)
4119            },
4120            "skewness",
4121        ),
4122        ("kurtosis", 1) => apply_stat(
4123            &args[0],
4124            |s| {
4125                let n = s.len();
4126                if n < 2 {
4127                    return f64::NAN;
4128                }
4129                let mean = s.iter().sum::<f64>() / n as f64;
4130                let m2 = s.iter().map(|&x| (x - mean).powi(2)).sum::<f64>() / n as f64;
4131                if m2 == 0.0 {
4132                    return f64::NAN;
4133                }
4134                let m4 = s.iter().map(|&x| (x - mean).powi(4)).sum::<f64>() / n as f64;
4135                m4 / m2.powi(2)
4136            },
4137            "kurtosis",
4138        ),
4139        ("histc", 2) => {
4140            let vals = numeric_vec(&args[0], name)?;
4141            let edges = numeric_vec(&args[1], name)?;
4142            if edges.is_empty() {
4143                return Err("histc: edges must not be empty".to_string());
4144            }
4145            let n_edges = edges.len();
4146            let mut counts = vec![0.0f64; n_edges];
4147            for &v in &vals {
4148                // Linear scan — fine for typical edge counts
4149                let last = n_edges - 1;
4150                if v == edges[last] {
4151                    counts[last] += 1.0;
4152                } else {
4153                    for i in 0..last {
4154                        if v >= edges[i] && v < edges[i + 1] {
4155                            counts[i] += 1.0;
4156                            break;
4157                        }
4158                    }
4159                }
4160            }
4161            Ok(Value::Matrix(Box::new(
4162                Array2::from_shape_vec((1, n_edges), counts).unwrap(),
4163            )))
4164        }
4165        // --- Percentiles and distributions ---
4166        ("prctile", 2) => {
4167            let p_vals = numeric_vec(&args[1], name)?;
4168            let n_p = p_vals.len();
4169
4170            // Sort one column of floats and compute all requested percentiles.
4171            let compute_col = |vals: &[f64]| -> Vec<f64> {
4172                let mut s = vals.to_vec();
4173                s.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
4174                p_vals.iter().map(|&p| percentile_sorted(&s, p)).collect()
4175            };
4176
4177            match &args[0] {
4178                Value::Scalar(n) => {
4179                    let pr = compute_col(&[*n]);
4180                    if n_p == 1 {
4181                        Ok(Value::Scalar(pr[0]))
4182                    } else {
4183                        Ok(Value::Matrix(Box::new(
4184                            Array2::from_shape_vec((1, n_p), pr).unwrap(),
4185                        )))
4186                    }
4187                }
4188                Value::Matrix(m) if m.nrows() == 1 || m.ncols() == 1 => {
4189                    let vals: Vec<f64> = m.iter().copied().collect();
4190                    let pr = compute_col(&vals);
4191                    if n_p == 1 {
4192                        Ok(Value::Scalar(pr[0]))
4193                    } else {
4194                        Ok(Value::Matrix(Box::new(
4195                            Array2::from_shape_vec((1, n_p), pr).unwrap(),
4196                        )))
4197                    }
4198                }
4199                Value::Matrix(m) => {
4200                    // M×N matrix: column-wise → n_p × ncols result
4201                    let ncols = m.ncols();
4202                    let mut result = Array2::<f64>::zeros((n_p, ncols));
4203                    for j in 0..ncols {
4204                        let col: Vec<f64> = m.column(j).iter().copied().collect();
4205                        let pr = compute_col(&col);
4206                        for (i, &v) in pr.iter().enumerate() {
4207                            result[[i, j]] = v;
4208                        }
4209                    }
4210                    if n_p == 1 {
4211                        let row: Vec<f64> = result.row(0).iter().copied().collect();
4212                        Ok(Value::Matrix(Box::new(
4213                            Array2::from_shape_vec((1, ncols), row).unwrap(),
4214                        )))
4215                    } else {
4216                        Ok(Value::Matrix(Box::new(result)))
4217                    }
4218                }
4219                _ => Err("prctile: first argument must be numeric".to_string()),
4220            }
4221        }
4222        ("iqr", 1) => apply_stat(
4223            &args[0],
4224            |s| {
4225                let mut sorted = s.to_vec();
4226                sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
4227                percentile_sorted(&sorted, 75.0) - percentile_sorted(&sorted, 25.0)
4228            },
4229            "iqr",
4230        ),
4231        ("zscore", 1) => match &args[0] {
4232            Value::Scalar(_) => Ok(Value::Scalar(0.0)),
4233            Value::Matrix(m) => {
4234                if m.nrows() == 1 || m.ncols() == 1 {
4235                    let vals: Vec<f64> = m.iter().copied().collect();
4236                    let n = vals.len() as f64;
4237                    let mean = vals.iter().sum::<f64>() / n;
4238                    let s = stat_var_vec(&vals, false).sqrt();
4239                    let result: Vec<f64> = vals
4240                        .iter()
4241                        .map(|&x| if s == 0.0 { 0.0 } else { (x - mean) / s })
4242                        .collect();
4243                    Ok(Value::Matrix(Box::new(
4244                        Array2::from_shape_vec(m.raw_dim(), result).unwrap(),
4245                    )))
4246                } else {
4247                    let (nrows, ncols) = (m.nrows(), m.ncols());
4248                    let mut result = m.clone();
4249                    for j in 0..ncols {
4250                        let col: Vec<f64> = m.column(j).iter().copied().collect();
4251                        let mean = col.iter().sum::<f64>() / col.len() as f64;
4252                        let s = stat_var_vec(&col, false).sqrt();
4253                        for i in 0..nrows {
4254                            result[[i, j]] = if s == 0.0 {
4255                                0.0
4256                            } else {
4257                                (m[[i, j]] - mean) / s
4258                            };
4259                        }
4260                    }
4261                    Ok(Value::Matrix(result))
4262                }
4263            }
4264            _ => Err("zscore: argument must be numeric".to_string()),
4265        },
4266        // diag(v) — vector → diagonal matrix; diag(A) → column vector of main diagonal.
4267        ("diag", 1) => match &args[0] {
4268            Value::Scalar(n) => Ok(Value::Matrix(Box::new(Array2::from_elem((1, 1), *n)))),
4269            Value::Matrix(m) => {
4270                let (rows, cols) = (m.nrows(), m.ncols());
4271                if rows == 1 || cols == 1 {
4272                    // vector → N×N diagonal matrix
4273                    let v: Vec<f64> = m.iter().copied().collect();
4274                    let n = v.len();
4275                    let mut result = Array2::<f64>::zeros((n, n));
4276                    for (i, &val) in v.iter().enumerate() {
4277                        result[[i, i]] = val;
4278                    }
4279                    Ok(Value::Matrix(Box::new(result)))
4280                } else {
4281                    // matrix → extract main diagonal as N×1 column vector
4282                    let n = rows.min(cols);
4283                    let d: Vec<f64> = (0..n).map(|i| m[[i, i]]).collect();
4284                    Ok(Value::Matrix(Box::new(
4285                        Array2::from_shape_vec((n, 1), d).unwrap(),
4286                    )))
4287                }
4288            }
4289            Value::Void => Err("diag: not applicable to void".to_string()),
4290            Value::Complex(re, im) => {
4291                let mut result = Array2::<Complex<f64>>::zeros((1, 1));
4292                result[[0, 0]] = Complex::new(*re, *im);
4293                Ok(Value::ComplexMatrix(Box::new(result)))
4294            }
4295            Value::ComplexMatrix(m) => {
4296                let (rows, cols) = (m.nrows(), m.ncols());
4297                if rows == 1 || cols == 1 {
4298                    let v: Vec<Complex<f64>> = m.iter().copied().collect();
4299                    let n = v.len();
4300                    let mut result = Array2::<Complex<f64>>::zeros((n, n));
4301                    for (i, &val) in v.iter().enumerate() {
4302                        result[[i, i]] = val;
4303                    }
4304                    Ok(Value::ComplexMatrix(Box::new(result)))
4305                } else {
4306                    let n = rows.min(cols);
4307                    let d: Vec<Complex<f64>> = (0..n).map(|i| m[[i, i]]).collect();
4308                    Ok(Value::ComplexMatrix(Box::new(
4309                        Array2::from_shape_vec((n, 1), d).unwrap(),
4310                    )))
4311                }
4312            }
4313            Value::Str(_)
4314            | Value::StringObj(_)
4315            | Value::Lambda(_)
4316            | Value::Function(_)
4317            | Value::Tuple(_)
4318            | Value::Cell(_)
4319            | Value::Struct(_)
4320            | Value::StructArray(_)
4321            | Value::DateTime(_)
4322            | Value::Duration(_)
4323            | Value::DateTimeArray(_)
4324            | Value::DurationArray(_)
4325            | Value::Map(_) => Err("diag: not applicable to non-numeric values".to_string()),
4326        },
4327
4328        // --- Complex built-ins ---
4329        // real(z) — real part; works on scalars, matrices, and complex matrices.
4330        ("real", 1) => match &args[0] {
4331            Value::Void => Err("real: not applicable to void".to_string()),
4332            Value::Scalar(n) => Ok(Value::Scalar(*n)),
4333            Value::Complex(re, _) => Ok(Value::Scalar(*re)),
4334            Value::Matrix(m) => Ok(Value::Matrix(m.clone())),
4335            Value::ComplexMatrix(m) => Ok(Value::Matrix(Box::new(m.mapv(|c| c.re)))),
4336            Value::Str(_)
4337            | Value::StringObj(_)
4338            | Value::Lambda(_)
4339            | Value::Function(_)
4340            | Value::Tuple(_)
4341            | Value::Cell(_)
4342            | Value::Struct(_)
4343            | Value::StructArray(_)
4344            | Value::DateTime(_)
4345            | Value::Duration(_)
4346            | Value::DateTimeArray(_)
4347            | Value::DurationArray(_)
4348            | Value::Map(_) => Err("real: not applicable to non-numeric values".to_string()),
4349        },
4350        // imag(z) — imaginary part; returns 0.0 for real scalars and real matrices.
4351        ("imag", 1) => match &args[0] {
4352            Value::Void => Err("imag: not applicable to void".to_string()),
4353            Value::Scalar(_) => Ok(Value::Scalar(0.0)),
4354            Value::Complex(_, im) => Ok(Value::Scalar(*im)),
4355            Value::Matrix(m) => Ok(Value::Matrix(Box::new(Array2::zeros(m.raw_dim())))),
4356            Value::ComplexMatrix(m) => Ok(Value::Matrix(Box::new(m.mapv(|c| c.im)))),
4357            Value::Str(_)
4358            | Value::StringObj(_)
4359            | Value::Lambda(_)
4360            | Value::Function(_)
4361            | Value::Tuple(_)
4362            | Value::Cell(_)
4363            | Value::Struct(_)
4364            | Value::StructArray(_)
4365            | Value::DateTime(_)
4366            | Value::Duration(_)
4367            | Value::DateTimeArray(_)
4368            | Value::DurationArray(_)
4369            | Value::Map(_) => Err("imag: not applicable to non-numeric values".to_string()),
4370        },
4371        // abs(z) — modulus; overloads scalar abs; element-wise on matrices.
4372        ("abs", 1) => match &args[0] {
4373            Value::Void => Err("abs: not applicable to void".to_string()),
4374            Value::Scalar(n) => Ok(Value::Scalar(n.abs())),
4375            Value::Complex(re, im) => Ok(Value::Scalar((re * re + im * im).sqrt())),
4376            Value::Matrix(m) => Ok(Value::Matrix(Box::new(m.mapv(|x| x.abs())))),
4377            Value::ComplexMatrix(m) => Ok(Value::Matrix(Box::new(m.mapv(|c| c.norm())))),
4378            Value::Str(_)
4379            | Value::StringObj(_)
4380            | Value::Lambda(_)
4381            | Value::Function(_)
4382            | Value::Tuple(_)
4383            | Value::Cell(_)
4384            | Value::Struct(_)
4385            | Value::StructArray(_)
4386            | Value::DateTime(_)
4387            | Value::Duration(_)
4388            | Value::DateTimeArray(_)
4389            | Value::DurationArray(_)
4390            | Value::Map(_) => Err("abs: not applicable to non-numeric values".to_string()),
4391        },
4392        // angle(z) — argument in radians; returns 0 for non-negative reals.
4393        ("angle", 1) => match &args[0] {
4394            Value::Void => Err("angle: not applicable to void".to_string()),
4395            Value::Scalar(n) => Ok(Value::Scalar(if *n >= 0.0 {
4396                0.0
4397            } else {
4398                std::f64::consts::PI
4399            })),
4400            Value::Complex(re, im) => Ok(Value::Scalar(im.atan2(*re))),
4401            Value::Matrix(m) => {
4402                Ok(Value::Matrix(Box::new(m.mapv(|x| {
4403                    if x >= 0.0 { 0.0 } else { std::f64::consts::PI }
4404                }))))
4405            }
4406            Value::ComplexMatrix(m) => Ok(Value::Matrix(Box::new(m.mapv(|c| c.im.atan2(c.re))))),
4407            Value::Str(_)
4408            | Value::StringObj(_)
4409            | Value::Lambda(_)
4410            | Value::Function(_)
4411            | Value::Tuple(_)
4412            | Value::Cell(_)
4413            | Value::Struct(_)
4414            | Value::StructArray(_)
4415            | Value::DateTime(_)
4416            | Value::Duration(_)
4417            | Value::DateTimeArray(_)
4418            | Value::DurationArray(_)
4419            | Value::Map(_) => Err("angle: not applicable to non-numeric values".to_string()),
4420        },
4421        // conj(z) — complex conjugate; element-wise over complex matrices.
4422        ("conj", 1) => match &args[0] {
4423            Value::Void => Err("conj: not applicable to void".to_string()),
4424            Value::Scalar(n) => Ok(Value::Scalar(*n)),
4425            Value::Complex(re, im) => Ok(make_complex(*re, -*im)),
4426            Value::Matrix(m) => Ok(Value::Matrix(m.clone())),
4427            Value::ComplexMatrix(m) => Ok(Value::ComplexMatrix(Box::new(m.mapv(|c| c.conj())))),
4428            Value::Str(_)
4429            | Value::StringObj(_)
4430            | Value::Lambda(_)
4431            | Value::Function(_)
4432            | Value::Tuple(_)
4433            | Value::Cell(_)
4434            | Value::Struct(_)
4435            | Value::StructArray(_)
4436            | Value::DateTime(_)
4437            | Value::Duration(_)
4438            | Value::DateTimeArray(_)
4439            | Value::DurationArray(_)
4440            | Value::Map(_) => Err("conj: not applicable to non-numeric values".to_string()),
4441        },
4442        // complex(re, im) — construct complex from two reals.
4443        ("complex", 2) => {
4444            let re = scalar_arg(&args[0], name, 1)?;
4445            let im = scalar_arg(&args[1], name, 2)?;
4446            Ok(make_complex(re, im))
4447        }
4448        // isreal(z) — 1.0 if imaginary part is zero, 0.0 otherwise.
4449        ("isreal", 1) => match &args[0] {
4450            Value::Void => Ok(Value::Scalar(0.0)),
4451            Value::Scalar(_) => Ok(Value::Scalar(1.0)),
4452            Value::Complex(_, im) => Ok(Value::Scalar(if *im == 0.0 { 1.0 } else { 0.0 })),
4453            Value::Matrix(_) => Ok(Value::Scalar(1.0)),
4454            Value::ComplexMatrix(_) => Ok(Value::Scalar(0.0)),
4455            // Strings are not real numbers; functions are not numbers
4456            Value::Str(_) | Value::StringObj(_) => Ok(Value::Scalar(0.0)),
4457            Value::Lambda(_)
4458            | Value::Function(_)
4459            | Value::Tuple(_)
4460            | Value::Cell(_)
4461            | Value::Struct(_)
4462            | Value::StructArray(_)
4463            | Value::DateTime(_)
4464            | Value::Duration(_)
4465            | Value::DateTimeArray(_)
4466            | Value::DurationArray(_)
4467            | Value::Map(_) => Ok(Value::Scalar(0.0)),
4468        },
4469        // --- String built-ins ---
4470        // num2str(x) — convert number to char array string
4471        ("num2str", 1) => match &args[0] {
4472            Value::Void => Err("num2str: not applicable to void".to_string()),
4473            Value::Str(s) => Ok(Value::Str(s.clone())),
4474            Value::StringObj(s) => Ok(Value::Str(s.clone())),
4475            Value::Scalar(n) => Ok(Value::Str(fmt_auto_sig(*n, 5))),
4476            Value::Complex(re, im) => Ok(Value::Str(format_complex(*re, *im, &FormatMode::Short))),
4477            Value::Matrix(m) => {
4478                let s = m
4479                    .iter()
4480                    .map(|x| fmt_auto_sig(*x, 5))
4481                    .collect::<Vec<_>>()
4482                    .join("  ");
4483                Ok(Value::Str(s))
4484            }
4485            Value::ComplexMatrix(_) => {
4486                Err("num2str: not supported for complex matrices".to_string())
4487            }
4488            Value::Lambda(_)
4489            | Value::Function(_)
4490            | Value::Tuple(_)
4491            | Value::Cell(_)
4492            | Value::Struct(_)
4493            | Value::StructArray(_)
4494            | Value::DateTime(_)
4495            | Value::Duration(_)
4496            | Value::DateTimeArray(_)
4497            | Value::DurationArray(_)
4498            | Value::Map(_) => Err("num2str: not applicable to this type".to_string()),
4499        },
4500        // num2str(x, N) — N significant digits
4501        ("num2str", 2) => {
4502            let n = scalar_arg(&args[1], name, 2)? as usize;
4503            match &args[0] {
4504                Value::Void => Err("num2str: not applicable to void".to_string()),
4505                Value::Str(s) => Ok(Value::Str(s.clone())),
4506                Value::StringObj(s) => Ok(Value::Str(s.clone())),
4507                Value::Scalar(v) => Ok(Value::Str(fmt_auto_sig(*v, n))),
4508                Value::Complex(re, im) => {
4509                    Ok(Value::Str(format_complex(*re, *im, &FormatMode::Custom(n))))
4510                }
4511                Value::Matrix(m) => {
4512                    let s = m
4513                        .iter()
4514                        .map(|x| fmt_auto_sig(*x, n))
4515                        .collect::<Vec<_>>()
4516                        .join("  ");
4517                    Ok(Value::Str(s))
4518                }
4519                Value::ComplexMatrix(_) => {
4520                    Err("num2str: not supported for complex matrices".to_string())
4521                }
4522                Value::Lambda(_)
4523                | Value::Function(_)
4524                | Value::Tuple(_)
4525                | Value::Cell(_)
4526                | Value::Struct(_)
4527                | Value::StructArray(_)
4528                | Value::DateTime(_)
4529                | Value::Duration(_)
4530                | Value::DateTimeArray(_)
4531                | Value::DurationArray(_)
4532                | Value::Map(_) => Err("num2str: not applicable to this type".to_string()),
4533            }
4534        }
4535        // str2double(s) — parse string as f64; return NaN on failure
4536        ("str2double", 1) => {
4537            let s = string_arg(&args[0], name, 1)?;
4538            match s.trim().parse::<f64>() {
4539                Ok(n) => Ok(Value::Scalar(n)),
4540                Err(_) => Ok(Value::Scalar(f64::NAN)),
4541            }
4542        }
4543        // str2num(s) — parse string as f64; return error on failure
4544        ("str2num", 1) => {
4545            let s = string_arg(&args[0], name, 1)?;
4546            s.trim()
4547                .parse::<f64>()
4548                .map(Value::Scalar)
4549                .map_err(|_| format!("str2num: cannot convert '{}' to number", s.trim()))
4550        }
4551        // strcat(a, b, ...) — concatenate strings
4552        ("strcat", n) if n >= 2 => {
4553            let mut result = String::new();
4554            let mut any_obj = false;
4555            for (i, arg) in args.iter().enumerate() {
4556                match arg {
4557                    Value::Str(s) => result.push_str(s.trim_end()),
4558                    Value::StringObj(s) => {
4559                        result.push_str(s);
4560                        any_obj = true;
4561                    }
4562                    _ => return Err(format!("strcat: argument {} must be a string", i + 1)),
4563                }
4564            }
4565            if any_obj {
4566                Ok(Value::StringObj(result))
4567            } else {
4568                Ok(Value::Str(result))
4569            }
4570        }
4571        // ischar(s) — 1.0 if char array, 0.0 otherwise
4572        ("ischar", 1) => Ok(Value::Scalar(if matches!(&args[0], Value::Str(_)) {
4573            1.0
4574        } else {
4575            0.0
4576        })),
4577        // isstring(s) — 1.0 if string object, 0.0 otherwise
4578        ("isstring", 1) => Ok(Value::Scalar(if matches!(&args[0], Value::StringObj(_)) {
4579            1.0
4580        } else {
4581            0.0
4582        })),
4583        // --- Struct built-ins ---
4584        // struct('k1',v1,'k2',v2,...) — construct a scalar struct from name-value pairs
4585        ("struct", _) => {
4586            if !args.len().is_multiple_of(2) {
4587                return Err(
4588                    "struct: requires an even number of arguments (name, value, ...)".to_string(),
4589                );
4590            }
4591            let mut map = IndexMap::new();
4592            for pair in args.chunks(2) {
4593                let key = match &pair[0] {
4594                    Value::Str(s) | Value::StringObj(s) => s.clone(),
4595                    _ => return Err("struct: field names must be strings".to_string()),
4596                };
4597                map.insert(key, pair[1].clone());
4598            }
4599            Ok(Value::Struct(Box::new(map)))
4600        }
4601        // fieldnames(s) — cell array of field names in insertion order
4602        ("fieldnames", 1) => match &args[0] {
4603            Value::Struct(map) => {
4604                let names: Vec<Value> = map.keys().map(|k| Value::Str(k.clone())).collect();
4605                Ok(Value::Cell(Box::new(names)))
4606            }
4607            Value::StructArray(arr) => {
4608                // Use field names from first element
4609                let names: Vec<Value> = arr
4610                    .first()
4611                    .map(|m| m.keys().map(|k| Value::Str(k.clone())).collect())
4612                    .unwrap_or_default();
4613                Ok(Value::Cell(Box::new(names)))
4614            }
4615            _ => Err("fieldnames: argument must be a struct".to_string()),
4616        },
4617        // isfield(s, 'name') — 1.0 if field exists, 0.0 otherwise
4618        ("isfield", 2) => {
4619            let field = match &args[1] {
4620                Value::Str(s) | Value::StringObj(s) => s.clone(),
4621                _ => return Err("isfield: second argument must be a string".to_string()),
4622            };
4623            Ok(Value::Scalar(match &args[0] {
4624                Value::Struct(map) if map.contains_key(&field) => 1.0,
4625                Value::StructArray(arr) if arr.first().is_some_and(|m| m.contains_key(&field)) => {
4626                    1.0
4627                }
4628                _ => 0.0,
4629            }))
4630        }
4631        // --- containers.Map built-ins ---
4632        ("isKey", 2) => {
4633            let key = match &args[1] {
4634                Value::Str(s) | Value::StringObj(s) => s.clone(),
4635                _ => return Err("isKey: second argument must be a string key".to_string()),
4636            };
4637            match &args[0] {
4638                Value::Map(map) => Ok(Value::Scalar(if map.contains_key(&key) {
4639                    1.0
4640                } else {
4641                    0.0
4642                })),
4643                _ => Err("isKey: first argument must be a containers.Map".to_string()),
4644            }
4645        }
4646        ("keys", 1) => match &args[0] {
4647            Value::Map(map) => {
4648                let mut sorted_keys: Vec<&String> = map.keys().collect();
4649                sorted_keys.sort();
4650                Ok(Value::Cell(Box::new(
4651                    sorted_keys
4652                        .into_iter()
4653                        .map(|k| Value::Str(k.clone()))
4654                        .collect(),
4655                )))
4656            }
4657            _ => Err("keys: argument must be a containers.Map".to_string()),
4658        },
4659        ("values", 1) => match &args[0] {
4660            Value::Map(map) => {
4661                let mut pairs: Vec<(&String, &Value)> = map.iter().collect();
4662                pairs.sort_by_key(|(k, _)| *k);
4663                Ok(Value::Cell(Box::new(
4664                    pairs.into_iter().map(|(_, v)| v.clone()).collect(),
4665                )))
4666            }
4667            _ => Err("values: argument must be a containers.Map".to_string()),
4668        },
4669        // rmfield(s, 'name') — copy of struct with field removed
4670        ("rmfield", 2) => {
4671            let field = match &args[1] {
4672                Value::Str(s) | Value::StringObj(s) => s.clone(),
4673                _ => return Err("rmfield: second argument must be a string".to_string()),
4674            };
4675            match &args[0] {
4676                Value::Struct(map) => {
4677                    if !map.contains_key(&field) {
4678                        return Err(format!("rmfield: field '{field}' does not exist"));
4679                    }
4680                    let mut updated = map.clone();
4681                    updated.shift_remove(&field);
4682                    Ok(Value::Struct(updated))
4683                }
4684                Value::StructArray(arr) => {
4685                    let updated: Result<Vec<_>, _> = arr
4686                        .iter()
4687                        .map(|m| {
4688                            if !m.contains_key(&field) {
4689                                return Err(format!("rmfield: field '{field}' does not exist"));
4690                            }
4691                            let mut m2 = m.clone();
4692                            m2.shift_remove(&field);
4693                            Ok(m2)
4694                        })
4695                        .collect();
4696                    Ok(Value::StructArray(Box::new(updated?)))
4697                }
4698                _ => Err("rmfield: first argument must be a struct".to_string()),
4699            }
4700        }
4701        // isstruct(v) — 1.0 if v is a struct or struct array, 0.0 otherwise
4702        ("isstruct", 1) => Ok(Value::Scalar(
4703            if matches!(&args[0], Value::Struct(_) | Value::StructArray(_)) {
4704                1.0
4705            } else {
4706                0.0
4707            },
4708        )),
4709        // --- Cell array built-ins ---
4710        // isempty(v) — 1.0 if v has no elements, 0.0 otherwise.
4711        // Matches MATLAB: empty matrix, empty string, empty cell, or Void are empty.
4712        ("isempty", 1) => {
4713            let empty = match &args[0] {
4714                Value::Matrix(m) => m.is_empty(),
4715                Value::Str(s) | Value::StringObj(s) => s.is_empty(),
4716                Value::Cell(v) => v.is_empty(),
4717                Value::Void => true,
4718                _ => false,
4719            };
4720            Ok(Value::Scalar(if empty { 1.0 } else { 0.0 }))
4721        }
4722        // iscell(v) — 1.0 if v is a cell array, 0.0 otherwise
4723        ("iscell", 1) => Ok(Value::Scalar(if matches!(&args[0], Value::Cell(_)) {
4724            1.0
4725        } else {
4726            0.0
4727        })),
4728        // cell(n) — create 1×n cell of Scalar(0.0) slots
4729        ("cell", 1) => {
4730            let n = scalar_arg(&args[0], name, 1)? as usize;
4731            Ok(Value::Cell(Box::new(vec![Value::Scalar(0.0); n])))
4732        }
4733        // cell(m, n) — create 1×(m*n) cell (2-D layout deferred; stored flat)
4734        ("cell", 2) => {
4735            let m = scalar_arg(&args[0], name, 1)? as usize;
4736            let n = scalar_arg(&args[1], name, 2)? as usize;
4737            Ok(Value::Cell(Box::new(vec![Value::Scalar(0.0); m * n])))
4738        }
4739        // cellfun(f, c) — apply f to each element of cell c.
4740        // Returns Value::Matrix when all results are scalars; otherwise Value::Cell.
4741        ("cellfun", 2) => {
4742            let f = args[0].clone();
4743            match &args[1] {
4744                Value::Cell(elems) => {
4745                    let elems: Vec<Value> = (**elems).clone();
4746                    let mut results = Vec::with_capacity(elems.len());
4747                    for elem in &elems {
4748                        let result =
4749                            call_function_value(&f, std::slice::from_ref(elem), io.as_deref_mut())?;
4750                        results.push(result);
4751                    }
4752                    // Try uniform output (all scalars)
4753                    let all_scalar = results.iter().all(|v| matches!(v, Value::Scalar(_)));
4754                    if all_scalar {
4755                        let vals: Vec<f64> = results
4756                            .iter()
4757                            .map(|v| {
4758                                if let Value::Scalar(n) = v {
4759                                    *n
4760                                } else {
4761                                    unreachable!()
4762                                }
4763                            })
4764                            .collect();
4765                        let n = vals.len();
4766                        if n == 0 {
4767                            Ok(Value::Matrix(Box::new(Array2::zeros((1, 0)))))
4768                        } else {
4769                            Ok(Value::Matrix(Box::new(
4770                                Array2::from_shape_vec((1, n), vals).unwrap(),
4771                            )))
4772                        }
4773                    } else {
4774                        Ok(Value::Cell(Box::new(results)))
4775                    }
4776                }
4777                _ => Err("cellfun: second argument must be a cell array".to_string()),
4778            }
4779        }
4780        // arrayfun(f, v) — apply f element-wise to matrix v.
4781        // Returns same-shape Value::Matrix (scalar-returning f only).
4782        ("arrayfun", 2) => {
4783            let f = args[0].clone();
4784            match &args[1] {
4785                Value::Matrix(m) => {
4786                    let m = m.clone();
4787                    let mut flat = Vec::with_capacity(m.len());
4788                    // Iterate in column-major order
4789                    for col in 0..m.ncols() {
4790                        for row in 0..m.nrows() {
4791                            let elem = Value::Scalar(m[[row, col]]);
4792                            let result = call_function_value(&f, &[elem], io.as_deref_mut())?;
4793                            match result {
4794                                Value::Scalar(n) => flat.push(n),
4795                                _ => {
4796                                    return Err(
4797                                        "arrayfun: function must return a scalar".to_string()
4798                                    );
4799                                }
4800                            }
4801                        }
4802                    }
4803                    Ok(Value::Matrix(Box::new(
4804                        Array2::from_shape_vec((m.nrows(), m.ncols()), flat).unwrap(),
4805                    )))
4806                }
4807                Value::Scalar(n) => {
4808                    let elem = Value::Scalar(*n);
4809                    let result = call_function_value(&f, &[elem], io.as_deref_mut())?;
4810                    Ok(result)
4811                }
4812                _ => {
4813                    Err("arrayfun: second argument must be a numeric matrix or scalar".to_string())
4814                }
4815            }
4816        }
4817        // lower(s) — convert to lowercase
4818        ("lower", 1) => match &args[0] {
4819            Value::Str(s) => Ok(Value::Str(s.to_lowercase())),
4820            Value::StringObj(s) => Ok(Value::StringObj(s.to_lowercase())),
4821            _ => Err("lower: argument must be a string".to_string()),
4822        },
4823        // upper(s) — convert to uppercase
4824        ("upper", 1) => match &args[0] {
4825            Value::Str(s) => Ok(Value::Str(s.to_uppercase())),
4826            Value::StringObj(s) => Ok(Value::StringObj(s.to_uppercase())),
4827            _ => Err("upper: argument must be a string".to_string()),
4828        },
4829        // strtrim(s) — trim leading/trailing whitespace
4830        ("strtrim", 1) => match &args[0] {
4831            Value::Str(s) => Ok(Value::Str(s.trim().to_string())),
4832            Value::StringObj(s) => Ok(Value::StringObj(s.trim().to_string())),
4833            _ => Err("strtrim: argument must be a string".to_string()),
4834        },
4835        // strrep(s, old, new) — replace all occurrences
4836        ("strrep", 3) => {
4837            let s = string_arg(&args[0], name, 1)?.to_string();
4838            let old = string_arg(&args[1], name, 2)?;
4839            let new = string_arg(&args[2], name, 3)?;
4840            let result = s.replace(old, new);
4841            match &args[0] {
4842                Value::StringObj(_) => Ok(Value::StringObj(result)),
4843                _ => Ok(Value::Str(result)),
4844            }
4845        }
4846        // strcmp(a, b) — case-sensitive string comparison
4847        ("strcmp", 2) => {
4848            let a = string_arg(&args[0], name, 1)?;
4849            let b = string_arg(&args[1], name, 2)?;
4850            Ok(Value::Scalar(bool_to_f64(a == b)))
4851        }
4852        // strcmpi(a, b) — case-insensitive comparison
4853        ("strcmpi", 2) => {
4854            let a = string_arg(&args[0], name, 1)?.to_lowercase();
4855            let b = string_arg(&args[1], name, 2)?.to_lowercase();
4856            Ok(Value::Scalar(bool_to_f64(a == b)))
4857        }
4858        // disp(x) — display value without variable name, like MATLAB disp()
4859        ("disp", 1) => {
4860            use std::io::Write;
4861            let mode = get_display_fmt();
4862            let output = match &args[0] {
4863                Value::Str(s) | Value::StringObj(s) => format!("{s}\n"),
4864                v => match format_value_full(v, &mode) {
4865                    Some(block) => format!("{block}\n\n"),
4866                    None => format!("{}\n", format_value(v, get_display_base(), &mode)),
4867                },
4868            };
4869            match io {
4870                Some(ctx) => ctx.write_to_fd(1, &output)?,
4871                None => {
4872                    print!("{output}");
4873                    if output.contains('\n') {
4874                        std::io::stdout().flush().ok();
4875                    }
4876                }
4877            }
4878            Ok(Value::Void)
4879        }
4880        // sprintf(fmt, ...) — format and return as char array
4881        ("sprintf", n) if n >= 1 => {
4882            let fmt = string_arg(&args[0], name, 1)?.to_string();
4883            let result = format_printf(&fmt, &args[1..])?;
4884            Ok(Value::Str(result))
4885        }
4886        // fprintf([fd,] fmt, ...) — format and print; fd defaults to 1 (stdout)
4887        ("fprintf", n) if n >= 1 => {
4888            // If first arg is a numeric scalar, treat it as a file descriptor.
4889            let (fd, fmt_idx) = match &args[0] {
4890                Value::Scalar(n) => (*n as i32, 1),
4891                _ => (1, 0),
4892            };
4893            if fmt_idx >= args.len() {
4894                return Err("fprintf: missing format string".to_string());
4895            }
4896            let fmt = string_arg(&args[fmt_idx], name, fmt_idx + 1)?.to_string();
4897            let output = format_printf(&fmt, &args[fmt_idx + 1..])?;
4898            match io {
4899                Some(ctx) => ctx.write_to_fd(fd, &output)?,
4900                None => {
4901                    // No I/O context: only stdout (fd 1) is allowed
4902                    if fd == 1 {
4903                        use std::io::Write;
4904                        print!("{output}");
4905                        if output.contains('\n') {
4906                            std::io::stdout().flush().ok();
4907                        }
4908                    } else {
4909                        return Err("fprintf: file I/O not available in this context".to_string());
4910                    }
4911                }
4912            }
4913            Ok(Value::Void)
4914        }
4915        // fopen(path, mode) — open a file; returns fd or -1 on failure
4916        ("fopen", 2) => {
4917            let path = string_arg(&args[0], name, 1)?;
4918            let mode = string_arg(&args[1], name, 2)?;
4919            match io {
4920                Some(ctx) => Ok(Value::Scalar(ctx.fopen(path, mode) as f64)),
4921                None => Err("fopen: file I/O not available in this context".to_string()),
4922            }
4923        }
4924        // fclose(fd) or fclose('all')
4925        ("fclose", 1) => match &args[0] {
4926            Value::Str(s) if s == "all" => {
4927                if let Some(ctx) = io {
4928                    ctx.fclose_all();
4929                }
4930                Ok(Value::Scalar(0.0))
4931            }
4932            _ => {
4933                let fd = scalar_arg(&args[0], name, 1)? as i32;
4934                match io {
4935                    Some(ctx) => Ok(Value::Scalar(ctx.fclose(fd) as f64)),
4936                    None => Err("fclose: file I/O not available in this context".to_string()),
4937                }
4938            }
4939        },
4940        // fgetl(fd) — read line, strip newline; returns Str or Scalar(-1) at EOF
4941        ("fgetl", 1) => {
4942            let fd = scalar_arg(&args[0], name, 1)? as i32;
4943            match io {
4944                Some(ctx) => match ctx.fgetl(fd) {
4945                    Some(line) => Ok(Value::Str(line)),
4946                    None => Ok(Value::Scalar(-1.0)),
4947                },
4948                None => Err("fgetl: file I/O not available in this context".to_string()),
4949            }
4950        }
4951        // fgets(fd) — read line, keep newline; returns Str or Scalar(-1) at EOF
4952        ("fgets", 1) => {
4953            let fd = scalar_arg(&args[0], name, 1)? as i32;
4954            match io {
4955                Some(ctx) => match ctx.fgets(fd) {
4956                    Some(line) => Ok(Value::Str(line)),
4957                    None => Ok(Value::Scalar(-1.0)),
4958                },
4959                None => Err("fgets: file I/O not available in this context".to_string()),
4960            }
4961        }
4962        // isfile(path) — 1.0 if path exists and is a regular file, else 0.0
4963        ("isfile", 1) => {
4964            let path = string_arg(&args[0], name, 1)?;
4965            let is_file = std::fs::metadata(path)
4966                .map(|m| m.is_file())
4967                .unwrap_or(false);
4968            Ok(Value::Scalar(bool_to_f64(is_file)))
4969        }
4970        // isfolder(path) — 1.0 if path exists and is a directory, else 0.0
4971        ("isfolder", 1) => {
4972            let path = string_arg(&args[0], name, 1)?;
4973            let is_dir = std::fs::metadata(path).map(|m| m.is_dir()).unwrap_or(false);
4974            Ok(Value::Scalar(bool_to_f64(is_dir)))
4975        }
4976        // dir([path]) — directory listing; returns StructArray with name/folder/isdir/bytes
4977        ("dir", _) => {
4978            let path = if args.is_empty() {
4979                "."
4980            } else {
4981                string_arg(&args[0], "dir", 1)?
4982            };
4983            Ok(dir_impl(path))
4984        }
4985        // genpath(dir) — return dir and all subdirectories as a path separator-delimited string
4986        ("genpath", 1) => {
4987            let root = string_arg(&args[0], name, 1)?;
4988            let sep = if cfg!(windows) { ';' } else { ':' };
4989            let mut dirs: Vec<String> = Vec::new();
4990            let mut stack = vec![std::path::PathBuf::from(root)];
4991            while let Some(dir) = stack.pop() {
4992                if !dir.is_dir() {
4993                    continue;
4994                }
4995                dirs.push(dir.to_string_lossy().into_owned());
4996                if let Ok(entries) = std::fs::read_dir(&dir) {
4997                    let mut children: Vec<std::path::PathBuf> = entries
4998                        .filter_map(|e| e.ok())
4999                        .map(|e| e.path())
5000                        .filter(|p| p.is_dir())
5001                        .collect();
5002                    children.sort();
5003                    children.reverse();
5004                    stack.extend(children);
5005                }
5006            }
5007            Ok(Value::Str(dirs.join(&sep.to_string())))
5008        }
5009        // pwd() — current working directory as a char array (parser sends ans as sole arg for empty calls)
5010        ("pwd", _) => {
5011            let cwd = std::env::current_dir()
5012                .map(|p| p.to_string_lossy().into_owned())
5013                .unwrap_or_default();
5014            Ok(Value::Str(cwd))
5015        }
5016        // exist(name) — check var (1), then file (2), else 0
5017        ("exist", 1) => {
5018            let name_arg = string_arg(&args[0], name, 1)?;
5019            if env.contains_key(name_arg) {
5020                Ok(Value::Scalar(1.0))
5021            } else if std::path::Path::new(name_arg).is_file() {
5022                Ok(Value::Scalar(2.0))
5023            } else {
5024                Ok(Value::Scalar(0.0))
5025            }
5026        }
5027        // exist(name, 'var') or exist(name, 'file')
5028        ("exist", 2) => {
5029            let name_arg = string_arg(&args[0], name, 1)?;
5030            let kind = string_arg(&args[1], name, 2)?;
5031            match kind {
5032                "var" => Ok(Value::Scalar(if env.contains_key(name_arg) {
5033                    1.0
5034                } else {
5035                    0.0
5036                })),
5037                "file" => Ok(Value::Scalar(if std::path::Path::new(name_arg).is_file() {
5038                    2.0
5039                } else {
5040                    0.0
5041                })),
5042                other => Err(format!(
5043                    "exist: unknown type '{other}', expected 'var' or 'file'"
5044                )),
5045            }
5046        }
5047        // dlmread(path) / dlmread(path, delim)
5048        ("dlmread", 1) => {
5049            let path = string_arg(&args[0], name, 1)?.to_string();
5050            dlmread_impl(&path, None)
5051        }
5052        ("dlmread", 2) => {
5053            let path = string_arg(&args[0], name, 1)?.to_string();
5054            let delim = interpret_delim(string_arg(&args[1], name, 2)?);
5055            dlmread_impl(&path, Some(delim))
5056        }
5057        // dlmwrite(path, A) / dlmwrite(path, A, delim)
5058        ("dlmwrite", 2) => {
5059            let path = string_arg(&args[0], name, 1)?.to_string();
5060            dlmwrite_impl(&path, &args[1], None)
5061        }
5062        ("dlmwrite", 3) => {
5063            let path = string_arg(&args[0], name, 1)?.to_string();
5064            let delim = interpret_delim(string_arg(&args[2], name, 3)?);
5065            dlmwrite_impl(&path, &args[1], Some(delim))
5066        }
5067        // readmatrix(path) / readmatrix(path, 'Delimiter', d)
5068        ("readmatrix", n) if n == 1 || n == 3 => {
5069            let path = string_arg(&args[0], name, 1)?.to_string();
5070            let delim = parse_delimiter_opt(name, args, 1)?;
5071            readmatrix_impl(&path, delim)
5072        }
5073        // readtable(path) / readtable(path, 'Delimiter', d)
5074        ("readtable", n) if n == 1 || n == 3 => {
5075            let path = string_arg(&args[0], name, 1)?.to_string();
5076            let delim = parse_delimiter_opt(name, args, 1)?;
5077            readtable_impl(&path, delim)
5078        }
5079        // writetable(T, path) / writetable(T, path, 'Delimiter', d)
5080        ("writetable", n) if n == 2 || n == 4 => {
5081            let path = string_arg(&args[1], name, 2)?.to_string();
5082            let delim = parse_delimiter_opt(name, args, 2)?;
5083            writetable_impl(&args[0], &path, delim)
5084        }
5085        // xor(a, b) — element-wise XOR: (a != 0) XOR (b != 0)
5086        ("xor", 2) => {
5087            let a = &args[0];
5088            let b = &args[1];
5089            match (a, b) {
5090                (Value::Scalar(x), Value::Scalar(y)) => {
5091                    Ok(Value::Scalar(bool_to_f64((*x != 0.0) ^ (*y != 0.0))))
5092                }
5093                (Value::Matrix(mx), Value::Matrix(my)) => {
5094                    if mx.shape() != my.shape() {
5095                        return Err("xor: matrices must have the same dimensions".to_string());
5096                    }
5097                    Ok(Value::Matrix(Box::new(
5098                        ndarray::Zip::from(&**mx)
5099                            .and(&**my)
5100                            .map_collect(|a, b| bool_to_f64((*a != 0.0) ^ (*b != 0.0))),
5101                    )))
5102                }
5103                (Value::Scalar(s), Value::Matrix(m)) => {
5104                    let sv = *s != 0.0;
5105                    Ok(Value::Matrix(Box::new(
5106                        m.mapv(|x| bool_to_f64(sv ^ (x != 0.0))),
5107                    )))
5108                }
5109                (Value::Matrix(m), Value::Scalar(s)) => {
5110                    let sv = *s != 0.0;
5111                    Ok(Value::Matrix(Box::new(
5112                        m.mapv(|x| bool_to_f64((x != 0.0) ^ sv)),
5113                    )))
5114                }
5115                _ => Err("xor: arguments must be numeric".to_string()),
5116            }
5117        }
5118        // not(a) — element-wise NOT (alias for ~a)
5119        ("not", 1) => apply_elem(&args[0], |x| if x == 0.0 { 1.0 } else { 0.0 }),
5120        // int2str(x) — round to nearest integer, return as char array
5121        ("int2str", 1) => match &args[0] {
5122            Value::Scalar(n) => Ok(Value::Str(format!("{}", n.round() as i64))),
5123            Value::Matrix(m) => {
5124                let parts: Vec<String> =
5125                    m.iter().map(|x| format!("{}", x.round() as i64)).collect();
5126                Ok(Value::Str(parts.join("  ")))
5127            }
5128            _ => Err("int2str: argument must be numeric".to_string()),
5129        },
5130        // mat2str(A) — matrix to MATLAB literal syntax string
5131        ("mat2str", 1) => match &args[0] {
5132            Value::Scalar(n) => Ok(Value::Str(format!("{n}"))),
5133            Value::Matrix(m) => {
5134                if m.nrows() == 0 || m.ncols() == 0 {
5135                    return Ok(Value::Str("[]".to_string()));
5136                }
5137                let mut s = String::from("[");
5138                for (r, row) in m.rows().into_iter().enumerate() {
5139                    if r > 0 {
5140                        s.push(';');
5141                    }
5142                    for (c, val) in row.iter().enumerate() {
5143                        if c > 0 {
5144                            s.push(' ');
5145                        }
5146                        s.push_str(&format!("{val}"));
5147                    }
5148                }
5149                s.push(']');
5150                Ok(Value::Str(s))
5151            }
5152            _ => Err("mat2str: argument must be numeric".to_string()),
5153        },
5154        // strsplit(s, delim) — split string by delimiter, return cell array
5155        ("strsplit", 2) => {
5156            let s = string_arg(&args[0], name, 1)?.to_string();
5157            let delim = string_arg(&args[1], name, 2)?.to_string();
5158            let parts: Vec<Value> = s
5159                .split(delim.as_str())
5160                .map(|p| Value::Str(p.to_string()))
5161                .collect();
5162            Ok(Value::Cell(Box::new(parts)))
5163        }
5164        // strsplit(s) — split on whitespace
5165        ("strsplit", 1) => {
5166            let s = string_arg(&args[0], name, 1)?.to_string();
5167            let parts: Vec<Value> = s
5168                .split_whitespace()
5169                .map(|p| Value::Str(p.to_string()))
5170                .collect();
5171            Ok(Value::Cell(Box::new(parts)))
5172        }
5173        // strjoin(c) / strjoin(c, delim) — join a cell array of strings
5174        ("strjoin", n) if n == 1 || n == 2 => {
5175            let cells = match &args[0] {
5176                Value::Cell(v) => v,
5177                _ => {
5178                    return Err(
5179                        "strjoin: first argument must be a cell array of strings".to_string()
5180                    );
5181                }
5182            };
5183            let delim = if n == 2 {
5184                string_arg(&args[1], name, 2)?.to_string()
5185            } else {
5186                " ".to_string()
5187            };
5188            let mut parts: Vec<String> = Vec::with_capacity(cells.len());
5189            for (i, v) in cells.iter().enumerate() {
5190                match v {
5191                    Value::Str(s) | Value::StringObj(s) => parts.push(s.clone()),
5192                    _ => return Err(format!("strjoin: element {} must be a string", i + 1)),
5193                }
5194            }
5195            Ok(Value::Str(parts.join(&delim)))
5196        }
5197        // contains(s, pat) / contains(s, pat, 'IgnoreCase', tf) — substring check
5198        ("contains", 2) => {
5199            let s = string_arg(&args[0], name, 1)?;
5200            let pat = string_arg(&args[1], name, 2)?;
5201            Ok(Value::Scalar(bool_to_f64(s.contains(pat))))
5202        }
5203        ("contains", 4) => {
5204            let s = string_arg(&args[0], name, 1)?;
5205            let pat = string_arg(&args[1], name, 2)?;
5206            let key = string_arg(&args[2], name, 3)?;
5207            if key != "IgnoreCase" {
5208                return Err(format!(
5209                    "contains: unknown option '{key}'; expected 'IgnoreCase'"
5210                ));
5211            }
5212            let ignore = match &args[3] {
5213                Value::Scalar(n) => *n != 0.0,
5214                _ => return Err("contains: 'IgnoreCase' value must be a scalar".to_string()),
5215            };
5216            if ignore {
5217                Ok(Value::Scalar(bool_to_f64(
5218                    s.to_lowercase().contains(&pat.to_lowercase()),
5219                )))
5220            } else {
5221                Ok(Value::Scalar(bool_to_f64(s.contains(pat))))
5222            }
5223        }
5224        // startsWith(s, pat) — prefix check
5225        ("startsWith", 2) => {
5226            let s = string_arg(&args[0], name, 1)?;
5227            let pat = string_arg(&args[1], name, 2)?;
5228            Ok(Value::Scalar(bool_to_f64(s.starts_with(pat))))
5229        }
5230        // endsWith(s, pat) — suffix check
5231        ("endsWith", 2) => {
5232            let s = string_arg(&args[0], name, 1)?;
5233            let pat = string_arg(&args[1], name, 2)?;
5234            Ok(Value::Scalar(bool_to_f64(s.ends_with(pat))))
5235        }
5236        // regexp(s, pat) / regexp(s, pat, 'match') — regular expression search
5237        ("regexp", 2) => {
5238            let s = string_arg(&args[0], name, 1)?.to_string();
5239            let pat = string_arg(&args[1], name, 2)?.to_string();
5240            regexp_impl("regexp", &s, &pat, false, false)
5241        }
5242        ("regexp", 3) => {
5243            let s = string_arg(&args[0], name, 1)?.to_string();
5244            let pat = string_arg(&args[1], name, 2)?.to_string();
5245            let opt = string_arg(&args[2], name, 3)?;
5246            if opt != "match" {
5247                return Err(format!("regexp: unknown option '{opt}'; expected 'match'"));
5248            }
5249            regexp_impl("regexp", &s, &pat, false, true)
5250        }
5251        // regexpi(s, pat) — case-insensitive regexp
5252        ("regexpi", 2) => {
5253            let s = string_arg(&args[0], name, 1)?.to_string();
5254            let pat = string_arg(&args[1], name, 2)?.to_string();
5255            regexp_impl("regexpi", &s, &pat, true, false)
5256        }
5257        ("regexpi", 3) => {
5258            let s = string_arg(&args[0], name, 1)?.to_string();
5259            let pat = string_arg(&args[1], name, 2)?.to_string();
5260            let opt = string_arg(&args[2], name, 3)?;
5261            if opt != "match" {
5262                return Err(format!("regexpi: unknown option '{opt}'; expected 'match'"));
5263            }
5264            regexp_impl("regexpi", &s, &pat, true, true)
5265        }
5266        // regexprep(s, pat, rep) — replace all matches with literal replacement
5267        ("regexprep", 3) => {
5268            let s = string_arg(&args[0], name, 1)?.to_string();
5269            let pat = string_arg(&args[1], name, 2)?.to_string();
5270            let rep = string_arg(&args[2], name, 3)?.to_string();
5271            regexprep_impl(&s, &pat, &rep)
5272        }
5273        // error(fmt, args...) — raise a runtime error with a formatted message
5274        ("error", _) if !args.is_empty() => {
5275            let fmt_str = match &args[0] {
5276                Value::Str(s) | Value::StringObj(s) => s.clone(),
5277                _ => return Err("error: first argument must be a format string".to_string()),
5278            };
5279            let msg = format_printf(&fmt_str, &args[1..])?;
5280            Err(msg)
5281        }
5282        // warning(fmt, args...) — print a warning to stderr, continue execution
5283        ("warning", _) if !args.is_empty() => {
5284            let fmt_str = match &args[0] {
5285                Value::Str(s) | Value::StringObj(s) => s.clone(),
5286                _ => return Err("warning: first argument must be a format string".to_string()),
5287            };
5288            let msg = format_printf(&fmt_str, &args[1..])?;
5289            eprintln!("warning: {msg}");
5290            Ok(Value::Void)
5291        }
5292        // lasterr() — return last error message; lasterr(msg) — set and return previous
5293        ("lasterr", 0) => Ok(Value::Str(get_last_err())),
5294        ("lasterr", 1) => {
5295            let prev = get_last_err();
5296            let new_msg = match &args[0] {
5297                Value::Str(s) | Value::StringObj(s) => s.clone(),
5298                _ => return Err("lasterr: argument must be a string".to_string()),
5299            };
5300            set_last_err(&new_msg);
5301            Ok(Value::Str(prev))
5302        }
5303        // pcall(@func, args...) — protected call; returns [ok, result_or_msg]
5304        ("pcall", _) if !args.is_empty() => {
5305            let callable = args[0].clone();
5306            let call_args = &args[1..];
5307            let result = match &callable {
5308                Value::Lambda(f) => {
5309                    let f = f.clone();
5310                    f.0(call_args, io)
5311                }
5312                Value::Function(_) => match io {
5313                    Some(io_ref) => FN_CALL_HOOK.with(|c| match c.get() {
5314                        Some(hook) => hook("<pcall>", &callable, call_args, env, io_ref),
5315                        None => Err("pcall: function execution not initialized".to_string()),
5316                    }),
5317                    None => {
5318                        let mut tmp_io = IoContext::new();
5319                        FN_CALL_HOOK.with(|c| match c.get() {
5320                            Some(hook) => hook("<pcall>", &callable, call_args, env, &mut tmp_io),
5321                            None => Err("pcall: function execution not initialized".to_string()),
5322                        })
5323                    }
5324                },
5325                _ => {
5326                    return Err(
5327                        "pcall: first argument must be a function handle (@func)".to_string()
5328                    );
5329                }
5330            };
5331            match result {
5332                Ok(v) => Ok(Value::Tuple(vec![Value::Scalar(1.0), v])),
5333                Err(msg) => {
5334                    set_last_err(&msg);
5335                    Ok(Value::Tuple(vec![Value::Scalar(0.0), Value::Str(msg)]))
5336                }
5337            }
5338        }
5339        // ── Phase 18 — Advanced linear algebra ──────────────────────────────
5340
5341        // eig(A): d = eig(A) → eigenvalue column vector; [V,D] = eig(A) → tuple.
5342        ("eig", 1) => match &args[0] {
5343            Value::Scalar(n) => {
5344                if get_nargout() <= 1 {
5345                    Ok(Value::Matrix(Box::new(
5346                        Array2::from_shape_vec((1, 1), vec![*n]).unwrap(),
5347                    )))
5348                } else {
5349                    Ok(Value::Tuple(vec![
5350                        Value::Matrix(Box::new(Array2::eye(1))),
5351                        Value::Matrix(Box::new(Array2::from_elem((1, 1), *n))),
5352                    ]))
5353                }
5354            }
5355            Value::Matrix(m) => {
5356                let (evals, evecs) = eig_compute(m)?;
5357                let nn = evals.len();
5358                let has_imag = evals.iter().any(|c| c.im.abs() > 1e-14);
5359                if get_nargout() <= 1 {
5360                    if has_imag {
5361                        Ok(Value::ComplexMatrix(Box::new(
5362                            Array2::from_shape_vec((nn, 1), evals).unwrap(),
5363                        )))
5364                    } else {
5365                        let reals: Vec<f64> = evals.iter().map(|c| c.re).collect();
5366                        Ok(Value::Matrix(Box::new(
5367                            Array2::from_shape_vec((nn, 1), reals).unwrap(),
5368                        )))
5369                    }
5370                } else if has_imag {
5371                    Err("eig: [V,D] form not supported when eigenvalues are complex".to_string())
5372                } else {
5373                    let reals: Vec<f64> = evals.iter().map(|c| c.re).collect();
5374                    let mut d = Array2::<f64>::zeros((nn, nn));
5375                    for (i, &e) in reals.iter().enumerate() {
5376                        d[[i, i]] = e;
5377                    }
5378                    Ok(Value::Tuple(vec![
5379                        Value::Matrix(Box::new(evecs)),
5380                        Value::Matrix(Box::new(d)),
5381                    ]))
5382                }
5383            }
5384            _ => Err("eig: argument must be a real numeric matrix".to_string()),
5385        },
5386
5387        // svd(A): s = svd(A) → singular values; [U,S,V] = svd(A) → full tuple.
5388        // svd(A, 'econ') → economy tuple.
5389        ("svd", 1) => match &args[0] {
5390            Value::Scalar(n) => {
5391                let sv = n.abs();
5392                if get_nargout() <= 1 {
5393                    Ok(Value::Matrix(Box::new(
5394                        Array2::from_shape_vec((1, 1), vec![sv]).unwrap(),
5395                    )))
5396                } else {
5397                    Ok(Value::Tuple(vec![
5398                        Value::Matrix(Box::new(Array2::eye(1))),
5399                        Value::Matrix(Box::new(Array2::from_elem((1, 1), sv))),
5400                        Value::Matrix(Box::new(Array2::eye(1))),
5401                    ]))
5402                }
5403            }
5404            Value::Matrix(m) => {
5405                let mm = m.nrows();
5406                let nn = m.ncols();
5407                let (u_c, s_v, v_c) = svd_compute(m)?;
5408                let k = s_v.len();
5409                if get_nargout() <= 1 {
5410                    let col: Vec<f64> = s_v;
5411                    Ok(Value::Matrix(Box::new(
5412                        Array2::from_shape_vec((k, 1), col).unwrap(),
5413                    )))
5414                } else {
5415                    // Full SVD: extend U to m×m, S to m×n.
5416                    let u_full = complete_orthonormal_basis(&u_c);
5417                    let mut s_mat = Array2::<f64>::zeros((mm, nn));
5418                    for (i, &sv) in s_v.iter().enumerate() {
5419                        s_mat[[i, i]] = sv;
5420                    }
5421                    Ok(Value::Tuple(vec![
5422                        Value::Matrix(Box::new(u_full)),
5423                        Value::Matrix(Box::new(s_mat)),
5424                        Value::Matrix(Box::new(v_c)),
5425                    ]))
5426                }
5427            }
5428            _ => Err("svd: argument must be a real numeric matrix".to_string()),
5429        },
5430        ("svd", 2) => match (&args[0], &args[1]) {
5431            (Value::Matrix(m), Value::Str(opt) | Value::StringObj(opt)) if opt == "econ" => {
5432                let (u_c, s_v, v_c) = svd_compute(m)?;
5433                let k = s_v.len();
5434                let mut s_mat = Array2::<f64>::zeros((k, k));
5435                for (i, &sv) in s_v.iter().enumerate() {
5436                    s_mat[[i, i]] = sv;
5437                }
5438                Ok(Value::Tuple(vec![
5439                    Value::Matrix(Box::new(u_c)),
5440                    Value::Matrix(Box::new(s_mat)),
5441                    Value::Matrix(Box::new(v_c)),
5442                ]))
5443            }
5444            _ => Err("svd: expected svd(A, 'econ')".to_string()),
5445        },
5446
5447        // lu(A): R = lu(A) → U factor; [L,U,P] = lu(A) → full tuple (PA=LU).
5448        ("lu", 1) => match &args[0] {
5449            Value::Scalar(n) => {
5450                if get_nargout() <= 1 {
5451                    Ok(Value::Scalar(*n))
5452                } else {
5453                    Ok(Value::Tuple(vec![
5454                        Value::Matrix(Box::new(Array2::eye(1))),
5455                        Value::Matrix(Box::new(Array2::from_elem((1, 1), *n))),
5456                        Value::Matrix(Box::new(Array2::eye(1))),
5457                    ]))
5458                }
5459            }
5460            Value::Matrix(m) => {
5461                let (l, u, p) = lu_decompose(m)?;
5462                if get_nargout() <= 1 {
5463                    Ok(Value::Matrix(Box::new(u)))
5464                } else {
5465                    Ok(Value::Tuple(vec![
5466                        Value::Matrix(Box::new(l)),
5467                        Value::Matrix(Box::new(u)),
5468                        Value::Matrix(Box::new(p)),
5469                    ]))
5470                }
5471            }
5472            _ => Err("lu: argument must be a real numeric matrix".to_string()),
5473        },
5474
5475        // qr(A): R = qr(A) → R factor; [Q,R] = qr(A) → full tuple.
5476        ("qr", 1) => match &args[0] {
5477            Value::Scalar(n) => {
5478                if get_nargout() <= 1 {
5479                    Ok(Value::Scalar(*n))
5480                } else {
5481                    Ok(Value::Tuple(vec![
5482                        Value::Matrix(Box::new(Array2::from_elem(
5483                            (1, 1),
5484                            if *n >= 0.0 { 1.0 } else { -1.0 },
5485                        ))),
5486                        Value::Matrix(Box::new(Array2::from_elem((1, 1), n.abs()))),
5487                    ]))
5488                }
5489            }
5490            Value::Matrix(m) => {
5491                let (q, r) = qr_decompose(m)?;
5492                if get_nargout() <= 1 {
5493                    Ok(Value::Matrix(Box::new(r)))
5494                } else {
5495                    Ok(Value::Tuple(vec![
5496                        Value::Matrix(Box::new(q)),
5497                        Value::Matrix(Box::new(r)),
5498                    ]))
5499                }
5500            }
5501            _ => Err("qr: argument must be a real numeric matrix".to_string()),
5502        },
5503
5504        // chol(A): always returns upper triangular R such that A = R'*R.
5505        ("chol", 1) => match &args[0] {
5506            Value::Scalar(n) => {
5507                if *n < 0.0 {
5508                    Err("chol: value is not positive definite".to_string())
5509                } else {
5510                    Ok(Value::Scalar(n.sqrt()))
5511                }
5512            }
5513            Value::Matrix(m) => Ok(Value::Matrix(Box::new(chol_decompose(m)?))),
5514            _ => Err("chol: argument must be a real numeric matrix".to_string()),
5515        },
5516
5517        // rank(A): numerical rank via SVD threshold.
5518        ("rank", 1) => match &args[0] {
5519            Value::Scalar(x) => Ok(Value::Scalar(if x.abs() > 1e-15 { 1.0 } else { 0.0 })),
5520            Value::Matrix(m) => {
5521                let (_, s_v, _) = svd_compute(m)?;
5522                let tol = (m.nrows().max(m.ncols())) as f64
5523                    * s_v.first().copied().unwrap_or(0.0)
5524                    * f64::EPSILON
5525                    * 2.0;
5526                let r = s_v.iter().filter(|&&s| s > tol).count();
5527                Ok(Value::Scalar(r as f64))
5528            }
5529            _ => Err("rank: argument must be a real numeric matrix".to_string()),
5530        },
5531
5532        // null(A): orthonormal basis for null space of A (columns of V for ~0 singular values).
5533        ("null", 1) => match &args[0] {
5534            Value::Scalar(_) => Ok(Value::Matrix(Box::new(Array2::zeros((1, 0))))),
5535            Value::Matrix(m) => {
5536                let nn = m.ncols();
5537                let (_, s_v, v_c) = svd_compute(m)?;
5538                let tol = (m.nrows().max(nn)) as f64
5539                    * s_v.first().copied().unwrap_or(0.0)
5540                    * f64::EPSILON
5541                    * 2.0;
5542                let r = s_v.iter().filter(|&&s| s > tol).count();
5543                let null_k = nn.saturating_sub(r);
5544                if null_k == 0 {
5545                    return Ok(Value::Matrix(Box::new(Array2::zeros((nn, 0)))));
5546                }
5547                let mut result = Array2::<f64>::zeros((nn, null_k));
5548                for j in 0..null_k {
5549                    let col_idx = r + j;
5550                    if col_idx < v_c.ncols() {
5551                        for i in 0..nn {
5552                            result[[i, j]] = v_c[[i, col_idx]];
5553                        }
5554                    }
5555                }
5556                Ok(Value::Matrix(Box::new(result)))
5557            }
5558            _ => Err("null: argument must be a real numeric matrix".to_string()),
5559        },
5560
5561        // orth(A): orthonormal basis for column space of A (columns of U for nonzero singular values).
5562        ("orth", 1) => match &args[0] {
5563            Value::Scalar(x) => {
5564                if x.abs() > 1e-15 {
5565                    Ok(Value::Matrix(Box::new(Array2::from_elem((1, 1), 1.0))))
5566                } else {
5567                    Ok(Value::Matrix(Box::new(Array2::zeros((1, 0)))))
5568                }
5569            }
5570            Value::Matrix(m) => {
5571                let mm = m.nrows();
5572                let (u_c, s_v, _) = svd_compute(m)?;
5573                let tol = (mm.max(m.ncols())) as f64
5574                    * s_v.first().copied().unwrap_or(0.0)
5575                    * f64::EPSILON
5576                    * 2.0;
5577                let r = s_v.iter().filter(|&&s| s > tol).count();
5578                if r == 0 {
5579                    return Ok(Value::Matrix(Box::new(Array2::zeros((mm, 0)))));
5580                }
5581                let mut result = Array2::<f64>::zeros((mm, r));
5582                for j in 0..r {
5583                    if j < u_c.ncols() {
5584                        for i in 0..mm {
5585                            result[[i, j]] = u_c[[i, j]];
5586                        }
5587                    }
5588                }
5589                Ok(Value::Matrix(Box::new(result)))
5590            }
5591            _ => Err("orth: argument must be a real numeric matrix".to_string()),
5592        },
5593
5594        // cond(A): condition number = σ_max / σ_min (2-norm by default).
5595        ("cond", 1) => match &args[0] {
5596            Value::Scalar(x) => {
5597                if x.abs() < 1e-15 {
5598                    Ok(Value::Scalar(f64::INFINITY))
5599                } else {
5600                    Ok(Value::Scalar(1.0))
5601                }
5602            }
5603            Value::Matrix(m) => {
5604                let (_, s_v, _) = svd_compute(m)?;
5605                if s_v.is_empty() {
5606                    return Ok(Value::Scalar(1.0));
5607                }
5608                let s_max = s_v[0];
5609                let s_min = *s_v.last().unwrap();
5610                Ok(Value::Scalar(if s_min < 1e-15 {
5611                    f64::INFINITY
5612                } else {
5613                    s_max / s_min
5614                }))
5615            }
5616            _ => Err("cond: argument must be a real numeric matrix".to_string()),
5617        },
5618
5619        // pinv(A): Moore-Penrose pseudoinverse via SVD.
5620        ("pinv", 1) => match &args[0] {
5621            Value::Scalar(x) => Ok(Value::Scalar(if x.abs() < 1e-15 { 0.0 } else { 1.0 / x })),
5622            Value::Matrix(m) => {
5623                let mm = m.nrows();
5624                let nn = m.ncols();
5625                let (u_c, s_v, v_c) = svd_compute(m)?;
5626                let k = s_v.len();
5627                let tol =
5628                    (mm.max(nn)) as f64 * s_v.first().copied().unwrap_or(0.0) * f64::EPSILON * 2.0;
5629                // pinv = V * diag(1/σ) * U^T
5630                let mut result = Array2::<f64>::zeros((nn, mm));
5631                for j in 0..k {
5632                    if s_v[j] > tol {
5633                        let inv_s = 1.0 / s_v[j];
5634                        for r in 0..nn {
5635                            for c in 0..mm {
5636                                result[[r, c]] += v_c[[r, j]] * inv_s * u_c[[c, j]];
5637                            }
5638                        }
5639                    }
5640                }
5641                Ok(Value::Matrix(Box::new(result)))
5642            }
5643            _ => Err("pinv: argument must be a real numeric matrix".to_string()),
5644        },
5645
5646        // ── Phase 26 — FFT ───────────────────────────────────────────────────────
5647        ("fft", 1) => fft_call(&args[0], None),
5648        ("fft", 2) => {
5649            let n = scalar_arg(&args[1], "fft", 2)?;
5650            let n = n as usize;
5651            if n == 0 {
5652                return Err("fft: length must be positive".to_string());
5653            }
5654            fft_call(&args[0], Some(n))
5655        }
5656        ("ifft", 1) => ifft_call(&args[0]),
5657
5658        // fftshift(x) — circular shift by floor(N/2), no feature flag required
5659        ("fftshift", 1) => match &args[0] {
5660            Value::Scalar(s) => Ok(Value::Scalar(*s)),
5661            Value::Matrix(m) => {
5662                let (nrows, ncols) = (m.nrows(), m.ncols());
5663                if nrows == 1 {
5664                    let n = ncols;
5665                    let shift = n / 2;
5666                    let data: Vec<f64> = m.iter().copied().collect();
5667                    let mut out = vec![0.0f64; n];
5668                    for (i, &x) in data.iter().enumerate() {
5669                        out[(i + shift) % n] = x;
5670                    }
5671                    Ok(Value::Matrix(Box::new(
5672                        Array2::from_shape_vec((1, n), out).unwrap(),
5673                    )))
5674                } else if ncols == 1 {
5675                    let n = nrows;
5676                    let shift = n / 2;
5677                    let data: Vec<f64> = m.iter().copied().collect();
5678                    let mut out = vec![0.0f64; n];
5679                    for (i, &x) in data.iter().enumerate() {
5680                        out[(i + shift) % n] = x;
5681                    }
5682                    Ok(Value::Matrix(Box::new(
5683                        Array2::from_shape_vec((n, 1), out).unwrap(),
5684                    )))
5685                } else {
5686                    let row_shift = nrows / 2;
5687                    let col_shift = ncols / 2;
5688                    let mut out = Array2::<f64>::zeros((nrows, ncols));
5689                    for i in 0..nrows {
5690                        for j in 0..ncols {
5691                            out[[(i + row_shift) % nrows, (j + col_shift) % ncols]] = m[[i, j]];
5692                        }
5693                    }
5694                    Ok(Value::Matrix(Box::new(out)))
5695                }
5696            }
5697            _ => Err("fftshift: argument must be a numeric matrix".to_string()),
5698        },
5699
5700        // ifftshift(x) — inverse circular shift by ceil(N/2), no feature flag required
5701        ("ifftshift", 1) => match &args[0] {
5702            Value::Scalar(s) => Ok(Value::Scalar(*s)),
5703            Value::Matrix(m) => {
5704                let (nrows, ncols) = (m.nrows(), m.ncols());
5705                if nrows == 1 {
5706                    let n = ncols;
5707                    let shift = n.div_ceil(2);
5708                    let data: Vec<f64> = m.iter().copied().collect();
5709                    let mut out = vec![0.0f64; n];
5710                    for (i, &x) in data.iter().enumerate() {
5711                        out[(i + shift) % n] = x;
5712                    }
5713                    Ok(Value::Matrix(Box::new(
5714                        Array2::from_shape_vec((1, n), out).unwrap(),
5715                    )))
5716                } else if ncols == 1 {
5717                    let n = nrows;
5718                    let shift = n.div_ceil(2);
5719                    let data: Vec<f64> = m.iter().copied().collect();
5720                    let mut out = vec![0.0f64; n];
5721                    for (i, &x) in data.iter().enumerate() {
5722                        out[(i + shift) % n] = x;
5723                    }
5724                    Ok(Value::Matrix(Box::new(
5725                        Array2::from_shape_vec((n, 1), out).unwrap(),
5726                    )))
5727                } else {
5728                    let row_shift = nrows.div_ceil(2);
5729                    let col_shift = ncols.div_ceil(2);
5730                    let mut out = Array2::<f64>::zeros((nrows, ncols));
5731                    for i in 0..nrows {
5732                        for j in 0..ncols {
5733                            out[[(i + row_shift) % nrows, (j + col_shift) % ncols]] = m[[i, j]];
5734                        }
5735                    }
5736                    Ok(Value::Matrix(Box::new(out)))
5737                }
5738            }
5739            _ => Err("ifftshift: argument must be a numeric matrix".to_string()),
5740        },
5741
5742        // fftfreq(n, d) — DFT sample frequencies, no feature flag required
5743        ("fftfreq", 2) => {
5744            let n = match &args[0] {
5745                Value::Scalar(s) => {
5746                    let n = *s as usize;
5747                    if *s < 1.0 || (*s - n as f64).abs() > 1e-9 {
5748                        return Err("fftfreq: n must be a positive integer".to_string());
5749                    }
5750                    n
5751                }
5752                _ => return Err("fftfreq: first argument must be a scalar integer".to_string()),
5753            };
5754            let d = scalar_arg(&args[1], "fftfreq", 2)?;
5755            if d == 0.0 {
5756                return Err("fftfreq: sample spacing d must be nonzero".to_string());
5757            }
5758            // NumPy-compatible formula: [0:pos_count-1, -neg_count:-1] / (n*d)
5759            let pos_count = (n - 1) / 2 + 1;
5760            let neg_count = n / 2;
5761            let factor = 1.0 / (n as f64 * d);
5762            let mut freqs = Vec::with_capacity(n);
5763            for k in 0..pos_count as i64 {
5764                freqs.push(k as f64 * factor);
5765            }
5766            let neg_start = -(neg_count as i64);
5767            for k in neg_start..0 {
5768                freqs.push(k as f64 * factor);
5769            }
5770            Ok(Value::Matrix(Box::new(
5771                Array2::from_shape_vec((1, n), freqs).unwrap(),
5772            )))
5773        }
5774
5775        // jsondecode(str) / jsonencode(val)
5776        ("jsondecode", 1) => jsondecode_impl(&args[0]),
5777        ("jsonencode", 1) => jsonencode_impl(&args[0]),
5778
5779        // load('file.mat') — assignment form: data = load('file.mat')
5780        ("load", 1) => {
5781            let path = match &args[0] {
5782                Value::Str(s) | Value::StringObj(s) => s.clone(),
5783                _ => return Err("load: argument must be a string path".to_string()),
5784            };
5785            if !path.ends_with(".mat") {
5786                return Err("load: use bare 'load path' syntax for non-.mat files".to_string());
5787            }
5788            load_mat_file(&path)
5789        }
5790
5791        // assert(cond)
5792        ("assert", 1) => {
5793            let truthy = match &args[0] {
5794                Value::Scalar(n) => *n != 0.0 && !n.is_nan(),
5795                Value::Matrix(m) => m.iter().all(|&x| x != 0.0 && !x.is_nan()),
5796                Value::Complex(re, im) => *re != 0.0 || *im != 0.0,
5797                Value::Str(s) | Value::StringObj(s) => !s.is_empty(),
5798                _ => false,
5799            };
5800            if truthy {
5801                Ok(Value::Void)
5802            } else {
5803                Err("assert: condition is false".to_string())
5804            }
5805        }
5806
5807        // assert(expected, actual)
5808        ("assert", 2) => assert_values_equal(&args[0], &args[1], None),
5809
5810        // assert(expected, actual, tol)
5811        ("assert", 3) => {
5812            let tol = match &args[2] {
5813                Value::Scalar(t) => *t,
5814                _ => return Err("assert: tolerance must be a scalar".to_string()),
5815            };
5816            assert_values_equal(&args[0], &args[1], Some(tol))
5817        }
5818
5819        // ── datetime() constructor ────────────────────────────────────────────
5820        ("datetime", 1) => match &args[0] {
5821            Value::Str(s) | Value::StringObj(s) => {
5822                let s = s.as_str();
5823                if s == "now" {
5824                    return Ok(Value::DateTime(crate::datetime::now_timestamp()));
5825                }
5826                if s == "today" {
5827                    return Ok(Value::DateTime(crate::datetime::today_timestamp()));
5828                }
5829                crate::datetime::parse_iso8601(s).map(Value::DateTime)
5830            }
5831            _ => Err("datetime: expected a string or numeric constructor arguments".to_string()),
5832        },
5833        // datetime(ts, 'ConvertFrom', 'posixtime') — must come before the 3-scalar form
5834        ("datetime", 3) if matches!(&args[1], Value::Str(_) | Value::StringObj(_)) => {
5835            let ts = scalar_arg(&args[0], "datetime", 1)?;
5836            match (&args[1], &args[2]) {
5837                (Value::Str(k) | Value::StringObj(k), Value::Str(v) | Value::StringObj(v))
5838                    if k.eq_ignore_ascii_case("convertfrom")
5839                        && v.eq_ignore_ascii_case("posixtime") =>
5840                {
5841                    Ok(Value::DateTime(ts))
5842                }
5843                _ => Err("datetime: unsupported arguments".to_string()),
5844            }
5845        }
5846        ("datetime", 3) => {
5847            let y = scalar_arg(&args[0], "datetime", 1)? as i64;
5848            let mo = scalar_arg(&args[1], "datetime", 2)? as u32;
5849            let d = scalar_arg(&args[2], "datetime", 3)? as u32;
5850            Ok(Value::DateTime(crate::datetime::civil_to_timestamp(
5851                y, mo, d, 0, 0, 0.0,
5852            )))
5853        }
5854        ("datetime", 6) => {
5855            let y = scalar_arg(&args[0], "datetime", 1)? as i64;
5856            let mo = scalar_arg(&args[1], "datetime", 2)? as u32;
5857            let d = scalar_arg(&args[2], "datetime", 3)? as u32;
5858            let h = scalar_arg(&args[3], "datetime", 4)? as u32;
5859            let mi = scalar_arg(&args[4], "datetime", 5)? as u32;
5860            let s = scalar_arg(&args[5], "datetime", 6)?;
5861            Ok(Value::DateTime(crate::datetime::civil_to_timestamp(
5862                y, mo, d, h, mi, s,
5863            )))
5864        }
5865
5866        // ── Component extractors ──────────────────────────────────────────────
5867        ("year", 1) => match &args[0] {
5868            Value::DateTime(ts) => {
5869                let (y, ..) = crate::datetime::timestamp_to_civil(*ts);
5870                Ok(Value::Scalar(y as f64))
5871            }
5872            Value::DateTimeArray(v) => {
5873                let rows: Vec<f64> = v
5874                    .iter()
5875                    .map(|ts| {
5876                        let (y, ..) = crate::datetime::timestamp_to_civil(*ts);
5877                        y as f64
5878                    })
5879                    .collect();
5880                Ok(Value::Matrix(Box::new(
5881                    ndarray::Array2::from_shape_vec((rows.len(), 1), rows)
5882                        .map_err(|e| e.to_string())?,
5883                )))
5884            }
5885            _ => Err("year: argument must be a datetime".to_string()),
5886        },
5887        ("month", 1) => match &args[0] {
5888            Value::DateTime(ts) => {
5889                let (_, mo, ..) = crate::datetime::timestamp_to_civil(*ts);
5890                Ok(Value::Scalar(mo as f64))
5891            }
5892            Value::DateTimeArray(v) => {
5893                let rows: Vec<f64> = v
5894                    .iter()
5895                    .map(|ts| {
5896                        let (_, mo, ..) = crate::datetime::timestamp_to_civil(*ts);
5897                        mo as f64
5898                    })
5899                    .collect();
5900                Ok(Value::Matrix(Box::new(
5901                    ndarray::Array2::from_shape_vec((rows.len(), 1), rows)
5902                        .map_err(|e| e.to_string())?,
5903                )))
5904            }
5905            _ => Err("month: argument must be a datetime".to_string()),
5906        },
5907        ("day", 1) => match &args[0] {
5908            Value::DateTime(ts) => {
5909                let (_, _, d, ..) = crate::datetime::timestamp_to_civil(*ts);
5910                Ok(Value::Scalar(d as f64))
5911            }
5912            Value::DateTimeArray(v) => {
5913                let rows: Vec<f64> = v
5914                    .iter()
5915                    .map(|ts| {
5916                        let (_, _, d, ..) = crate::datetime::timestamp_to_civil(*ts);
5917                        d as f64
5918                    })
5919                    .collect();
5920                Ok(Value::Matrix(Box::new(
5921                    ndarray::Array2::from_shape_vec((rows.len(), 1), rows)
5922                        .map_err(|e| e.to_string())?,
5923                )))
5924            }
5925            _ => Err("day: argument must be a datetime".to_string()),
5926        },
5927        ("hour", 1) => match &args[0] {
5928            Value::DateTime(ts) => {
5929                let (_, _, _, h, ..) = crate::datetime::timestamp_to_civil(*ts);
5930                Ok(Value::Scalar(h as f64))
5931            }
5932            Value::DateTimeArray(v) => {
5933                let rows: Vec<f64> = v
5934                    .iter()
5935                    .map(|ts| {
5936                        let (_, _, _, h, ..) = crate::datetime::timestamp_to_civil(*ts);
5937                        h as f64
5938                    })
5939                    .collect();
5940                Ok(Value::Matrix(Box::new(
5941                    ndarray::Array2::from_shape_vec((rows.len(), 1), rows)
5942                        .map_err(|e| e.to_string())?,
5943                )))
5944            }
5945            _ => Err("hour: argument must be a datetime or duration".to_string()),
5946        },
5947        ("minute", 1) => match &args[0] {
5948            Value::DateTime(ts) => {
5949                let (_, _, _, _, mi, ..) = crate::datetime::timestamp_to_civil(*ts);
5950                Ok(Value::Scalar(mi as f64))
5951            }
5952            Value::DateTimeArray(v) => {
5953                let rows: Vec<f64> = v
5954                    .iter()
5955                    .map(|ts| {
5956                        let (_, _, _, _, mi, ..) = crate::datetime::timestamp_to_civil(*ts);
5957                        mi as f64
5958                    })
5959                    .collect();
5960                Ok(Value::Matrix(Box::new(
5961                    ndarray::Array2::from_shape_vec((rows.len(), 1), rows)
5962                        .map_err(|e| e.to_string())?,
5963                )))
5964            }
5965            _ => Err("minute: argument must be a datetime or duration".to_string()),
5966        },
5967        ("second", 1) => match &args[0] {
5968            Value::DateTime(ts) => {
5969                let (_, _, _, _, _, s) = crate::datetime::timestamp_to_civil(*ts);
5970                Ok(Value::Scalar(s))
5971            }
5972            Value::DateTimeArray(v) => {
5973                let rows: Vec<f64> = v
5974                    .iter()
5975                    .map(|ts| {
5976                        let (_, _, _, _, _, s) = crate::datetime::timestamp_to_civil(*ts);
5977                        s
5978                    })
5979                    .collect();
5980                Ok(Value::Matrix(Box::new(
5981                    ndarray::Array2::from_shape_vec((rows.len(), 1), rows)
5982                        .map_err(|e| e.to_string())?,
5983                )))
5984            }
5985            _ => Err("second: argument must be a datetime or duration".to_string()),
5986        },
5987
5988        // ── Predicates ────────────────────────────────────────────────────────
5989        ("isdatetime", 1) => Ok(Value::Scalar(bool_to_f64(matches!(
5990            &args[0],
5991            Value::DateTime(_) | Value::DateTimeArray(_)
5992        )))),
5993        ("isduration", 1) => Ok(Value::Scalar(bool_to_f64(matches!(
5994            &args[0],
5995            Value::Duration(_) | Value::DurationArray(_)
5996        )))),
5997        ("isnat", 1) => match &args[0] {
5998            Value::DateTime(ts) => Ok(Value::Scalar(bool_to_f64(ts.is_nan()))),
5999            Value::DateTimeArray(v) => {
6000                let rows: Vec<f64> = v
6001                    .iter()
6002                    .map(|ts| if ts.is_nan() { 1.0 } else { 0.0 })
6003                    .collect();
6004                Ok(Value::Matrix(Box::new(
6005                    ndarray::Array2::from_shape_vec((rows.len(), 1), rows)
6006                        .map_err(|e| e.to_string())?,
6007                )))
6008            }
6009            _ => Ok(Value::Scalar(0.0)),
6010        },
6011
6012        // ── Duration constructors / extractors (overloaded) ───────────────────
6013        ("hours", 1) => match &args[0] {
6014            Value::Duration(s) => Ok(Value::Scalar(*s / 3600.0)),
6015            Value::DurationArray(v) => {
6016                let rows: Vec<f64> = v.iter().map(|s| s / 3600.0).collect();
6017                Ok(Value::Matrix(Box::new(
6018                    ndarray::Array2::from_shape_vec((rows.len(), 1), rows)
6019                        .map_err(|e| e.to_string())?,
6020                )))
6021            }
6022            _ => {
6023                let s = scalar_arg(&args[0], "hours", 1)?;
6024                Ok(Value::Duration(s * 3600.0))
6025            }
6026        },
6027        ("minutes", 1) => match &args[0] {
6028            Value::Duration(s) => Ok(Value::Scalar(*s / 60.0)),
6029            Value::DurationArray(v) => {
6030                let rows: Vec<f64> = v.iter().map(|s| s / 60.0).collect();
6031                Ok(Value::Matrix(Box::new(
6032                    ndarray::Array2::from_shape_vec((rows.len(), 1), rows)
6033                        .map_err(|e| e.to_string())?,
6034                )))
6035            }
6036            _ => {
6037                let s = scalar_arg(&args[0], "minutes", 1)?;
6038                Ok(Value::Duration(s * 60.0))
6039            }
6040        },
6041        ("seconds", 1) => match &args[0] {
6042            Value::Duration(s) => Ok(Value::Scalar(*s)),
6043            Value::DurationArray(v) => {
6044                let rows = v.to_vec();
6045                Ok(Value::Matrix(Box::new(
6046                    ndarray::Array2::from_shape_vec((rows.len(), 1), rows)
6047                        .map_err(|e| e.to_string())?,
6048                )))
6049            }
6050            _ => {
6051                let s = scalar_arg(&args[0], "seconds", 1)?;
6052                Ok(Value::Duration(s))
6053            }
6054        },
6055        ("days", 1) => match &args[0] {
6056            Value::Duration(s) => Ok(Value::Scalar(*s / 86400.0)),
6057            Value::DurationArray(v) => {
6058                let rows: Vec<f64> = v.iter().map(|s| s / 86400.0).collect();
6059                Ok(Value::Matrix(Box::new(
6060                    ndarray::Array2::from_shape_vec((rows.len(), 1), rows)
6061                        .map_err(|e| e.to_string())?,
6062                )))
6063            }
6064            _ => {
6065                let s = scalar_arg(&args[0], "days", 1)?;
6066                Ok(Value::Duration(s * 86400.0))
6067            }
6068        },
6069        ("milliseconds", 1) => match &args[0] {
6070            Value::Duration(s) => Ok(Value::Scalar(*s * 1000.0)),
6071            Value::DurationArray(v) => {
6072                let rows: Vec<f64> = v.iter().map(|s| s * 1000.0).collect();
6073                Ok(Value::Matrix(Box::new(
6074                    ndarray::Array2::from_shape_vec((rows.len(), 1), rows)
6075                        .map_err(|e| e.to_string())?,
6076                )))
6077            }
6078            _ => {
6079                let s = scalar_arg(&args[0], "milliseconds", 1)?;
6080                Ok(Value::Duration(s / 1000.0))
6081            }
6082        },
6083        ("years", 1) => match &args[0] {
6084            Value::Duration(s) => Ok(Value::Scalar(*s / (365.2425 * 86400.0))),
6085            Value::DurationArray(v) => {
6086                let rows: Vec<f64> = v.iter().map(|s| s / (365.2425 * 86400.0)).collect();
6087                Ok(Value::Matrix(Box::new(
6088                    ndarray::Array2::from_shape_vec((rows.len(), 1), rows)
6089                        .map_err(|e| e.to_string())?,
6090                )))
6091            }
6092            _ => {
6093                let s = scalar_arg(&args[0], "years", 1)?;
6094                Ok(Value::Duration(s * 365.2425 * 86400.0))
6095            }
6096        },
6097        // duration(H, M, S)
6098        ("duration", 3) => {
6099            let h = scalar_arg(&args[0], "duration", 1)?;
6100            let m = scalar_arg(&args[1], "duration", 2)?;
6101            let s = scalar_arg(&args[2], "duration", 3)?;
6102            Ok(Value::Duration(h * 3600.0 + m * 60.0 + s))
6103        }
6104
6105        // ── Formatting and conversion ─────────────────────────────────────────
6106        ("datestr", 1) => match &args[0] {
6107            Value::DateTime(ts) => {
6108                let s = crate::datetime::format_datestr(*ts, "dd-MMM-yyyy HH:mm:ss");
6109                Ok(Value::Str(s))
6110            }
6111            Value::DateTimeArray(v) => Ok(Value::Cell(Box::new(
6112                v.iter()
6113                    .map(|ts| {
6114                        Value::Str(crate::datetime::format_datestr(*ts, "dd-MMM-yyyy HH:mm:ss"))
6115                    })
6116                    .collect(),
6117            ))),
6118            _ => Err("datestr: argument must be a datetime".to_string()),
6119        },
6120        ("datestr", 2) => {
6121            let fmt_str = match &args[1] {
6122                Value::Str(s) | Value::StringObj(s) => s.clone(),
6123                _ => return Err("datestr: second argument must be a format string".to_string()),
6124            };
6125            match &args[0] {
6126                Value::DateTime(ts) => {
6127                    Ok(Value::Str(crate::datetime::format_datestr(*ts, &fmt_str)))
6128                }
6129                Value::DateTimeArray(v) => Ok(Value::Cell(Box::new(
6130                    v.iter()
6131                        .map(|ts| Value::Str(crate::datetime::format_datestr(*ts, &fmt_str)))
6132                        .collect(),
6133                ))),
6134                _ => Err("datestr: first argument must be a datetime".to_string()),
6135            }
6136        }
6137        ("datevec", 1) => match &args[0] {
6138            Value::DateTime(ts) => {
6139                let (y, mo, d, h, mi, s) = crate::datetime::timestamp_to_civil(*ts);
6140                let sec_i = s.floor() as u32;
6141                let data = vec![
6142                    y as f64,
6143                    mo as f64,
6144                    d as f64,
6145                    h as f64,
6146                    mi as f64,
6147                    sec_i as f64,
6148                ];
6149                Ok(Value::Matrix(Box::new(
6150                    ndarray::Array2::from_shape_vec((1, 6), data).map_err(|e| e.to_string())?,
6151                )))
6152            }
6153            _ => Err("datevec: argument must be a datetime".to_string()),
6154        },
6155        ("datenum", 1) => match &args[0] {
6156            Value::DateTime(ts) => Ok(Value::Scalar(crate::datetime::to_datenum(*ts))),
6157            _ => Err("datenum: argument must be a datetime".to_string()),
6158        },
6159        ("datenum", 3) => {
6160            let y = scalar_arg(&args[0], "datenum", 1)? as i64;
6161            let mo = scalar_arg(&args[1], "datenum", 2)? as u32;
6162            let d = scalar_arg(&args[2], "datenum", 3)? as u32;
6163            let ts = crate::datetime::civil_to_timestamp(y, mo, d, 0, 0, 0.0);
6164            Ok(Value::Scalar(crate::datetime::to_datenum(ts)))
6165        }
6166        ("posixtime", 1) => match &args[0] {
6167            Value::DateTime(ts) => Ok(Value::Scalar(*ts)),
6168            _ => Err("posixtime: argument must be a datetime".to_string()),
6169        },
6170
6171        // ── diff for datetime/duration arrays ─────────────────────────────────
6172        ("diff", 1) => match &args[0] {
6173            Value::DateTimeArray(v) if v.len() >= 2 => {
6174                let diffs: Vec<f64> = v.windows(2).map(|w| w[1] - w[0]).collect();
6175                Ok(Value::DurationArray(diffs))
6176            }
6177            Value::DurationArray(v) if v.len() >= 2 => {
6178                let diffs: Vec<f64> = v.windows(2).map(|w| w[1] - w[0]).collect();
6179                Ok(Value::DurationArray(diffs))
6180            }
6181            Value::Matrix(m) => {
6182                // diff on numeric matrix: successive differences along first non-singleton dim
6183                let (nrows, ncols) = (m.nrows(), m.ncols());
6184                if ncols > 1 && nrows == 1 {
6185                    // Row vector → diff along columns
6186                    let data: Vec<f64> =
6187                        (0..ncols - 1).map(|j| m[[0, j + 1]] - m[[0, j]]).collect();
6188                    Ok(Value::Matrix(Box::new(
6189                        ndarray::Array2::from_shape_vec((1, data.len()), data)
6190                            .map_err(|e| e.to_string())?,
6191                    )))
6192                } else if nrows > 1 {
6193                    // Column vector or matrix → diff along rows
6194                    let data: Vec<f64> = (0..nrows - 1)
6195                        .flat_map(|i| (0..ncols).map(move |j| m[[i + 1, j]] - m[[i, j]]))
6196                        .collect();
6197                    Ok(Value::Matrix(Box::new(
6198                        ndarray::Array2::from_shape_vec((nrows - 1, ncols), data)
6199                            .map_err(|e| e.to_string())?,
6200                    )))
6201                } else {
6202                    Err("diff: input must have at least 2 elements".to_string())
6203                }
6204            }
6205            _ => Err("diff: unsupported argument type".to_string()),
6206        },
6207
6208        // ── Phase 23a — Matrix shape utilities ───────────────────────────────
6209        ("triu", 1) => match &args[0] {
6210            Value::Matrix(m) => {
6211                let mut r = m.clone();
6212                for i in 0..m.nrows() {
6213                    for j in 0..m.ncols() {
6214                        if (j as isize) < (i as isize) {
6215                            r[[i, j]] = 0.0;
6216                        }
6217                    }
6218                }
6219                Ok(Value::Matrix(r))
6220            }
6221            Value::Scalar(n) => Ok(Value::Scalar(*n)),
6222            _ => Err("triu: argument must be a numeric matrix".to_string()),
6223        },
6224        ("triu", 2) => match (&args[0], &args[1]) {
6225            (Value::Matrix(m), Value::Scalar(k)) => {
6226                let k = *k as isize;
6227                let mut r = m.clone();
6228                for i in 0..m.nrows() {
6229                    for j in 0..m.ncols() {
6230                        if (j as isize) - (i as isize) < k {
6231                            r[[i, j]] = 0.0;
6232                        }
6233                    }
6234                }
6235                Ok(Value::Matrix(r))
6236            }
6237            _ => Err("triu: expects (matrix, scalar)".to_string()),
6238        },
6239
6240        ("tril", 1) => match &args[0] {
6241            Value::Matrix(m) => {
6242                let mut r = m.clone();
6243                for i in 0..m.nrows() {
6244                    for j in 0..m.ncols() {
6245                        if (j as isize) > (i as isize) {
6246                            r[[i, j]] = 0.0;
6247                        }
6248                    }
6249                }
6250                Ok(Value::Matrix(r))
6251            }
6252            Value::Scalar(n) => Ok(Value::Scalar(*n)),
6253            _ => Err("tril: argument must be a numeric matrix".to_string()),
6254        },
6255        ("tril", 2) => match (&args[0], &args[1]) {
6256            (Value::Matrix(m), Value::Scalar(k)) => {
6257                let k = *k as isize;
6258                let mut r = m.clone();
6259                for i in 0..m.nrows() {
6260                    for j in 0..m.ncols() {
6261                        if (j as isize) - (i as isize) > k {
6262                            r[[i, j]] = 0.0;
6263                        }
6264                    }
6265                }
6266                Ok(Value::Matrix(r))
6267            }
6268            _ => Err("tril: expects (matrix, scalar)".to_string()),
6269        },
6270
6271        ("repmat", 3) => match (&args[0], &args[1], &args[2]) {
6272            (Value::Matrix(a), Value::Scalar(rm), Value::Scalar(cn)) => {
6273                let rm = *rm as usize;
6274                let cn = *cn as usize;
6275                if rm == 0 || cn == 0 {
6276                    return Ok(Value::Matrix(Box::new(Array2::zeros((0, 0)))));
6277                }
6278                let row_tile: Vec<Array2<f64>> = std::iter::repeat_n(a.view(), cn)
6279                    .map(|v| v.to_owned())
6280                    .collect();
6281                let row_block = ndarray::concatenate(
6282                    ndarray::Axis(1),
6283                    &row_tile.iter().map(|m| m.view()).collect::<Vec<_>>(),
6284                )
6285                .map_err(|e| e.to_string())?;
6286                let col_tiles: Vec<Array2<f64>> = std::iter::repeat_n(row_block.view(), rm)
6287                    .map(|v| v.to_owned())
6288                    .collect();
6289                let result = ndarray::concatenate(
6290                    ndarray::Axis(0),
6291                    &col_tiles.iter().map(|m| m.view()).collect::<Vec<_>>(),
6292                )
6293                .map_err(|e| e.to_string())?;
6294                Ok(Value::Matrix(Box::new(result)))
6295            }
6296            (Value::Scalar(s), Value::Scalar(rm), Value::Scalar(cn)) => {
6297                let rm = *rm as usize;
6298                let cn = *cn as usize;
6299                Ok(Value::Matrix(Box::new(Array2::from_elem((rm, cn), *s))))
6300            }
6301            _ => Err("repmat: expects (matrix, m, n)".to_string()),
6302        },
6303
6304        ("kron", 2) => match (&args[0], &args[1]) {
6305            (Value::Matrix(a), Value::Matrix(b)) => {
6306                let (ra, ca) = (a.nrows(), a.ncols());
6307                let (rb, cb) = (b.nrows(), b.ncols());
6308                let mut result = Array2::<f64>::zeros((ra * rb, ca * cb));
6309                for i in 0..ra {
6310                    for j in 0..ca {
6311                        let aij = a[[i, j]];
6312                        for p in 0..rb {
6313                            for q in 0..cb {
6314                                result[[i * rb + p, j * cb + q]] = aij * b[[p, q]];
6315                            }
6316                        }
6317                    }
6318                }
6319                Ok(Value::Matrix(Box::new(result)))
6320            }
6321            (Value::Scalar(s), Value::Matrix(b)) => Ok(Value::Matrix(Box::new(b.mapv(|x| x * s)))),
6322            (Value::Matrix(a), Value::Scalar(s)) => Ok(Value::Matrix(Box::new(a.mapv(|x| x * s)))),
6323            (Value::Scalar(a), Value::Scalar(b)) => Ok(Value::Scalar(a * b)),
6324            _ => Err("kron: arguments must be numeric matrices".to_string()),
6325        },
6326
6327        // ── Phase 30b — meshgrid ─────────────────────────────────────────────
6328        ("meshgrid", 1) => {
6329            let xv = numeric_vec(&args[0], "meshgrid")?;
6330            let n = xv.len();
6331            let x_mat = Array2::from_shape_fn((n, n), |(_r, c)| xv[c]);
6332            let y_mat = Array2::from_shape_fn((n, n), |(r, _c)| xv[r]);
6333            if get_nargout() >= 2 {
6334                Ok(Value::Tuple(vec![
6335                    Value::Matrix(Box::new(x_mat)),
6336                    Value::Matrix(Box::new(y_mat)),
6337                ]))
6338            } else {
6339                Ok(Value::Matrix(Box::new(x_mat)))
6340            }
6341        }
6342        ("meshgrid", 2) => {
6343            let xv = numeric_vec(&args[0], "meshgrid")?;
6344            let yv = numeric_vec(&args[1], "meshgrid")?;
6345            let n_rows = yv.len();
6346            let n_cols = xv.len();
6347            let x_mat = Array2::from_shape_fn((n_rows, n_cols), |(_r, c)| xv[c]);
6348            let y_mat = Array2::from_shape_fn((n_rows, n_cols), |(r, _c)| yv[r]);
6349            if get_nargout() >= 2 {
6350                Ok(Value::Tuple(vec![
6351                    Value::Matrix(Box::new(x_mat)),
6352                    Value::Matrix(Box::new(y_mat)),
6353                ]))
6354            } else {
6355                Ok(Value::Matrix(Box::new(x_mat)))
6356            }
6357        }
6358
6359        // ── Phase 23b — Vector products ──────────────────────────────────────
6360        ("cross", 2) => {
6361            fn to_vec3(v: &Value, argn: usize) -> Result<[f64; 3], String> {
6362                match v {
6363                    Value::Matrix(m) => {
6364                        let flat: Vec<f64> = m.iter().copied().collect();
6365                        if flat.len() != 3 {
6366                            Err(format!(
6367                                "cross: argument {} must have exactly 3 elements",
6368                                argn
6369                            ))
6370                        } else {
6371                            Ok([flat[0], flat[1], flat[2]])
6372                        }
6373                    }
6374                    _ => Err(format!(
6375                        "cross: argument {} must be a 3-element vector",
6376                        argn
6377                    )),
6378                }
6379            }
6380            let a = to_vec3(&args[0], 1)?;
6381            let b = to_vec3(&args[1], 2)?;
6382            let cx = a[1] * b[2] - a[2] * b[1];
6383            let cy = a[2] * b[0] - a[0] * b[2];
6384            let cz = a[0] * b[1] - a[1] * b[0];
6385            // Result orientation follows first argument
6386            let result = match &args[0] {
6387                Value::Matrix(m) if m.nrows() == 1 => {
6388                    Array2::from_shape_vec((1, 3), vec![cx, cy, cz]).unwrap()
6389                }
6390                _ => Array2::from_shape_vec((3, 1), vec![cx, cy, cz]).unwrap(),
6391            };
6392            Ok(Value::Matrix(Box::new(result)))
6393        }
6394
6395        ("dot", 2) => {
6396            fn to_flat(v: &Value, argn: usize) -> Result<Vec<f64>, String> {
6397                match v {
6398                    Value::Matrix(m) => Ok(m.iter().copied().collect()),
6399                    Value::Scalar(s) => Ok(vec![*s]),
6400                    _ => Err(format!("dot: argument {} must be a numeric vector", argn)),
6401                }
6402            }
6403            let a = to_flat(&args[0], 1)?;
6404            let b = to_flat(&args[1], 2)?;
6405            if a.len() != b.len() {
6406                return Err(format!(
6407                    "dot: vectors must have the same length ({} vs {})",
6408                    a.len(),
6409                    b.len()
6410                ));
6411            }
6412            let s: f64 = a.iter().zip(b.iter()).map(|(x, y)| x * y).sum();
6413            Ok(Value::Scalar(s))
6414        }
6415
6416        // ── Phase 23c — Set operations ────────────────────────────────────────
6417        ("intersect", 2) => {
6418            fn to_sorted_vec(v: &Value, fname: &str) -> Result<Vec<f64>, String> {
6419                match v {
6420                    Value::Matrix(m) => {
6421                        let mut vals: Vec<f64> = m.iter().copied().collect();
6422                        vals.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
6423                        Ok(vals)
6424                    }
6425                    Value::Scalar(s) => Ok(vec![*s]),
6426                    _ => Err(format!("{fname}: arguments must be numeric vectors")),
6427                }
6428            }
6429            let a = to_sorted_vec(&args[0], "intersect")?;
6430            let b = to_sorted_vec(&args[1], "intersect")?;
6431            let b_set: std::collections::HashSet<u64> = b
6432                .iter()
6433                .filter(|x| !x.is_nan())
6434                .map(|x| x.to_bits())
6435                .collect();
6436            let mut result: Vec<f64> = Vec::new();
6437            for x in &a {
6438                if !x.is_nan()
6439                    && b_set.contains(&x.to_bits())
6440                    && result.last().is_none_or(|&last| last != *x)
6441                {
6442                    result.push(*x);
6443                }
6444            }
6445            let n = result.len();
6446            if n == 0 {
6447                Ok(Value::Matrix(Box::new(Array2::zeros((1, 0)))))
6448            } else {
6449                Ok(Value::Matrix(Box::new(
6450                    Array2::from_shape_vec((1, n), result).unwrap(),
6451                )))
6452            }
6453        }
6454
6455        ("union", 2) => {
6456            fn collect_vals(v: &Value, fname: &str) -> Result<Vec<f64>, String> {
6457                match v {
6458                    Value::Matrix(m) => Ok(m.iter().copied().collect()),
6459                    Value::Scalar(s) => Ok(vec![*s]),
6460                    _ => Err(format!("{fname}: arguments must be numeric vectors")),
6461                }
6462            }
6463            let mut combined = collect_vals(&args[0], "union")?;
6464            combined.extend(collect_vals(&args[1], "union")?);
6465            combined.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
6466            let mut result: Vec<f64> = Vec::new();
6467            for x in combined {
6468                if result.last().is_none_or(|&last| last != x) {
6469                    result.push(x);
6470                }
6471            }
6472            let n = result.len();
6473            if n == 0 {
6474                Ok(Value::Matrix(Box::new(Array2::zeros((1, 0)))))
6475            } else {
6476                Ok(Value::Matrix(Box::new(
6477                    Array2::from_shape_vec((1, n), result).unwrap(),
6478                )))
6479            }
6480        }
6481
6482        ("setdiff", 2) => {
6483            fn collect_vals2(v: &Value, fname: &str) -> Result<Vec<f64>, String> {
6484                match v {
6485                    Value::Matrix(m) => Ok(m.iter().copied().collect()),
6486                    Value::Scalar(s) => Ok(vec![*s]),
6487                    _ => Err(format!("{fname}: arguments must be numeric vectors")),
6488                }
6489            }
6490            let a = collect_vals2(&args[0], "setdiff")?;
6491            let b = collect_vals2(&args[1], "setdiff")?;
6492            let b_set: std::collections::HashSet<u64> = b
6493                .iter()
6494                .filter(|x| !x.is_nan())
6495                .map(|x| x.to_bits())
6496                .collect();
6497            let mut a_sorted = a.clone();
6498            a_sorted.sort_by(|x, y| x.partial_cmp(y).unwrap_or(std::cmp::Ordering::Equal));
6499            let mut result: Vec<f64> = Vec::new();
6500            for x in a_sorted {
6501                if !x.is_nan()
6502                    && !b_set.contains(&x.to_bits())
6503                    && result.last().is_none_or(|&last| last != x)
6504                {
6505                    result.push(x);
6506                }
6507            }
6508            let n = result.len();
6509            if n == 0 {
6510                Ok(Value::Matrix(Box::new(Array2::zeros((1, 0)))))
6511            } else {
6512                Ok(Value::Matrix(Box::new(
6513                    Array2::from_shape_vec((1, n), result).unwrap(),
6514                )))
6515            }
6516        }
6517
6518        ("ismember", 2) => {
6519            fn collect_vals3(v: &Value, fname: &str) -> Result<Vec<f64>, String> {
6520                match v {
6521                    Value::Matrix(m) => Ok(m.iter().copied().collect()),
6522                    Value::Scalar(s) => Ok(vec![*s]),
6523                    _ => Err(format!("{fname}: arguments must be numeric")),
6524                }
6525            }
6526            let set: std::collections::HashSet<u64> = collect_vals3(&args[1], "ismember")?
6527                .into_iter()
6528                .filter(|x| !x.is_nan())
6529                .map(|x| x.to_bits())
6530                .collect();
6531            match &args[0] {
6532                Value::Scalar(s) => {
6533                    let found = !s.is_nan() && set.contains(&s.to_bits());
6534                    Ok(Value::Scalar(if found { 1.0 } else { 0.0 }))
6535                }
6536                Value::Matrix(m) => {
6537                    let result: Vec<f64> = m
6538                        .iter()
6539                        .map(|x| {
6540                            if !x.is_nan() && set.contains(&x.to_bits()) {
6541                                1.0
6542                            } else {
6543                                0.0
6544                            }
6545                        })
6546                        .collect();
6547                    let shape = m.raw_dim();
6548                    Ok(Value::Matrix(Box::new(
6549                        Array2::from_shape_vec(shape, result).unwrap(),
6550                    )))
6551                }
6552                _ => Err("ismember: first argument must be numeric".to_string()),
6553            }
6554        }
6555
6556        // ── Phase 23d — Index utilities and element repetition ────────────────
6557        ("sub2ind", 3) => {
6558            let sz = match &args[0] {
6559                Value::Matrix(m) if m.len() == 2 => (m[[0, 0]] as usize, m[[0, 1]] as usize),
6560                _ => return Err("sub2ind: first argument must be [rows cols]".to_string()),
6561            };
6562            let rows = sz.0;
6563            fn idx_vals(v: &Value, argn: usize) -> Result<Vec<f64>, String> {
6564                match v {
6565                    Value::Scalar(s) => Ok(vec![*s]),
6566                    Value::Matrix(m) => Ok(m.iter().copied().collect()),
6567                    _ => Err(format!("sub2ind: argument {} must be numeric", argn)),
6568                }
6569            }
6570            let r = idx_vals(&args[1], 2)?;
6571            let c = idx_vals(&args[2], 3)?;
6572            if r.len() != c.len() {
6573                return Err(
6574                    "sub2ind: row and column index vectors must have the same length".to_string(),
6575                );
6576            }
6577            if r.len() == 1 {
6578                let idx = (c[0] as usize - 1) * rows + r[0] as usize;
6579                Ok(Value::Scalar(idx as f64))
6580            } else {
6581                let vals: Vec<f64> = r
6582                    .iter()
6583                    .zip(c.iter())
6584                    .map(|(&ri, &ci)| ((ci as usize - 1) * rows + ri as usize) as f64)
6585                    .collect();
6586                let n = vals.len();
6587                Ok(Value::Matrix(Box::new(
6588                    Array2::from_shape_vec((1, n), vals).unwrap(),
6589                )))
6590            }
6591        }
6592
6593        ("ind2sub", 2) => {
6594            let sz = match &args[0] {
6595                Value::Matrix(m) if m.len() == 2 => (m[[0, 0]] as usize, m[[0, 1]] as usize),
6596                _ => return Err("ind2sub: first argument must be [rows cols]".to_string()),
6597            };
6598            let rows = sz.0;
6599            fn idx_vals2(v: &Value, argn: usize) -> Result<Vec<f64>, String> {
6600                match v {
6601                    Value::Scalar(s) => Ok(vec![*s]),
6602                    Value::Matrix(m) => Ok(m.iter().copied().collect()),
6603                    _ => Err(format!("ind2sub: argument {} must be numeric", argn)),
6604                }
6605            }
6606            let indices = idx_vals2(&args[1], 2)?;
6607            if indices.len() == 1 {
6608                let idx = indices[0] as usize;
6609                let r = ((idx - 1) % rows + 1) as f64;
6610                let c = ((idx - 1) / rows + 1) as f64;
6611                Ok(Value::Tuple(vec![Value::Scalar(r), Value::Scalar(c)]))
6612            } else {
6613                let n = indices.len();
6614                let rs: Vec<f64> = indices
6615                    .iter()
6616                    .map(|&idx| ((idx as usize - 1) % rows + 1) as f64)
6617                    .collect();
6618                let cs: Vec<f64> = indices
6619                    .iter()
6620                    .map(|&idx| ((idx as usize - 1) / rows + 1) as f64)
6621                    .collect();
6622                let rm = Value::Matrix(Box::new(Array2::from_shape_vec((1, n), rs).unwrap()));
6623                let cm = Value::Matrix(Box::new(Array2::from_shape_vec((1, n), cs).unwrap()));
6624                Ok(Value::Tuple(vec![rm, cm]))
6625            }
6626        }
6627
6628        ("repelem", 2) => match (&args[0], &args[1]) {
6629            (Value::Matrix(a), Value::Scalar(n)) => {
6630                let n = *n as usize;
6631                let flat: Vec<f64> = a.iter().flat_map(|&x| std::iter::repeat_n(x, n)).collect();
6632                let total = flat.len();
6633                Ok(Value::Matrix(Box::new(
6634                    Array2::from_shape_vec((1, total), flat).unwrap(),
6635                )))
6636            }
6637            (Value::Matrix(a), Value::Matrix(ns)) => {
6638                let av: Vec<f64> = a.iter().copied().collect();
6639                let nv: Vec<f64> = ns.iter().copied().collect();
6640                if av.len() != nv.len() {
6641                    return Err(
6642                        "repelem: element count vector must match source vector length".to_string(),
6643                    );
6644                }
6645                let flat: Vec<f64> = av
6646                    .iter()
6647                    .zip(nv.iter())
6648                    .flat_map(|(&x, &n)| std::iter::repeat_n(x, n as usize))
6649                    .collect();
6650                let total = flat.len();
6651                Ok(Value::Matrix(Box::new(
6652                    Array2::from_shape_vec((1, total), flat).unwrap(),
6653                )))
6654            }
6655            (Value::Scalar(s), Value::Scalar(n)) => {
6656                let n = *n as usize;
6657                Ok(Value::Matrix(Box::new(Array2::from_elem((1, n), *s))))
6658            }
6659            _ => Err("repelem: unsupported argument types".to_string()),
6660        },
6661        ("repelem", 3) => match (&args[0], &args[1], &args[2]) {
6662            (Value::Matrix(a), Value::Scalar(rm), Value::Scalar(cn)) => {
6663                let rm = *rm as usize;
6664                let cn = *cn as usize;
6665                let (nrows, ncols) = (a.nrows(), a.ncols());
6666                let mut result = Array2::<f64>::zeros((nrows * rm, ncols * cn));
6667                for i in 0..nrows {
6668                    for j in 0..ncols {
6669                        let v = a[[i, j]];
6670                        for di in 0..rm {
6671                            for dj in 0..cn {
6672                                result[[i * rm + di, j * cn + dj]] = v;
6673                            }
6674                        }
6675                    }
6676                }
6677                Ok(Value::Matrix(Box::new(result)))
6678            }
6679            (Value::Scalar(s), Value::Scalar(rm), Value::Scalar(cn)) => Ok(Value::Matrix(
6680                Box::new(Array2::from_elem((*rm as usize, *cn as usize), *s)),
6681            )),
6682            _ => Err("repelem: expects (matrix, m, n) for 2D repetition".to_string()),
6683        },
6684
6685        // ── Phase 24a — Polynomial evaluation, fitting, and roots ────────────
6686        ("polyval", 2) => {
6687            let coeffs = poly_coeffs(&args[0], "polyval")?;
6688            if coeffs.is_empty() {
6689                return Err("polyval: polynomial vector is empty".to_string());
6690            }
6691            match &args[1] {
6692                Value::Scalar(x) => Ok(Value::Scalar(horner(&coeffs, *x))),
6693                Value::Matrix(m) => Ok(Value::Matrix(Box::new(m.mapv(|x| horner(&coeffs, x))))),
6694                _ => Err("polyval: second argument must be a real numeric value".to_string()),
6695            }
6696        }
6697
6698        ("polyfit", 3) => {
6699            let xv = poly_coeffs(&args[0], "polyfit")?;
6700            let yv = poly_coeffs(&args[1], "polyfit")?;
6701            let deg = match &args[2] {
6702                Value::Scalar(n) => {
6703                    let d = *n as usize;
6704                    if *n < 0.0 || (*n - d as f64).abs() > 1e-9 {
6705                        return Err("polyfit: degree must be a non-negative integer".to_string());
6706                    }
6707                    d
6708                }
6709                _ => return Err("polyfit: degree must be a scalar".to_string()),
6710            };
6711            if xv.len() != yv.len() {
6712                return Err("polyfit: x and y must have the same length".to_string());
6713            }
6714            let m = xv.len();
6715            let ncols = deg + 1;
6716            if ncols > m {
6717                return Err(format!(
6718                    "polyfit: not enough data points ({m}) for degree-{deg} fit"
6719                ));
6720            }
6721            // Build Vandermonde matrix (m × ncols), highest power first
6722            let mut vander = Array2::<f64>::zeros((m, ncols));
6723            for (i, &xi) in xv.iter().enumerate() {
6724                for j in 0..ncols {
6725                    vander[[i, j]] = xi.powi((deg - j) as i32);
6726                }
6727            }
6728            // Solve via QR: V*c = y  →  Q*R*c = y  →  R*c = Q^T*y
6729            let (q, r) = qr_decompose(&vander)?;
6730            let qty: Vec<f64> = (0..ncols)
6731                .map(|i| (0..m).map(|k| q[[k, i]] * yv[k]).sum())
6732                .collect();
6733            // Extract upper-left ncols×ncols block of R
6734            let mut r_sq = Array2::<f64>::zeros((ncols, ncols));
6735            for i in 0..ncols {
6736                for j in 0..ncols {
6737                    r_sq[[i, j]] = r[[i, j]];
6738                }
6739            }
6740            let coeffs = poly_back_sub(&r_sq, &qty)?;
6741            let result = Array2::from_shape_vec((1, ncols), coeffs)
6742                .map_err(|e| format!("polyfit: internal error: {e}"))?;
6743            Ok(Value::Matrix(Box::new(result)))
6744        }
6745
6746        ("roots", 1) => {
6747            let raw = poly_coeffs(&args[0], "roots")?;
6748            // Strip leading zeros
6749            let start = raw.iter().position(|&c| c != 0.0).unwrap_or(raw.len());
6750            let coeffs = &raw[start..];
6751            if coeffs.len() <= 1 {
6752                return Ok(Value::Matrix(Box::new(Array2::zeros((0, 1)))));
6753            }
6754            let roots = durand_kerner(coeffs)?;
6755            Ok(roots_to_value(&roots))
6756        }
6757
6758        ("poly", 1) => match &args[0] {
6759            Value::Scalar(r) => {
6760                let data = vec![1.0, -*r];
6761                Ok(Value::Matrix(Box::new(
6762                    Array2::from_shape_vec((1, 2), data).unwrap(),
6763                )))
6764            }
6765            Value::Matrix(m) => {
6766                if m.nrows() == 1 || m.ncols() == 1 {
6767                    // Root vector: expand (x − r_1)(x − r_2)…
6768                    let roots: Vec<f64> = if m.nrows() == 1 {
6769                        m.row(0).iter().copied().collect()
6770                    } else {
6771                        m.column(0).iter().copied().collect()
6772                    };
6773                    let mut p = vec![1.0_f64];
6774                    for &r in &roots {
6775                        p = poly_conv(&p, &[1.0, -r]);
6776                    }
6777                    let ncols = p.len();
6778                    Ok(Value::Matrix(Box::new(
6779                        Array2::from_shape_vec((1, ncols), p).unwrap(),
6780                    )))
6781                } else {
6782                    // Square matrix: characteristic polynomial via Faddeev-LeVerrier
6783                    let coeffs = characteristic_poly(m)?;
6784                    let ncols = coeffs.len();
6785                    Ok(Value::Matrix(Box::new(
6786                        Array2::from_shape_vec((1, ncols), coeffs).unwrap(),
6787                    )))
6788                }
6789            }
6790            _ => Err("poly: argument must be a numeric vector or square matrix".to_string()),
6791        },
6792
6793        // ── Phase 24b — Convolution, deconvolution, interpolation ────────────
6794        ("conv", 2) => {
6795            let a = poly_coeffs(&args[0], "conv")?;
6796            let b = poly_coeffs(&args[1], "conv")?;
6797            if a.is_empty() || b.is_empty() {
6798                return Ok(Value::Matrix(Box::new(Array2::zeros((1, 0)))));
6799            }
6800            let c = poly_conv(&a, &b);
6801            let len = c.len();
6802            Ok(Value::Matrix(Box::new(
6803                Array2::from_shape_vec((1, len), c).unwrap(),
6804            )))
6805        }
6806
6807        ("deconv", 2) => {
6808            let c = poly_coeffs(&args[0], "deconv")?;
6809            let b = poly_coeffs(&args[1], "deconv")?;
6810            let (q, r) = poly_deconv(&c, &b)?;
6811            let qn = q.len();
6812            let rn = r.len();
6813            let q_val = Value::Matrix(Box::new(Array2::from_shape_vec((1, qn), q).unwrap()));
6814            let r_val = Value::Matrix(Box::new(Array2::from_shape_vec((1, rn), r).unwrap()));
6815            Ok(Value::Tuple(vec![q_val, r_val]))
6816        }
6817
6818        ("interp1", 3) => {
6819            let xv = poly_coeffs(&args[0], "interp1")?;
6820            let yv = poly_coeffs(&args[1], "interp1")?;
6821            if xv.len() != yv.len() {
6822                return Err("interp1: x and y must have the same length".to_string());
6823            }
6824            if xv.len() < 2 {
6825                return Err("interp1: requires at least two knot points".to_string());
6826            }
6827            match &args[2] {
6828                Value::Scalar(xi) => Ok(Value::Scalar(interp1_at(&xv, &yv, *xi, "linear"))),
6829                Value::Matrix(xi_m) => Ok(Value::Matrix(Box::new(
6830                    xi_m.mapv(|xi| interp1_at(&xv, &yv, xi, "linear")),
6831                ))),
6832                _ => Err("interp1: query points must be numeric".to_string()),
6833            }
6834        }
6835
6836        ("interp1", 4) => {
6837            let xv = poly_coeffs(&args[0], "interp1")?;
6838            let yv = poly_coeffs(&args[1], "interp1")?;
6839            let method = match &args[3] {
6840                Value::Str(s) | Value::StringObj(s) => s.clone(),
6841                _ => return Err("interp1: method argument must be a string".to_string()),
6842            };
6843            if !matches!(method.as_str(), "linear" | "nearest" | "previous" | "next") {
6844                return Err(format!(
6845                    "interp1: unknown method '{method}'; supported: linear nearest previous next"
6846                ));
6847            }
6848            if xv.len() != yv.len() {
6849                return Err("interp1: x and y must have the same length".to_string());
6850            }
6851            if xv.len() < 2 {
6852                return Err("interp1: requires at least two knot points".to_string());
6853            }
6854            match &args[2] {
6855                Value::Scalar(xi) => Ok(Value::Scalar(interp1_at(&xv, &yv, *xi, &method))),
6856                Value::Matrix(xi_m) => {
6857                    let m_str = method.as_str();
6858                    Ok(Value::Matrix(Box::new(
6859                        xi_m.mapv(|xi| interp1_at(&xv, &yv, xi, m_str)),
6860                    )))
6861                }
6862                _ => Err("interp1: query points must be numeric".to_string()),
6863            }
6864        }
6865
6866        // ── 25b: tic / toc ────────────────────────────────────────────────────
6867        ("tic", 0) => {
6868            TIC_TIME.with(|t| t.set(Some(std::time::Instant::now())));
6869            Ok(Value::Void)
6870        }
6871        ("toc", 0) => {
6872            let elapsed = TIC_TIME.with(|t| t.get().map(|s| s.elapsed().as_secs_f64()));
6873            match elapsed {
6874                Some(t) => Ok(Value::Scalar(t)),
6875                None => Err("toc: tic must be called before toc".to_string()),
6876            }
6877        }
6878
6879        // ── 25a: eval (expression context — env mutations do not persist) ────
6880        ("eval", 1) => {
6881            let code = match &args[0] {
6882                Value::Str(s) | Value::StringObj(s) => s.clone(),
6883                _ => return Err("eval: argument must be a string".to_string()),
6884            };
6885            call_eval_str_hook(&code, env)
6886        }
6887        ("eval", 2) => {
6888            let code = match &args[0] {
6889                Value::Str(s) | Value::StringObj(s) => s.clone(),
6890                _ => return Err("eval: argument must be a string".to_string()),
6891            };
6892            match call_eval_str_hook(&code, env) {
6893                Err(e) => {
6894                    set_last_err(&e);
6895                    let catch = match &args[1] {
6896                        Value::Str(s) | Value::StringObj(s) => s.clone(),
6897                        _ => return Err("eval: catch argument must be a string".to_string()),
6898                    };
6899                    call_eval_str_hook(&catch, env)
6900                }
6901                ok => ok,
6902            }
6903        }
6904
6905        _ => {
6906            let hint = suggest_similar(name, env);
6907            match hint {
6908                Some(s) => Err(format!("Unknown function '{name}'; did you mean '{s}'?")),
6909                None => Err(format!("Unknown function: '{name}'")),
6910            }
6911        }
6912    }
6913}
6914
6915/// Interprets backslash escape sequences in delimiter strings.
6916/// `\t` → tab, `\n` → newline. Other strings are used as-is.
6917fn interpret_delim(s: &str) -> String {
6918    match s {
6919        r"\t" => "\t".to_string(),
6920        r"\n" => "\n".to_string(),
6921        other => other.to_string(),
6922    }
6923}
6924
6925/// Returns true if splitting every line by `delim` gives the same field count > 1.
6926fn delim_consistent(lines: &[&str], delim: char) -> bool {
6927    let counts: Vec<usize> = lines.iter().map(|l| l.split(delim).count()).collect();
6928    counts.iter().all(|&c| c > 1) && counts.windows(2).all(|w| w[0] == w[1])
6929}
6930
6931/// Reads a delimiter-separated numeric file and returns a `Value::Matrix`.
6932fn dlmread_impl(path: &str, explicit_delim: Option<String>) -> Result<Value, String> {
6933    let content =
6934        std::fs::read_to_string(path).map_err(|e| format!("dlmread: cannot read '{path}': {e}"))?;
6935
6936    let lines: Vec<&str> = content.lines().filter(|l| !l.trim().is_empty()).collect();
6937
6938    if lines.is_empty() {
6939        return Ok(Value::Matrix(Box::new(Array2::zeros((0, 0)))));
6940    }
6941
6942    // Determine delimiter: explicit → auto-detect (comma → tab → whitespace)
6943    let delim: Option<String> = match explicit_delim {
6944        Some(d) => Some(d),
6945        None => {
6946            if delim_consistent(&lines, ',') {
6947                Some(",".to_string())
6948            } else if delim_consistent(&lines, '\t') {
6949                Some("\t".to_string())
6950            } else {
6951                None // split by whitespace
6952            }
6953        }
6954    };
6955
6956    let mut rows: Vec<Vec<f64>> = Vec::new();
6957    for (line_num, line) in lines.iter().enumerate() {
6958        let fields: Vec<&str> = match &delim {
6959            Some(d) => line.split(d.as_str()).collect(),
6960            None => line.split_whitespace().collect(),
6961        };
6962        let mut row_vals: Vec<f64> = Vec::with_capacity(fields.len());
6963        for field in &fields {
6964            let trimmed = field.trim();
6965            if trimmed.is_empty() {
6966                row_vals.push(0.0);
6967            } else {
6968                row_vals.push(trimmed.parse::<f64>().map_err(|_| {
6969                    format!(
6970                        "dlmread: non-numeric value '{trimmed}' on line {}",
6971                        line_num + 1
6972                    )
6973                })?);
6974            }
6975        }
6976        if !row_vals.is_empty() {
6977            rows.push(row_vals);
6978        }
6979    }
6980
6981    if rows.is_empty() {
6982        return Ok(Value::Matrix(Box::new(Array2::zeros((0, 0)))));
6983    }
6984
6985    let ncols = rows[0].len();
6986    for (i, row) in rows.iter().enumerate() {
6987        if row.len() != ncols {
6988            return Err(format!(
6989                "dlmread: row {} has {} fields, expected {ncols}",
6990                i + 1,
6991                row.len()
6992            ));
6993        }
6994    }
6995
6996    let nrows = rows.len();
6997    let flat: Vec<f64> = rows.into_iter().flatten().collect();
6998    Array2::from_shape_vec((nrows, ncols), flat)
6999        .map_err(|e| format!("dlmread: shape error: {e}"))
7000        .map(|m| Value::Matrix(Box::new(m)))
7001}
7002
7003/// Formats one f64 value for use in a delimited file.
7004/// Integers are written without decimal point; floats use full precision.
7005fn fmt_dlm_number(n: f64) -> String {
7006    if n.is_finite() && n == n.trunc() && n.abs() < 1e15 {
7007        format!("{}", n as i64)
7008    } else {
7009        format!("{n}")
7010    }
7011}
7012
7013/// Writes a scalar or matrix to a delimiter-separated file.
7014fn dlmwrite_impl(path: &str, val: &Value, explicit_delim: Option<String>) -> Result<Value, String> {
7015    let delim = explicit_delim.unwrap_or_else(|| ",".to_string());
7016
7017    let content = match val {
7018        Value::Scalar(n) => format!("{}\n", fmt_dlm_number(*n)),
7019        Value::Matrix(m) => {
7020            let mut out = String::new();
7021            for row in m.rows() {
7022                let parts: Vec<String> = row.iter().map(|n| fmt_dlm_number(*n)).collect();
7023                out.push_str(&parts.join(&delim));
7024                out.push('\n');
7025            }
7026            out
7027        }
7028        _ => {
7029            return Err("dlmwrite: second argument must be a numeric scalar or matrix".to_string());
7030        }
7031    };
7032
7033    std::fs::write(path, content).map_err(|e| format!("dlmwrite: cannot write '{path}': {e}"))?;
7034    Ok(Value::Void)
7035}
7036
7037// --- CSV read/write helpers (readmatrix / readtable / writetable) ---
7038
7039/// Selects the delimiter shared across all lines; falls back to `None` (whitespace splitting).
7040///
7041/// Uses CSV-aware splitting (quoting) when checking for comma consistency.
7042fn auto_detect_delim(lines: &[&str]) -> Option<String> {
7043    // Comma: use CSV-aware split so quoted fields with commas don't confuse the count.
7044    let comma_counts: Vec<usize> = lines.iter().map(|l| split_csv_row(l, ",").len()).collect();
7045    if comma_counts.iter().all(|&c| c > 1) && comma_counts.windows(2).all(|w| w[0] == w[1]) {
7046        return Some(",".to_string());
7047    }
7048    if delim_consistent(lines, '\t') {
7049        Some("\t".to_string())
7050    } else {
7051        None
7052    }
7053}
7054
7055/// Splits one CSV line by `delim`, respecting RFC 4180 double-quoted fields.
7056/// `""` inside a quoted field encodes a literal `"`.
7057/// Falls back to a plain `str::split` for multi-character delimiters.
7058fn split_csv_row(line: &str, delim: &str) -> Vec<String> {
7059    if delim.chars().count() != 1 {
7060        return line.split(delim).map(str::to_string).collect();
7061    }
7062    let delim_char = delim.chars().next().unwrap();
7063    let chars: Vec<char> = line.chars().collect();
7064    let mut fields: Vec<String> = Vec::new();
7065    let mut field = String::new();
7066    let mut i = 0;
7067    let mut in_quotes = false;
7068    while i < chars.len() {
7069        let c = chars[i];
7070        if in_quotes {
7071            if c == '"' && i + 1 < chars.len() && chars[i + 1] == '"' {
7072                field.push('"');
7073                i += 2;
7074                continue;
7075            } else if c == '"' {
7076                in_quotes = false;
7077            } else {
7078                field.push(c);
7079            }
7080        } else if c == '"' {
7081            in_quotes = true;
7082        } else if c == delim_char {
7083            fields.push(std::mem::take(&mut field));
7084        } else {
7085            field.push(c);
7086        }
7087        i += 1;
7088    }
7089    fields.push(field);
7090    fields
7091}
7092
7093/// Splits a CSV row with an optional delimiter; `None` splits by whitespace.
7094fn split_csv_row_opt(line: &str, delim: &Option<String>) -> Vec<String> {
7095    match delim {
7096        None => line.split_whitespace().map(str::to_string).collect(),
7097        Some(d) => split_csv_row(line, d),
7098    }
7099}
7100
7101/// Returns `true` if any non-empty field in `fields` cannot be parsed as `f64`.
7102fn row_is_header(fields: &[String]) -> bool {
7103    fields
7104        .iter()
7105        .any(|f| !f.trim().is_empty() && f.trim().parse::<f64>().is_err())
7106}
7107
7108/// Converts a raw header string to a valid identifier-like name.
7109/// Runs of non-alphanumeric characters collapse to `_`; a leading digit gets an `x` prefix.
7110/// Empty results fall back to `x{col}`.
7111fn sanitize_header(s: &str, col_1based: usize) -> String {
7112    let s = s.trim();
7113    if s.is_empty() {
7114        return format!("x{col_1based}");
7115    }
7116    let mut out = String::new();
7117    for c in s.chars() {
7118        if c.is_alphanumeric() || c == '_' {
7119            out.push(c);
7120        } else if !out.ends_with('_') {
7121            out.push('_');
7122        }
7123    }
7124    let out = out.trim_end_matches('_').to_string();
7125    if out.is_empty() {
7126        return format!("x{col_1based}");
7127    }
7128    if out.chars().next().unwrap().is_ascii_digit() {
7129        format!("x{out}")
7130    } else {
7131        out
7132    }
7133}
7134
7135/// Appends `_N` (1-based) suffixes to duplicate entries in a header list.
7136/// Note: collisions between deduplicated names and pre-existing `_N` names are not resolved.
7137fn deduplicate_headers(headers: Vec<String>) -> Vec<String> {
7138    let mut count: HashMap<String, usize> = HashMap::new();
7139    for h in &headers {
7140        *count.entry(h.clone()).or_insert(0) += 1;
7141    }
7142    let mut seen: HashMap<String, usize> = HashMap::new();
7143    headers
7144        .into_iter()
7145        .map(|h| {
7146            if *count.get(&h).unwrap() == 1 {
7147                h
7148            } else {
7149                let idx = seen.entry(h.clone()).or_insert(0);
7150                *idx += 1;
7151                format!("{h}_{idx}")
7152            }
7153        })
7154        .collect()
7155}
7156
7157/// Parses an optional `('Delimiter', d)` argument pair starting at `args[start]`.
7158/// Returns `Ok(None)` when no extra arguments are present.
7159fn parse_delimiter_opt(
7160    fn_name: &str,
7161    args: &[Value],
7162    start: usize,
7163) -> Result<Option<String>, String> {
7164    if args.len() <= start {
7165        return Ok(None);
7166    }
7167    let key = string_arg(&args[start], fn_name, start + 1)?;
7168    if !key.eq_ignore_ascii_case("delimiter") {
7169        return Err(format!(
7170            "{fn_name}: expected 'Delimiter' option at argument {}, got '{key}'",
7171            start + 1
7172        ));
7173    }
7174    if args.len() <= start + 1 {
7175        return Err(format!("{fn_name}: 'Delimiter' option requires a value"));
7176    }
7177    let val = interpret_delim(string_arg(&args[start + 1], fn_name, start + 2)?);
7178    Ok(Some(val))
7179}
7180
7181/// Reads a delimiter-separated file and returns a [`Value::Matrix`].
7182///
7183/// Auto-detects the delimiter (comma → tab → whitespace). When the first row contains
7184/// non-numeric text it is treated as a header and skipped. Empty cells become `NaN`
7185/// (unlike [`dlmread_impl`], which uses `0.0`).
7186fn readmatrix_impl(path: &str, explicit_delim: Option<String>) -> Result<Value, String> {
7187    let content = std::fs::read_to_string(path)
7188        .map_err(|e| format!("readmatrix: cannot read '{path}': {e}"))?;
7189
7190    let lines: Vec<&str> = content.lines().filter(|l| !l.trim().is_empty()).collect();
7191    if lines.is_empty() {
7192        return Ok(Value::Matrix(Box::new(Array2::<f64>::zeros((0, 0)))));
7193    }
7194
7195    let delim = match explicit_delim {
7196        Some(d) => Some(d),
7197        None => auto_detect_delim(&lines),
7198    };
7199
7200    let first_fields = split_csv_row_opt(lines[0], &delim);
7201    let skip_header = row_is_header(&first_fields);
7202    let data_lines = if skip_header { &lines[1..] } else { &lines[..] };
7203
7204    if data_lines.is_empty() {
7205        return Ok(Value::Matrix(Box::new(Array2::<f64>::zeros((0, 0)))));
7206    }
7207
7208    let mut rows: Vec<Vec<f64>> = Vec::new();
7209    for (i, line) in data_lines.iter().enumerate() {
7210        let fields = split_csv_row_opt(line, &delim);
7211        let mut row: Vec<f64> = Vec::with_capacity(fields.len());
7212        for f in &fields {
7213            let t = f.trim();
7214            if t.is_empty() {
7215                row.push(f64::NAN);
7216            } else {
7217                row.push(t.parse::<f64>().map_err(|_| {
7218                    format!(
7219                        "readmatrix: non-numeric value '{t}' on line {}",
7220                        i + 1 + usize::from(skip_header)
7221                    )
7222                })?);
7223            }
7224        }
7225        rows.push(row);
7226    }
7227
7228    if rows.is_empty() {
7229        return Ok(Value::Matrix(Box::new(Array2::<f64>::zeros((0, 0)))));
7230    }
7231
7232    let ncols = rows[0].len();
7233    for (i, row) in rows.iter().enumerate() {
7234        if row.len() != ncols {
7235            return Err(format!(
7236                "readmatrix: row {} has {} fields, expected {ncols}",
7237                i + 1,
7238                row.len()
7239            ));
7240        }
7241    }
7242
7243    let nrows = rows.len();
7244    let flat: Vec<f64> = rows.into_iter().flatten().collect();
7245    Array2::from_shape_vec((nrows, ncols), flat)
7246        .map_err(|e| format!("readmatrix: shape error: {e}"))
7247        .map(|m| Value::Matrix(Box::new(m)))
7248}
7249
7250/// Reads a delimiter-separated file with a header row and returns a [`Value::Struct`] of columns.
7251///
7252/// The first row is always treated as column headers. Numeric columns become `Matrix` (N×1);
7253/// columns with any non-numeric cell become `Cell` of [`Value::Str`].
7254/// Whitespace is trimmed from all cell values after CSV unquoting.
7255fn readtable_impl(path: &str, explicit_delim: Option<String>) -> Result<Value, String> {
7256    let content = std::fs::read_to_string(path)
7257        .map_err(|e| format!("readtable: cannot read '{path}': {e}"))?;
7258
7259    let lines: Vec<&str> = content.lines().filter(|l| !l.trim().is_empty()).collect();
7260    if lines.is_empty() {
7261        return Ok(Value::Struct(Box::new(IndexMap::new())));
7262    }
7263
7264    let delim = match explicit_delim {
7265        Some(d) => Some(d),
7266        None => auto_detect_delim(&lines),
7267    };
7268
7269    let raw_headers = split_csv_row_opt(lines[0], &delim);
7270    let ncols = raw_headers.len();
7271    let headers: Vec<String> = deduplicate_headers(
7272        raw_headers
7273            .iter()
7274            .enumerate()
7275            .map(|(i, h)| sanitize_header(h.trim(), i + 1))
7276            .collect(),
7277    );
7278
7279    let data_lines = &lines[1..];
7280    if data_lines.is_empty() {
7281        let mut s: IndexMap<String, Value> = IndexMap::new();
7282        for h in &headers {
7283            s.insert(
7284                h.clone(),
7285                Value::Matrix(Box::new(Array2::<f64>::zeros((0, 1)))),
7286            );
7287        }
7288        return Ok(Value::Struct(Box::new(s)));
7289    }
7290
7291    let mut all_rows: Vec<Vec<String>> = Vec::new();
7292    for (i, line) in data_lines.iter().enumerate() {
7293        let fields = split_csv_row_opt(line, &delim);
7294        if fields.len() != ncols {
7295            return Err(format!(
7296                "readtable: row {} has {} fields, expected {ncols}",
7297                i + 2,
7298                fields.len()
7299            ));
7300        }
7301        all_rows.push(fields.into_iter().map(|f| f.trim().to_string()).collect());
7302    }
7303
7304    let nrows = all_rows.len();
7305    let mut s: IndexMap<String, Value> = IndexMap::new();
7306    for col in 0..ncols {
7307        let all_numeric = all_rows.iter().all(|row| {
7308            let t = row[col].as_str();
7309            t.is_empty() || t.parse::<f64>().is_ok()
7310        });
7311        if all_numeric {
7312            let vals: Vec<f64> = all_rows
7313                .iter()
7314                .map(|row| {
7315                    let t = row[col].as_str();
7316                    if t.is_empty() {
7317                        f64::NAN
7318                    } else {
7319                        t.parse::<f64>().unwrap()
7320                    }
7321                })
7322                .collect();
7323            let col_mat = Array2::from_shape_vec((nrows, 1), vals)
7324                .map_err(|e| format!("readtable: shape error: {e}"))?;
7325            s.insert(headers[col].clone(), Value::Matrix(Box::new(col_mat)));
7326        } else {
7327            let vals: Vec<Value> = all_rows
7328                .iter()
7329                .map(|row| Value::Str(row[col].clone()))
7330                .collect();
7331            s.insert(headers[col].clone(), Value::Cell(Box::new(vals)));
7332        }
7333    }
7334    Ok(Value::Struct(Box::new(s)))
7335}
7336
7337/// Quotes a CSV cell if it contains the delimiter, a double-quote, or a newline (RFC 4180).
7338fn csv_quote_cell(s: &str, delim: &str) -> String {
7339    if s.contains('"') || s.contains('\n') || s.contains(delim) {
7340        let escaped = s.replace('"', "\"\"");
7341        format!("\"{escaped}\"")
7342    } else {
7343        s.to_string()
7344    }
7345}
7346
7347/// Returns the number of rows in a struct column value.
7348///
7349/// Accepts `Matrix` (N×1), `Cell`, `Scalar`, and `Str`/`StringObj` (1 row each).
7350/// Returns `None` for unsupported types or non-column matrices.
7351fn col_nrows(v: &Value) -> Option<usize> {
7352    match v {
7353        Value::Matrix(m) if m.ncols() == 1 || m.nrows() == 0 => Some(m.nrows()),
7354        Value::Cell(c) => Some(c.len()),
7355        Value::Scalar(_) => Some(1),
7356        Value::Str(_) | Value::StringObj(_) => Some(1),
7357        _ => None,
7358    }
7359}
7360
7361/// Returns the formatted CSV cell string for row `row` of a struct column value.
7362fn col_cell_str(v: &Value, row: usize, delim: &str) -> Result<String, String> {
7363    match v {
7364        Value::Matrix(m) => Ok(csv_quote_cell(&fmt_dlm_number(m[[row, 0]]), delim)),
7365        Value::Cell(c) => match &c[row] {
7366            Value::Str(s) | Value::StringObj(s) => Ok(csv_quote_cell(s, delim)),
7367            Value::Scalar(n) => Ok(csv_quote_cell(&fmt_dlm_number(*n), delim)),
7368            _ => Err(format!(
7369                "writetable: cell element at row {} has unsupported type",
7370                row + 1
7371            )),
7372        },
7373        Value::Scalar(n) => Ok(csv_quote_cell(&fmt_dlm_number(*n), delim)),
7374        Value::Str(s) | Value::StringObj(s) => Ok(csv_quote_cell(s, delim)),
7375        _ => Err(format!(
7376            "writetable: unsupported column type at row {}",
7377            row + 1
7378        )),
7379    }
7380}
7381
7382/// Writes a struct table to a delimiter-separated file with a header row.
7383///
7384/// Each struct field is one column. All fields must have the same number of rows.
7385/// Accepts `Matrix` (N×1), `Cell`, `Scalar`, and `Str`/`StringObj` columns.
7386/// Cell values that contain the delimiter, `"`, or newlines are quoted per RFC 4180.
7387fn writetable_impl(
7388    tbl: &Value,
7389    path: &str,
7390    explicit_delim: Option<String>,
7391) -> Result<Value, String> {
7392    let delim = explicit_delim.unwrap_or_else(|| ",".to_string());
7393    let fields = match tbl {
7394        Value::Struct(m) => m,
7395        _ => return Err("writetable: first argument must be a struct".to_string()),
7396    };
7397    if fields.is_empty() {
7398        std::fs::write(path, "").map_err(|e| format!("writetable: cannot write '{path}': {e}"))?;
7399        return Ok(Value::Void);
7400    }
7401
7402    let nrows = {
7403        let (first_name, first_val) = fields.iter().next().unwrap();
7404        col_nrows(first_val).ok_or_else(|| {
7405            format!("writetable: column '{first_name}' must be a Matrix (N×1), Cell, or scalar")
7406        })?
7407    };
7408    for (cname, cval) in fields.iter() {
7409        let n = col_nrows(cval).ok_or_else(|| {
7410            format!("writetable: column '{cname}' must be a Matrix (N×1), Cell, or scalar")
7411        })?;
7412        if n != nrows {
7413            return Err(format!(
7414                "writetable: column '{cname}' has {n} rows, expected {nrows}"
7415            ));
7416        }
7417    }
7418
7419    let mut out = String::new();
7420    let header_parts: Vec<String> = fields.keys().map(|k| csv_quote_cell(k, &delim)).collect();
7421    out.push_str(&header_parts.join(&delim));
7422    out.push('\n');
7423
7424    for row in 0..nrows {
7425        let mut parts: Vec<String> = Vec::with_capacity(fields.len());
7426        for cval in fields.values() {
7427            parts.push(col_cell_str(cval, row, &delim)?);
7428        }
7429        out.push_str(&parts.join(&delim));
7430        out.push('\n');
7431    }
7432
7433    std::fs::write(path, out).map_err(|e| format!("writetable: cannot write '{path}': {e}"))?;
7434    Ok(Value::Void)
7435}
7436
7437/// Recursive glob matcher supporting `*` (any sequence) and `?` (single char).
7438fn glob_match_inner(pat: &[u8], name: &[u8]) -> bool {
7439    match (pat.first(), name.first()) {
7440        (None, None) => true,
7441        (Some(&b'*'), _) => {
7442            glob_match_inner(&pat[1..], name)
7443                || (!name.is_empty() && glob_match_inner(pat, &name[1..]))
7444        }
7445        (Some(&b'?'), Some(_)) => glob_match_inner(&pat[1..], &name[1..]),
7446        (Some(p), Some(n)) if p == n => glob_match_inner(&pat[1..], &name[1..]),
7447        _ => false,
7448    }
7449}
7450
7451/// Case-aware glob match: case-insensitive on Windows, case-sensitive elsewhere.
7452fn glob_match(pattern: &str, name: &str) -> bool {
7453    #[cfg(windows)]
7454    let (p, n) = (pattern.to_lowercase(), name.to_lowercase());
7455    #[cfg(not(windows))]
7456    let (p, n) = (pattern.to_string(), name.to_string());
7457    glob_match_inner(p.as_bytes(), n.as_bytes())
7458}
7459
7460/// Implementation of `dir(path)` — returns a `Value::StructArray` with MATLAB-compatible fields.
7461fn dir_impl(path_arg: &str) -> Value {
7462    let has_glob = path_arg.contains('*') || path_arg.contains('?');
7463    let p = std::path::Path::new(path_arg);
7464
7465    let (dir_path, pattern): (std::path::PathBuf, String) = if has_glob {
7466        let parent = p
7467            .parent()
7468            .filter(|d| *d != std::path::Path::new(""))
7469            .unwrap_or(std::path::Path::new("."));
7470        let pat = p
7471            .file_name()
7472            .map(|f| f.to_string_lossy().into_owned())
7473            .unwrap_or_default();
7474        (parent.to_path_buf(), pat)
7475    } else {
7476        (p.to_path_buf(), String::new())
7477    };
7478
7479    // Resolve to an absolute folder string.
7480    // Use current_dir() + join to avoid the UNC prefix (\\?\C:\...) that
7481    // canonicalize() produces on Windows.
7482    let abs = if dir_path.is_absolute() {
7483        dir_path.to_string_lossy().into_owned()
7484    } else {
7485        std::env::current_dir()
7486            .unwrap_or_else(|_| ".".into())
7487            .join(&dir_path)
7488            .to_string_lossy()
7489            .into_owned()
7490    };
7491    // Normalize path separators to the platform native one so the folder field
7492    // is consistent regardless of whether the user passed "/" or "\" on Windows.
7493    #[cfg(windows)]
7494    let abs = abs.replace('/', "\\");
7495    // Strip a trailing separator, but preserve root paths ("/", "C:\").
7496    let folder_str = if abs.len() > 1 && (abs.ends_with('/') || abs.ends_with('\\')) {
7497        abs[..abs.len() - 1].to_string()
7498    } else {
7499        abs
7500    };
7501
7502    let mut entries: Vec<IndexMap<String, Value>> = Vec::new();
7503
7504    // MATLAB always prepends "." and ".." for non-glob directory listings.
7505    if !has_glob {
7506        for dot in &[".", ".."] {
7507            let mut row = IndexMap::new();
7508            row.insert("name".to_string(), Value::Str(dot.to_string()));
7509            row.insert("folder".to_string(), Value::Str(folder_str.clone()));
7510            row.insert("isdir".to_string(), Value::Scalar(1.0));
7511            row.insert("bytes".to_string(), Value::Scalar(0.0));
7512            entries.push(row);
7513        }
7514    }
7515
7516    let Ok(rd) = std::fs::read_dir(&dir_path) else {
7517        return Value::StructArray(Box::default());
7518    };
7519
7520    let mut file_rows: Vec<(String, IndexMap<String, Value>)> = rd
7521        .filter_map(|e| e.ok())
7522        .filter_map(|e| {
7523            let file_name = e.file_name().to_string_lossy().into_owned();
7524            if has_glob && !glob_match(&pattern, &file_name) {
7525                return None;
7526            }
7527            let meta = e.metadata().ok()?;
7528            let is_dir = if meta.is_dir() { 1.0 } else { 0.0 };
7529            let bytes = if meta.is_file() {
7530                meta.len() as f64
7531            } else {
7532                0.0
7533            };
7534            let mut row = IndexMap::new();
7535            row.insert("name".to_string(), Value::Str(file_name.clone()));
7536            row.insert("folder".to_string(), Value::Str(folder_str.clone()));
7537            row.insert("isdir".to_string(), Value::Scalar(is_dir));
7538            row.insert("bytes".to_string(), Value::Scalar(bytes));
7539            Some((file_name, row))
7540        })
7541        .collect();
7542
7543    file_rows.sort_by(|a, b| a.0.cmp(&b.0));
7544    entries.extend(file_rows.into_iter().map(|(_, row)| row));
7545    Value::StructArray(Box::new(entries))
7546}
7547
7548/// Converts an f64 to u64 for bitwise operations.
7549/// Requires a non-negative integer value; returns an error otherwise.
7550fn to_bits(v: f64, fname: &str, pos: usize) -> Result<u64, String> {
7551    if v < 0.0 {
7552        return Err(format!(
7553            "{fname}: argument {pos} must be non-negative, got {v}"
7554        ));
7555    }
7556    if v.fract() != 0.0 {
7557        return Err(format!(
7558            "{fname}: argument {pos} must be an integer, got {v}"
7559        ));
7560    }
7561    if v > u64::MAX as f64 {
7562        return Err(format!(
7563            "{fname}: argument {pos} is too large for bitwise operations"
7564        ));
7565    }
7566    Ok(v as u64)
7567}
7568
7569/// Computes determinant of a square matrix via Gaussian elimination.
7570/// Computes the determinant of a square matrix via Gaussian elimination with
7571/// partial pivoting (pure Rust, no external dependencies).
7572fn det_matrix(m: &Array2<f64>) -> Result<f64, String> {
7573    let n = m.nrows();
7574    if m.ncols() != n {
7575        return Err("det: matrix must be square".to_string());
7576    }
7577    if n == 0 {
7578        return Ok(1.0);
7579    }
7580    let mut a = m.clone();
7581    let mut sign: f64 = 1.0;
7582    for col in 0..n {
7583        // Partial pivoting: swap in the row with the largest absolute value.
7584        let pivot = (col..n)
7585            .max_by(|&r1, &r2| a[[r1, col]].abs().partial_cmp(&a[[r2, col]].abs()).unwrap())
7586            .unwrap();
7587        if a[[pivot, col]].abs() < 1e-15 {
7588            return Ok(0.0); // singular
7589        }
7590        if pivot != col {
7591            for j in 0..n {
7592                let tmp = a[[pivot, j]];
7593                a[[pivot, j]] = a[[col, j]];
7594                a[[col, j]] = tmp;
7595            }
7596            sign = -sign;
7597        }
7598        let pv = a[[col, col]];
7599        for row in (col + 1)..n {
7600            let factor = a[[row, col]] / pv;
7601            for j in col..n {
7602                let val = a[[col, j]] * factor;
7603                a[[row, j]] -= val;
7604            }
7605        }
7606    }
7607    Ok(sign * (0..n).map(|i| a[[i, i]]).product::<f64>())
7608}
7609
7610/// Computes the inverse of a square matrix via Gauss-Jordan elimination with
7611/// partial pivoting (pure Rust, no external dependencies).
7612fn inv_matrix(m: &Array2<f64>) -> Result<Array2<f64>, String> {
7613    let n = m.nrows();
7614    if m.ncols() != n {
7615        return Err("inv: matrix must be square".to_string());
7616    }
7617    let cols = 2 * n;
7618    let mut aug = vec![0.0f64; n * cols];
7619    for i in 0..n {
7620        for j in 0..n {
7621            aug[i * cols + j] = m[[i, j]];
7622        }
7623        aug[i * cols + n + i] = 1.0;
7624    }
7625    for col in 0..n {
7626        // Partial pivoting: swap in the row with the largest absolute value.
7627        let pivot = (col..n)
7628            .max_by(|&r1, &r2| {
7629                aug[r1 * cols + col]
7630                    .abs()
7631                    .partial_cmp(&aug[r2 * cols + col].abs())
7632                    .unwrap()
7633            })
7634            .filter(|&r| aug[r * cols + col].abs() > 1e-12)
7635            .ok_or_else(|| "inv: matrix is singular".to_string())?;
7636        if pivot != col {
7637            for j in 0..cols {
7638                aug.swap(col * cols + j, pivot * cols + j);
7639            }
7640        }
7641        let pv = aug[col * cols + col];
7642        for j in 0..cols {
7643            aug[col * cols + j] /= pv;
7644        }
7645        for row in 0..n {
7646            if row == col {
7647                continue;
7648            }
7649            let factor = aug[row * cols + col];
7650            for j in 0..cols {
7651                let val = aug[col * cols + j] * factor;
7652                aug[row * cols + j] -= val;
7653            }
7654        }
7655    }
7656    let mut result = Array2::<f64>::zeros((n, n));
7657    for i in 0..n {
7658        for j in 0..n {
7659            result[[i, j]] = aug[i * cols + n + j];
7660        }
7661    }
7662    Ok(result)
7663}
7664
7665/// Solves the linear system `A * x = B` using Gaussian elimination with partial pivoting.
7666///
7667/// `A` must be square (n×n); `B` must have n rows. Returns x (n × k where k = B.ncols()).
7668/// This is the engine for the `\` left-division operator.
7669fn solve_linear(a: &Array2<f64>, b: &Array2<f64>) -> Result<Array2<f64>, String> {
7670    let n = a.nrows();
7671    if a.ncols() != n {
7672        return Err(format!(
7673            "\\: coefficient matrix must be square, got {}×{}",
7674            n,
7675            a.ncols()
7676        ));
7677    }
7678    let k = b.ncols();
7679    if b.nrows() != n {
7680        return Err(format!(
7681            "\\: size mismatch — A is {}×{} but b has {} rows",
7682            n,
7683            n,
7684            b.nrows()
7685        ));
7686    }
7687    if n == 0 {
7688        return Ok(Array2::zeros((0, k)));
7689    }
7690    let cols = n + k;
7691    let mut aug = vec![0.0f64; n * cols];
7692    for i in 0..n {
7693        for j in 0..n {
7694            aug[i * cols + j] = a[[i, j]];
7695        }
7696        for j in 0..k {
7697            aug[i * cols + n + j] = b[[i, j]];
7698        }
7699    }
7700    for col in 0..n {
7701        let pivot = (col..n)
7702            .max_by(|&r1, &r2| {
7703                aug[r1 * cols + col]
7704                    .abs()
7705                    .partial_cmp(&aug[r2 * cols + col].abs())
7706                    .unwrap()
7707            })
7708            .filter(|&r| aug[r * cols + col].abs() > 1e-12)
7709            .ok_or_else(|| "\\: matrix is singular or nearly singular".to_string())?;
7710        if pivot != col {
7711            for j in 0..cols {
7712                aug.swap(col * cols + j, pivot * cols + j);
7713            }
7714        }
7715        let pv = aug[col * cols + col];
7716        for j in col..cols {
7717            aug[col * cols + j] /= pv;
7718        }
7719        for row in 0..n {
7720            if row == col {
7721                continue;
7722            }
7723            let factor = aug[row * cols + col];
7724            if factor == 0.0 {
7725                continue;
7726            }
7727            for j in col..cols {
7728                let val = aug[col * cols + j] * factor;
7729                aug[row * cols + j] -= val;
7730            }
7731        }
7732    }
7733    let mut result = Array2::<f64>::zeros((n, k));
7734    for i in 0..n {
7735        for j in 0..k {
7736            result[[i, j]] = aug[i * cols + n + j];
7737        }
7738    }
7739    Ok(result)
7740}
7741
7742// ---------------------------------------------------------------------------
7743// Advanced linear algebra helpers (Phase 18)
7744// ---------------------------------------------------------------------------
7745
7746/// QR decomposition via Householder reflectors.
7747///
7748/// For an m×n matrix A returns (Q, R) where Q is m×m orthogonal and R is
7749/// m×n upper triangular such that A = Q * R.
7750fn qr_decompose(a: &Array2<f64>) -> Result<(Array2<f64>, Array2<f64>), String> {
7751    let m = a.nrows();
7752    let n = a.ncols();
7753    let k = m.min(n);
7754    let mut r = a.clone();
7755    let mut q = Array2::<f64>::eye(m);
7756
7757    for j in 0..k {
7758        let col_len = m - j;
7759        let mut v: Vec<f64> = (j..m).map(|i| r[[i, j]]).collect();
7760
7761        let norm_x = v.iter().map(|&x| x * x).sum::<f64>().sqrt();
7762        if norm_x < 1e-14 {
7763            continue;
7764        }
7765        // Householder sign convention avoids cancellation.
7766        v[0] += if v[0] >= 0.0 { norm_x } else { -norm_x };
7767        let v_sq: f64 = v.iter().map(|&x| x * x).sum();
7768        if v_sq < 1e-28 {
7769            continue;
7770        }
7771
7772        // Apply H from left to R: R[j:m, :] -= 2*v*(v^T*R[j:m,:])/v^Tv
7773        for col in j..n {
7774            let dot: f64 = (0..col_len).map(|i| v[i] * r[[j + i, col]]).sum();
7775            let fac = 2.0 * dot / v_sq;
7776            for i in 0..col_len {
7777                r[[j + i, col]] -= fac * v[i];
7778            }
7779        }
7780        // Accumulate Q from right: Q[:, j:m] -= (Q[:,j:m]*v) * 2*v^T/v^Tv
7781        for row in 0..m {
7782            let dot: f64 = (0..col_len).map(|i| q[[row, j + i]] * v[i]).sum();
7783            let fac = 2.0 * dot / v_sq;
7784            for i in 0..col_len {
7785                q[[row, j + i]] -= fac * v[i];
7786            }
7787        }
7788    }
7789
7790    Ok((q, r))
7791}
7792
7793/// LU decomposition with partial pivoting.
7794///
7795/// For an n×n square matrix A returns (L, U, P) where P*A = L*U,
7796/// L is unit lower triangular, U is upper triangular, and P is a
7797/// permutation matrix.
7798type LuResult = Result<(Array2<f64>, Array2<f64>, Array2<f64>), String>;
7799fn lu_decompose(a: &Array2<f64>) -> LuResult {
7800    let n = a.nrows();
7801    if a.ncols() != n {
7802        return Err("lu: matrix must be square".to_string());
7803    }
7804    let mut u = a.clone();
7805    let mut l = Array2::<f64>::eye(n);
7806    let mut perm: Vec<usize> = (0..n).collect();
7807
7808    for j in 0..n {
7809        let pivot = (j..n)
7810            .max_by(|&r1, &r2| {
7811                u[[r1, j]]
7812                    .abs()
7813                    .partial_cmp(&u[[r2, j]].abs())
7814                    .unwrap_or(std::cmp::Ordering::Equal)
7815            })
7816            .unwrap();
7817
7818        if pivot != j {
7819            for col in 0..n {
7820                let tmp = u[[j, col]];
7821                u[[j, col]] = u[[pivot, col]];
7822                u[[pivot, col]] = tmp;
7823            }
7824            for col in 0..j {
7825                let tmp = l[[j, col]];
7826                l[[j, col]] = l[[pivot, col]];
7827                l[[pivot, col]] = tmp;
7828            }
7829            perm.swap(j, pivot);
7830        }
7831
7832        if u[[j, j]].abs() < 1e-15 {
7833            continue;
7834        }
7835        for i in (j + 1)..n {
7836            l[[i, j]] = u[[i, j]] / u[[j, j]];
7837            for k in j..n {
7838                let val = l[[i, j]] * u[[j, k]];
7839                u[[i, k]] -= val;
7840            }
7841        }
7842    }
7843
7844    let mut p = Array2::<f64>::zeros((n, n));
7845    for (i, &j) in perm.iter().enumerate() {
7846        p[[i, j]] = 1.0;
7847    }
7848    Ok((l, u, p))
7849}
7850
7851/// Cholesky decomposition.
7852///
7853/// For a symmetric positive-definite n×n matrix A returns the upper triangular
7854/// factor R such that A = R^T * R (MATLAB convention).
7855fn chol_decompose(a: &Array2<f64>) -> Result<Array2<f64>, String> {
7856    let n = a.nrows();
7857    if a.ncols() != n {
7858        return Err("chol: matrix must be square".to_string());
7859    }
7860    let mut r = Array2::<f64>::zeros((n, n));
7861    for j in 0..n {
7862        let mut s = a[[j, j]];
7863        for k in 0..j {
7864            s -= r[[k, j]] * r[[k, j]];
7865        }
7866        if s <= 0.0 {
7867            return Err("chol: matrix is not positive definite".to_string());
7868        }
7869        r[[j, j]] = s.sqrt();
7870        for i in (j + 1)..n {
7871            let mut t = a[[j, i]];
7872            for k in 0..j {
7873                t -= r[[k, j]] * r[[k, i]];
7874            }
7875            r[[j, i]] = t / r[[j, j]];
7876        }
7877    }
7878    Ok(r)
7879}
7880
7881/// One-sided Jacobi SVD (economy form).
7882///
7883/// For an m×n matrix A returns (U, s, V) where
7884/// - U is m×k with orthonormal columns (k = min(m,n))
7885/// - s is a `Vec<f64>` of singular values in descending order (length k)
7886/// - V is n×k with orthonormal columns
7887///
7888/// For m < n the inputs are transparently transposed and outputs swapped.
7889type SvdResult = Result<(Array2<f64>, Vec<f64>, Array2<f64>), String>;
7890fn svd_compute(a: &Array2<f64>) -> SvdResult {
7891    let m = a.nrows();
7892    let n = a.ncols();
7893    if m < n {
7894        let (v, s, u) = svd_compute(&a.t().to_owned())?;
7895        return Ok((u, s, v));
7896    }
7897    // m >= n from here.
7898    let k = n;
7899    let mut b = a.clone();
7900    let mut v = Array2::<f64>::eye(k);
7901
7902    const MAX_ITER: usize = 200;
7903    const EPS: f64 = 1e-14;
7904
7905    'outer: for _ in 0..MAX_ITER {
7906        let mut changed = false;
7907        for p in 0..k {
7908            for q in (p + 1)..k {
7909                let alpha: f64 = (0..m).map(|i| b[[i, p]] * b[[i, p]]).sum();
7910                let beta: f64 = (0..m).map(|i| b[[i, q]] * b[[i, q]]).sum();
7911                let gamma: f64 = (0..m).map(|i| b[[i, p]] * b[[i, q]]).sum();
7912
7913                if gamma.abs() <= EPS * (alpha * beta).sqrt() {
7914                    continue;
7915                }
7916                changed = true;
7917
7918                let zeta = (beta - alpha) / (2.0 * gamma);
7919                let t = zeta.signum() / (zeta.abs() + (1.0 + zeta * zeta).sqrt());
7920                let c = 1.0 / (1.0 + t * t).sqrt();
7921                let s = c * t;
7922
7923                for i in 0..m {
7924                    let bp = b[[i, p]];
7925                    let bq = b[[i, q]];
7926                    b[[i, p]] = c * bp - s * bq;
7927                    b[[i, q]] = s * bp + c * bq;
7928                }
7929                for i in 0..k {
7930                    let vp = v[[i, p]];
7931                    let vq = v[[i, q]];
7932                    v[[i, p]] = c * vp - s * vq;
7933                    v[[i, q]] = s * vp + c * vq;
7934                }
7935            }
7936        }
7937        if !changed {
7938            break 'outer;
7939        }
7940    }
7941
7942    let mut sigma: Vec<f64> = (0..k)
7943        .map(|j| (0..m).map(|i| b[[i, j]] * b[[i, j]]).sum::<f64>().sqrt())
7944        .collect();
7945    let mut u_mat = Array2::<f64>::zeros((m, k));
7946    for j in 0..k {
7947        if sigma[j] > EPS {
7948            for i in 0..m {
7949                u_mat[[i, j]] = b[[i, j]] / sigma[j];
7950            }
7951        }
7952    }
7953
7954    // Sort descending by singular value.
7955    let mut order: Vec<usize> = (0..k).collect();
7956    order.sort_by(|&a, &b| {
7957        sigma[b]
7958            .partial_cmp(&sigma[a])
7959            .unwrap_or(std::cmp::Ordering::Equal)
7960    });
7961    let sigma_s: Vec<f64> = order.iter().map(|&i| sigma[i]).collect();
7962    let mut u_s = Array2::<f64>::zeros((m, k));
7963    let mut v_s = Array2::<f64>::zeros((n, k));
7964    for (ni, &oi) in order.iter().enumerate() {
7965        for r in 0..m {
7966            u_s[[r, ni]] = u_mat[[r, oi]];
7967        }
7968        for r in 0..k {
7969            v_s[[r, ni]] = v[[r, oi]];
7970        }
7971    }
7972    sigma = sigma_s;
7973
7974    Ok((u_s, sigma, v_s))
7975}
7976
7977/// Extends an m×k matrix with orthonormal columns to a full m×m orthogonal matrix.
7978///
7979/// Tries each standard basis vector e_0..e_{m-1} in order; keeps those that
7980/// have non-negligible component orthogonal to the existing basis.
7981fn complete_orthonormal_basis(u: &Array2<f64>) -> Array2<f64> {
7982    let m = u.nrows();
7983    let k = u.ncols();
7984    let mut basis: Vec<Vec<f64>> = (0..k).map(|j| u.column(j).to_vec()).collect();
7985
7986    let mut ei = 0usize;
7987    while basis.len() < m && ei < m {
7988        let mut v: Vec<f64> = vec![0.0; m];
7989        v[ei] = 1.0;
7990        ei += 1;
7991        for b in &basis {
7992            let dot: f64 = v.iter().zip(b.iter()).map(|(&a, &b)| a * b).sum();
7993            for (vi, &bi) in v.iter_mut().zip(b.iter()) {
7994                *vi -= dot * bi;
7995            }
7996        }
7997        let norm = v.iter().map(|&x| x * x).sum::<f64>().sqrt();
7998        if norm > 1e-10 {
7999            for vi in &mut v {
8000                *vi /= norm;
8001            }
8002            basis.push(v);
8003        }
8004    }
8005
8006    let mut result = Array2::<f64>::zeros((m, m));
8007    for (j, b) in basis.iter().enumerate() {
8008        for (i, &val) in b.iter().enumerate() {
8009            result[[i, j]] = val;
8010        }
8011    }
8012    result
8013}
8014
8015/// QR-iteration eigendecomposition for a real square matrix.
8016///
8017/// Returns `(eigenvalues, eigenvectors)` where eigenvalues is a `Vec<f64>` of
8018/// length n and eigenvectors is an n×n matrix whose columns are the eigenvectors.
8019/// Uses the basic QR iteration with a simple diagonal shift (Wilkinson-style).
8020/// Convergence is reliable for symmetric matrices; general matrices converge for
8021/// most well-conditioned inputs within `MAX_ITER` steps.
8022fn eig_compute(a: &Array2<f64>) -> Result<(Vec<Complex<f64>>, Array2<f64>), String> {
8023    let n = a.nrows();
8024    if a.ncols() != n {
8025        return Err("eig: matrix must be square".to_string());
8026    }
8027    if n == 0 {
8028        return Ok((vec![], Array2::zeros((0, 0))));
8029    }
8030    if n == 1 {
8031        return Ok((vec![Complex::new(a[[0, 0]], 0.0)], Array2::eye(1)));
8032    }
8033
8034    let mut ak = a.clone();
8035    let mut evecs = Array2::<f64>::eye(n);
8036
8037    const MAX_ITER: usize = 2000;
8038    const EPS: f64 = 1e-12;
8039
8040    for _ in 0..MAX_ITER {
8041        // Wilkinson shift: uses the trailing 2×2 submatrix for cubic convergence.
8042        let mu = {
8043            let d = ak[[n - 1, n - 1]];
8044            if n >= 2 {
8045                let a = ak[[n - 2, n - 2]];
8046                let b = ak[[n - 2, n - 1]];
8047                let delta = (a - d) / 2.0;
8048                if delta.abs() < 1e-30 {
8049                    d - b.abs()
8050                } else {
8051                    d - b * b / (delta + delta.signum() * (delta * delta + b * b).sqrt())
8052                }
8053            } else {
8054                d
8055            }
8056        };
8057
8058        for i in 0..n {
8059            ak[[i, i]] -= mu;
8060        }
8061        let (q, r) = qr_decompose(&ak)?;
8062        ak = r.dot(&q);
8063        for i in 0..n {
8064            ak[[i, i]] += mu;
8065        }
8066        evecs = evecs.dot(&q);
8067
8068        // Convergence check: all sub-diagonals small (real eigenvalues only).
8069        let max_sub = (0..(n - 1))
8070            .map(|i| ak[[i + 1, i]].abs())
8071            .fold(0.0_f64, f64::max);
8072        if max_sub < EPS {
8073            break;
8074        }
8075    }
8076
8077    // Post-convergence scan: extract complex conjugate pairs from 2×2 quasi-triangular blocks.
8078    // A sub-diagonal entry larger than EPS_BLOCK indicates a complex eigenvalue pair.
8079    const EPS_BLOCK: f64 = 1e-8;
8080    let mut evals: Vec<Complex<f64>> = Vec::with_capacity(n);
8081    let mut i = 0;
8082    while i < n {
8083        if i + 1 < n && ak[[i + 1, i]].abs() > EPS_BLOCK {
8084            let (a_ii, b, c, d_ii) = (
8085                ak[[i, i]],
8086                ak[[i, i + 1]],
8087                ak[[i + 1, i]],
8088                ak[[i + 1, i + 1]],
8089            );
8090            let p = (a_ii + d_ii) / 2.0;
8091            let disc = ((a_ii - d_ii) / 2.0).powi(2) + b * c;
8092            if disc < 0.0 {
8093                let q = (-disc).sqrt();
8094                evals.push(Complex::new(p, q));
8095                evals.push(Complex::new(p, -q));
8096            } else {
8097                let q = disc.sqrt();
8098                evals.push(Complex::new(p + q, 0.0));
8099                evals.push(Complex::new(p - q, 0.0));
8100            }
8101            i += 2;
8102        } else {
8103            evals.push(Complex::new(ak[[i, i]], 0.0));
8104            i += 1;
8105        }
8106    }
8107
8108    Ok((evals, evecs))
8109}
8110
8111// ---------------------------------------------------------------------------
8112// Indexing
8113// ---------------------------------------------------------------------------
8114
8115/// Creates a copy of `env` with `end` set to `dim_size`.
8116/// Used by `eval_index` so that `end` in index expressions resolves to the correct dimension size.
8117fn env_with_end(env: &Env, dim_size: usize) -> Env {
8118    let mut e = env.clone();
8119    e.insert("end".to_string(), Value::Scalar(dim_size as f64));
8120    e
8121}
8122
8123/// Returns `true` if `expr` (or any sub-expression) references the identifier `end`.
8124///
8125/// Used to skip the full [`Env`] clone inside [`eval_index`] when `end` is absent.
8126pub(crate) fn contains_end(expr: &Expr) -> bool {
8127    match expr {
8128        Expr::Var(s) => s == "end",
8129        Expr::Number(_)
8130        | Expr::Colon
8131        | Expr::StrLiteral(_)
8132        | Expr::StringObjLiteral(_)
8133        | Expr::NaT
8134        | Expr::FuncHandle(_) => false,
8135        Expr::UnaryMinus(e)
8136        | Expr::UnaryNot(e)
8137        | Expr::Transpose(e)
8138        | Expr::PlainTranspose(e)
8139        | Expr::FieldGet(e, _) => contains_end(e),
8140        Expr::DynFieldGet(a, b) => contains_end(a) || contains_end(b),
8141        Expr::BinOp(l, _, r) => contains_end(l) || contains_end(r),
8142        Expr::Call(_, args) | Expr::DotCall(_, args) => args.iter().any(contains_end),
8143        Expr::Matrix(rows) => rows.iter().flat_map(|r| r.iter()).any(contains_end),
8144        Expr::Range(a, step, b) => {
8145            contains_end(a) || step.as_deref().is_some_and(contains_end) || contains_end(b)
8146        }
8147        Expr::Lambda { body, .. } => contains_end(body),
8148        Expr::CellLiteral(elems) => elems.iter().any(contains_end),
8149        Expr::CellIndex(a, b) => contains_end(a) || contains_end(b),
8150    }
8151}
8152
8153/// Evaluates `val(args...)` — indexing a variable with one or two index arguments.
8154///
8155/// Disambiguation rule (Octave semantics): a name that exists in `Env` is always
8156/// treated as a variable to be indexed, never as a function call.
8157/// Constructs a `Value::Map` from `containers.Map(keys_cell, values_cell)` call.
8158///
8159/// 0-arg form: empty map. 2-arg form: cell of string keys + cell of values.
8160fn make_containers_map(
8161    args: &[Expr],
8162    env: &Env,
8163    io: Option<&mut IoContext>,
8164) -> Result<Value, String> {
8165    if args.is_empty() {
8166        return Ok(Value::Map(Box::new(IndexMap::new())));
8167    }
8168    if args.len() != 2 {
8169        return Err(
8170            "containers.Map: expected 0 or 2 arguments (keys cell, values cell)".to_string(),
8171        );
8172    }
8173    let mut io_wrap = io;
8174    let keys_val = eval_inner(&args[0], env, io_wrap.as_deref_mut())?;
8175    let vals_val = eval_inner(&args[1], env, io_wrap)?;
8176    let keys = match keys_val {
8177        Value::Cell(v) => v,
8178        _ => {
8179            return Err(
8180                "containers.Map: first argument must be a cell array of strings".to_string(),
8181            );
8182        }
8183    };
8184    let values = match vals_val {
8185        Value::Cell(v) => v,
8186        _ => {
8187            return Err(
8188                "containers.Map: second argument must be a cell array of values".to_string(),
8189            );
8190        }
8191    };
8192    if keys.len() != values.len() {
8193        return Err(format!(
8194            "containers.Map: key count ({}) does not match value count ({})",
8195            keys.len(),
8196            values.len()
8197        ));
8198    }
8199    let keys = *keys;
8200    let values = *values;
8201    let mut map = IndexMap::new();
8202    for (k, v) in keys.into_iter().zip(values) {
8203        let key = match k {
8204            Value::Str(s) | Value::StringObj(s) => s,
8205            _ => return Err("containers.Map: all keys must be strings".to_string()),
8206        };
8207        map.insert(key, v);
8208    }
8209    Ok(Value::Map(Box::new(map)))
8210}
8211
8212fn eval_index(val: &Value, args: &[Expr], env: &Env) -> Result<Value, String> {
8213    match args.len() {
8214        0 => Err("Indexing requires at least one index".to_string()),
8215        1 => {
8216            // v(i), v(1:3), v(:), v(end), v(end-1:end)
8217            match val {
8218                Value::Void => Err("Cannot index into void".to_string()),
8219                Value::Lambda(_) | Value::Function(_) | Value::Tuple(_) => {
8220                    Err("Cannot index into a function value".to_string())
8221                }
8222                Value::Cell(_) => Err("Use c{i} to index into a cell array, not c(i)".to_string()),
8223                Value::Struct(_) => {
8224                    Err("Use s.field to access struct fields, not s(i)".to_string())
8225                }
8226                Value::Map(map) => {
8227                    let key_val = eval_inner(&args[0], env, None)?;
8228                    let key = match key_val {
8229                        Value::Str(s) | Value::StringObj(s) => s,
8230                        _ => return Err("Map key must be a string".to_string()),
8231                    };
8232                    map.get(&key)
8233                        .cloned()
8234                        .ok_or_else(|| format!("Map key '{key}' not found"))
8235                }
8236                Value::StructArray(arr) => {
8237                    let total = arr.len();
8238                    let _owned_env;
8239                    let env1: &Env = if contains_end(&args[0]) {
8240                        _owned_env = env_with_end(env, total);
8241                        &_owned_env
8242                    } else {
8243                        env
8244                    };
8245                    match resolve_dim(&args[0], total, env1)? {
8246                        DimIdx::All => {
8247                            // s(:) — return all elements as a new struct array
8248                            Ok(Value::StructArray(arr.clone()))
8249                        }
8250                        DimIdx::Indices(idxs) => {
8251                            if idxs.len() == 1 {
8252                                let i = idxs[0];
8253                                if i >= total {
8254                                    return Err(format!(
8255                                        "Index {} out of range (1..{})",
8256                                        i + 1,
8257                                        total
8258                                    ));
8259                                }
8260                                Ok(Value::Struct(Box::new(arr[i].clone())))
8261                            } else {
8262                                let mut selected = Vec::with_capacity(idxs.len());
8263                                for &i in &idxs {
8264                                    if i >= total {
8265                                        return Err(format!(
8266                                            "Index {} out of range (1..{})",
8267                                            i + 1,
8268                                            total
8269                                        ));
8270                                    }
8271                                    selected.push(arr[i].clone());
8272                                }
8273                                Ok(Value::StructArray(Box::new(selected)))
8274                            }
8275                        }
8276                    }
8277                }
8278                Value::Scalar(n) => {
8279                    let _owned_env;
8280                    let env1: &Env = if contains_end(&args[0]) {
8281                        _owned_env = env_with_end(env, 1);
8282                        &_owned_env
8283                    } else {
8284                        env
8285                    };
8286                    match resolve_dim(&args[0], 1, env1)? {
8287                        DimIdx::All | DimIdx::Indices(_) => Ok(Value::Scalar(*n)),
8288                    }
8289                }
8290                Value::Complex(re, im) => {
8291                    let _owned_env;
8292                    let env1: &Env = if contains_end(&args[0]) {
8293                        _owned_env = env_with_end(env, 1);
8294                        &_owned_env
8295                    } else {
8296                        env
8297                    };
8298                    match resolve_dim(&args[0], 1, env1)? {
8299                        DimIdx::All | DimIdx::Indices(_) => Ok(Value::Complex(*re, *im)),
8300                    }
8301                }
8302                Value::ComplexMatrix(m) => {
8303                    let total = m.nrows() * m.ncols();
8304                    let _owned_env;
8305                    let env1: &Env = if contains_end(&args[0]) {
8306                        _owned_env = env_with_end(env, total);
8307                        &_owned_env
8308                    } else {
8309                        env
8310                    };
8311                    match resolve_dim(&args[0], total, env1)? {
8312                        DimIdx::All => {
8313                            // A(:) → column vector (column-major), as ComplexMatrix
8314                            let mut flat: Vec<Complex<f64>> = Vec::with_capacity(total);
8315                            for col in 0..m.ncols() {
8316                                for row in 0..m.nrows() {
8317                                    flat.push(m[[row, col]]);
8318                                }
8319                            }
8320                            Ok(Value::ComplexMatrix(Box::new(
8321                                Array2::from_shape_vec((total, 1), flat).unwrap(),
8322                            )))
8323                        }
8324                        DimIdx::Indices(idxs) => {
8325                            let nrows = m.nrows();
8326                            let ncols_m = m.ncols();
8327                            let vals: Result<Vec<Complex<f64>>, String> = idxs
8328                                .iter()
8329                                .map(|&i| {
8330                                    let row = i % nrows;
8331                                    let col = i / nrows;
8332                                    if col >= ncols_m {
8333                                        Err(format!("Index {} out of range (1..{})", i + 1, total))
8334                                    } else {
8335                                        Ok(m[[row, col]])
8336                                    }
8337                                })
8338                                .collect();
8339                            let vals = vals?;
8340                            if vals.len() == 1 {
8341                                let c = vals[0];
8342                                Ok(make_complex(c.re, c.im))
8343                            } else {
8344                                let n = vals.len();
8345                                Ok(Value::ComplexMatrix(Box::new(
8346                                    Array2::from_shape_vec((1, n), vals).unwrap(),
8347                                )))
8348                            }
8349                        }
8350                    }
8351                }
8352                Value::Matrix(m) => {
8353                    let total = m.nrows() * m.ncols();
8354                    let _owned_env;
8355                    let env1: &Env = if contains_end(&args[0]) {
8356                        _owned_env = env_with_end(env, total);
8357                        &_owned_env
8358                    } else {
8359                        env
8360                    };
8361                    match resolve_dim(&args[0], total, env1)? {
8362                        DimIdx::All => {
8363                            // A(:) → column vector, column-major order
8364                            let mut flat = Vec::with_capacity(total);
8365                            for col in 0..m.ncols() {
8366                                for row in 0..m.nrows() {
8367                                    flat.push(m[[row, col]]);
8368                                }
8369                            }
8370                            Ok(Value::Matrix(Box::new(
8371                                Array2::from_shape_vec((total, 1), flat).unwrap(),
8372                            )))
8373                        }
8374                        DimIdx::Indices(idxs) => {
8375                            // Column-major linear indexing
8376                            let nrows = m.nrows();
8377                            let ncols_m = m.ncols();
8378                            let vals: Result<Vec<f64>, String> = idxs
8379                                .iter()
8380                                .map(|&i| {
8381                                    // i is 0-based, column-major
8382                                    let row = i % nrows;
8383                                    let col = i / nrows;
8384                                    if col >= ncols_m {
8385                                        Err(format!("Index {} out of range (1..{})", i + 1, total))
8386                                    } else {
8387                                        Ok(m[[row, col]])
8388                                    }
8389                                })
8390                                .collect();
8391                            let vals = vals?;
8392                            if vals.len() == 1 {
8393                                Ok(Value::Scalar(vals[0]))
8394                            } else {
8395                                let n = vals.len();
8396                                Ok(Value::Matrix(Box::new(
8397                                    Array2::from_shape_vec((1, n), vals).unwrap(),
8398                                )))
8399                            }
8400                        }
8401                    }
8402                }
8403                Value::Str(s) => {
8404                    // Index into a char array — returns char code(s)
8405                    let chars: Vec<char> = s.chars().collect();
8406                    let total = chars.len();
8407                    let _owned_env;
8408                    let env1: &Env = if contains_end(&args[0]) {
8409                        _owned_env = env_with_end(env, total);
8410                        &_owned_env
8411                    } else {
8412                        env
8413                    };
8414                    match resolve_dim(&args[0], total, env1)? {
8415                        DimIdx::All => {
8416                            let codes: Vec<f64> = chars.iter().map(|&c| c as u32 as f64).collect();
8417                            if codes.len() == 1 {
8418                                Ok(Value::Scalar(codes[0]))
8419                            } else {
8420                                let n = codes.len();
8421                                Ok(Value::Matrix(Box::new(
8422                                    Array2::from_shape_vec((1, n), codes).unwrap(),
8423                                )))
8424                            }
8425                        }
8426                        DimIdx::Indices(idxs) => {
8427                            let mut selected = String::new();
8428                            for &i in &idxs {
8429                                if i >= chars.len() {
8430                                    return Err(format!("Index {} out of range", i + 1));
8431                                }
8432                                selected.push(chars[i]);
8433                            }
8434                            if selected.chars().count() == 1 {
8435                                Ok(Value::Scalar(selected.chars().next().unwrap() as u32 as f64))
8436                            } else {
8437                                Ok(Value::Str(selected))
8438                            }
8439                        }
8440                    }
8441                }
8442                Value::StringObj(s) => {
8443                    // String object indexing — treat as single element
8444                    let _owned_env;
8445                    let env1: &Env = if contains_end(&args[0]) {
8446                        _owned_env = env_with_end(env, 1);
8447                        &_owned_env
8448                    } else {
8449                        env
8450                    };
8451                    match resolve_dim(&args[0], 1, env1)? {
8452                        DimIdx::All | DimIdx::Indices(_) => Ok(Value::StringObj(s.clone())),
8453                    }
8454                }
8455                Value::DateTimeArray(v) => {
8456                    let total = v.len();
8457                    let _owned_env;
8458                    let env1: &Env = if contains_end(&args[0]) {
8459                        _owned_env = env_with_end(env, total);
8460                        &_owned_env
8461                    } else {
8462                        env
8463                    };
8464                    match resolve_dim(&args[0], total, env1)? {
8465                        DimIdx::All => Ok(Value::DateTimeArray(v.clone())),
8466                        DimIdx::Indices(idxs) => {
8467                            if idxs.len() == 1 {
8468                                let i = idxs[0];
8469                                if i >= total {
8470                                    return Err(format!(
8471                                        "Index {} out of range (1..{})",
8472                                        i + 1,
8473                                        total
8474                                    ));
8475                                }
8476                                Ok(Value::DateTime(v[i]))
8477                            } else {
8478                                let mut sel = Vec::with_capacity(idxs.len());
8479                                for &i in &idxs {
8480                                    if i >= total {
8481                                        return Err(format!(
8482                                            "Index {} out of range (1..{})",
8483                                            i + 1,
8484                                            total
8485                                        ));
8486                                    }
8487                                    sel.push(v[i]);
8488                                }
8489                                Ok(Value::DateTimeArray(sel))
8490                            }
8491                        }
8492                    }
8493                }
8494                Value::DurationArray(v) => {
8495                    let total = v.len();
8496                    let _owned_env;
8497                    let env1: &Env = if contains_end(&args[0]) {
8498                        _owned_env = env_with_end(env, total);
8499                        &_owned_env
8500                    } else {
8501                        env
8502                    };
8503                    match resolve_dim(&args[0], total, env1)? {
8504                        DimIdx::All => Ok(Value::DurationArray(v.clone())),
8505                        DimIdx::Indices(idxs) => {
8506                            if idxs.len() == 1 {
8507                                let i = idxs[0];
8508                                if i >= total {
8509                                    return Err(format!(
8510                                        "Index {} out of range (1..{})",
8511                                        i + 1,
8512                                        total
8513                                    ));
8514                                }
8515                                Ok(Value::Duration(v[i]))
8516                            } else {
8517                                let mut sel = Vec::with_capacity(idxs.len());
8518                                for &i in &idxs {
8519                                    if i >= total {
8520                                        return Err(format!(
8521                                            "Index {} out of range (1..{})",
8522                                            i + 1,
8523                                            total
8524                                        ));
8525                                    }
8526                                    sel.push(v[i]);
8527                                }
8528                                Ok(Value::DurationArray(sel))
8529                            }
8530                        }
8531                    }
8532                }
8533                Value::DateTime(_) | Value::Duration(_) => {
8534                    // Scalar datetime/duration: indexing with (1) is valid, returns self.
8535                    let _owned_env;
8536                    let env1: &Env = if contains_end(&args[0]) {
8537                        _owned_env = env_with_end(env, 1);
8538                        &_owned_env
8539                    } else {
8540                        env
8541                    };
8542                    match resolve_dim(&args[0], 1, env1)? {
8543                        DimIdx::All | DimIdx::Indices(_) => Ok(val.clone()),
8544                    }
8545                }
8546            }
8547        }
8548        2 => {
8549            // A(i, j), A(:, j), A(i, :), A(:, :), A(end, :), A(1:end, 2)
8550            if matches!(
8551                val,
8552                Value::Void
8553                    | Value::Str(_)
8554                    | Value::StringObj(_)
8555                    | Value::Lambda(_)
8556                    | Value::Function(_)
8557                    | Value::Tuple(_)
8558                    | Value::Cell(_)
8559                    | Value::Struct(_)
8560                    | Value::StructArray(_)
8561                    | Value::DateTime(_)
8562                    | Value::Duration(_)
8563                    | Value::DateTimeArray(_)
8564                    | Value::DurationArray(_)
8565            ) {
8566                return Err("2D indexing not supported for this type".to_string());
8567            }
8568            // ComplexMatrix is explicitly supported below (not in the guard above)
8569            let (nrows, ncols) = match val {
8570                Value::Scalar(_) | Value::Complex(_, _) => (1, 1),
8571                Value::Matrix(m) => (m.nrows(), m.ncols()),
8572                Value::ComplexMatrix(m) => (m.nrows(), m.ncols()),
8573                _ => unreachable!(),
8574            };
8575            let _owned_r;
8576            let env_r: &Env = if contains_end(&args[0]) {
8577                _owned_r = env_with_end(env, nrows);
8578                &_owned_r
8579            } else {
8580                env
8581            };
8582            let _owned_c;
8583            let env_c: &Env = if contains_end(&args[1]) {
8584                _owned_c = env_with_end(env, ncols);
8585                &_owned_c
8586            } else {
8587                env
8588            };
8589            let row_idx = resolve_dim(&args[0], nrows, env_r)?;
8590            let col_idx = resolve_dim(&args[1], ncols, env_c)?;
8591
8592            let rows: Vec<usize> = match row_idx {
8593                DimIdx::All => (0..nrows).collect(),
8594                DimIdx::Indices(v) => v,
8595            };
8596            let cols: Vec<usize> = match col_idx {
8597                DimIdx::All => (0..ncols).collect(),
8598                DimIdx::Indices(v) => v,
8599            };
8600
8601            if rows.len() == 1 && cols.len() == 1 {
8602                match val {
8603                    Value::Scalar(n) => Ok(Value::Scalar(*n)),
8604                    Value::Complex(re, im) => Ok(Value::Complex(*re, *im)),
8605                    Value::Matrix(m) => Ok(Value::Scalar(m[[rows[0], cols[0]]])),
8606                    Value::ComplexMatrix(m) => {
8607                        let c = m[[rows[0], cols[0]]];
8608                        Ok(make_complex(c.re, c.im))
8609                    }
8610                    _ => unreachable!(),
8611                }
8612            } else {
8613                let out_r = rows.len();
8614                let out_c = cols.len();
8615                match val {
8616                    Value::ComplexMatrix(m) => {
8617                        let flat: Vec<Complex<f64>> = rows
8618                            .iter()
8619                            .flat_map(|&r| cols.iter().map(move |&c| m[[r, c]]))
8620                            .collect();
8621                        Ok(Value::ComplexMatrix(Box::new(
8622                            Array2::from_shape_vec((out_r, out_c), flat).unwrap(),
8623                        )))
8624                    }
8625                    _ => {
8626                        let flat: Vec<f64> = rows
8627                            .iter()
8628                            .flat_map(|&r| {
8629                                cols.iter().map(move |&c| match val {
8630                                    Value::Scalar(n) => *n,
8631                                    Value::Complex(re, _) => *re,
8632                                    Value::Matrix(m) => m[[r, c]],
8633                                    _ => unreachable!(),
8634                                })
8635                            })
8636                            .collect();
8637                        Ok(Value::Matrix(Box::new(
8638                            Array2::from_shape_vec((out_r, out_c), flat).unwrap(),
8639                        )))
8640                    }
8641                }
8642            }
8643        }
8644        n => Err(format!(
8645            "Indexing with {n} indices is not supported (max 2)"
8646        )),
8647    }
8648}
8649
8650/// Resolved index along one dimension. Indices are 0-based.
8651enum DimIdx {
8652    All,
8653    Indices(Vec<usize>),
8654}
8655
8656/// Resolves one index argument for a dimension of size `dim_size`.
8657/// `Expr::Colon` → `DimIdx::All`.
8658/// Scalar → single 0-based index (validates 1-based bounds).
8659/// Row/column vector → multiple 0-based indices.
8660/// Logical mask: a 0/1 vector whose length equals `dim_size` selects positions where value is 1.
8661fn resolve_dim(expr: &Expr, dim_size: usize, env: &Env) -> Result<DimIdx, String> {
8662    if matches!(expr, Expr::Colon) {
8663        return Ok(DimIdx::All);
8664    }
8665    let val = eval(expr, env)?;
8666    let floats: Vec<f64> = match val {
8667        Value::Void => {
8668            return Err("Index must be numeric, not void".to_string());
8669        }
8670        Value::Scalar(n) => vec![n],
8671        Value::Complex(re, im) => {
8672            if im != 0.0 {
8673                return Err("Index must be real, not complex".to_string());
8674            }
8675            vec![re]
8676        }
8677        Value::Matrix(m) => {
8678            // Allow 2-D matrices only when they qualify as a logical mask (same numel as dim_size).
8679            let total = m.nrows() * m.ncols();
8680            if m.nrows() > 1 && m.ncols() > 1 && total != dim_size {
8681                return Err("Index must be a scalar or vector, not a matrix".to_string());
8682            }
8683            // Collect in column-major order so mask positions align with linear indexing.
8684            if m.nrows() > 1 && m.ncols() > 1 {
8685                let mut v = Vec::with_capacity(total);
8686                for col in 0..m.ncols() {
8687                    for row in 0..m.nrows() {
8688                        v.push(m[[row, col]]);
8689                    }
8690                }
8691                v
8692            } else {
8693                m.iter().copied().collect()
8694            }
8695        }
8696        Value::Str(_) | Value::StringObj(_) => {
8697            return Err("Index must be numeric, not a string".to_string());
8698        }
8699        Value::ComplexMatrix(_) => {
8700            return Err("Index must be real, not a complex matrix".to_string());
8701        }
8702        Value::Lambda(_)
8703        | Value::Function(_)
8704        | Value::Tuple(_)
8705        | Value::Cell(_)
8706        | Value::Struct(_)
8707        | Value::StructArray(_)
8708        | Value::DateTime(_)
8709        | Value::Duration(_)
8710        | Value::DateTimeArray(_)
8711        | Value::DurationArray(_)
8712        | Value::Map(_) => {
8713            return Err("Index must be numeric, not a function or datetime".to_string());
8714        }
8715    };
8716    // Logical mask: a 0/1 array whose element count matches dim_size selects by boolean mask.
8717    if dim_size > 0 && floats.len() == dim_size && floats.iter().all(|&f| f == 0.0 || f == 1.0) {
8718        let idxs: Vec<usize> = floats
8719            .iter()
8720            .enumerate()
8721            .filter(|&(_, &f)| f == 1.0)
8722            .map(|(i, _)| i)
8723            .collect();
8724        return Ok(DimIdx::Indices(idxs));
8725    }
8726    let mut idxs = Vec::with_capacity(floats.len());
8727    for n in floats {
8728        let i = n.round() as i64;
8729        if i < 1 || i as usize > dim_size {
8730            return Err(format!("Index {i} out of range (1..{dim_size})"));
8731        }
8732        idxs.push(i as usize - 1);
8733    }
8734    Ok(DimIdx::Indices(idxs))
8735}
8736
8737/// Formats a number for display: integers without decimal point,
8738/// floats with up to 10 significant fractional digits, trailing zeros trimmed.
8739/// Always decimal — used for expression re-display, not user-facing output.
8740pub fn format_number(n: f64) -> String {
8741    if n.fract() == 0.0 && n.abs() < 1e15 {
8742        format!("{}", n as i64)
8743    } else if n != 0.0 && (n.abs() >= 1e15 || n.abs() < 1e-9) {
8744        trim_sci(&format!("{:.15e}", n))
8745    } else {
8746        let s = format!("{:.10}", n);
8747        s.trim_end_matches('0').trim_end_matches('.').to_string()
8748    }
8749}
8750
8751/// Formats a scalar `f64` for user-facing output using the given base and format mode.
8752pub fn format_scalar(n: f64, base: Base, mode: &FormatMode) -> String {
8753    // FormatMode::Hex always shows IEEE 754 bits regardless of base.
8754    if matches!(mode, FormatMode::Hex) {
8755        return format_decimal(n, mode);
8756    }
8757    match base {
8758        Base::Dec => format_decimal(n, mode),
8759        _ => format_non_dec(n, base),
8760    }
8761}
8762
8763/// Formats a complex number `re + im*i` for display.
8764///
8765/// - `a + 0i` → `a`  (pure real)
8766/// - `0 + bi` → `bi`
8767/// - `im == ±1` suppresses the coefficient: `i`, `-i`, `a + i`, `a - i`
8768pub fn format_complex(re: f64, im: f64, mode: &FormatMode) -> String {
8769    if im == 0.0 {
8770        return format_decimal(re, mode);
8771    }
8772    let im_abs = im.abs();
8773    let im_str = if im_abs == 1.0 {
8774        String::new()
8775    } else {
8776        format_decimal(im_abs, mode)
8777    };
8778    if re == 0.0 {
8779        if im < 0.0 {
8780            format!("-{}i", im_str)
8781        } else {
8782            format!("{}i", im_str)
8783        }
8784    } else {
8785        let re_str = format_decimal(re, mode);
8786        if im < 0.0 {
8787            format!("{} - {}i", re_str, im_str)
8788        } else {
8789            format!("{} + {}i", re_str, im_str)
8790        }
8791    }
8792}
8793
8794/// Reconstructs a source-like string from an `Expr`.
8795///
8796/// Used to populate the display string of lambda values so that
8797/// `f = @(x) x.^2` shows `f = @(x) x .^ 2` in the REPL.
8798pub fn expr_to_string(e: &Expr) -> String {
8799    match e {
8800        Expr::Number(n) => {
8801            if n.is_nan() {
8802                "nan".to_string()
8803            } else if n.is_infinite() {
8804                if *n > 0.0 {
8805                    "inf".to_string()
8806                } else {
8807                    "-inf".to_string()
8808                }
8809            } else {
8810                format!("{n}")
8811            }
8812        }
8813        Expr::Var(name) => name.clone(),
8814        Expr::UnaryMinus(e) => format!("-{}", expr_to_string(e)),
8815        Expr::UnaryNot(e) => format!("~{}", expr_to_string(e)),
8816        Expr::BinOp(l, op, r) => {
8817            let op_str = match op {
8818                Op::Add => "+",
8819                Op::Sub => "-",
8820                Op::Mul => "*",
8821                Op::Div => "/",
8822                Op::Pow => "^",
8823                Op::ElemMul => ".*",
8824                Op::ElemDiv => "./",
8825                Op::ElemPow => ".^",
8826                Op::Eq => "==",
8827                Op::NotEq => "~=",
8828                Op::Lt => "<",
8829                Op::Gt => ">",
8830                Op::LtEq => "<=",
8831                Op::GtEq => ">=",
8832                Op::And => "&&",
8833                Op::Or => "||",
8834                Op::ElemAnd => "&",
8835                Op::ElemOr => "|",
8836                Op::LDiv => "\\",
8837            };
8838            format!("{} {op_str} {}", expr_to_string(l), expr_to_string(r))
8839        }
8840        Expr::Call(name, args) => {
8841            let args_str = args
8842                .iter()
8843                .map(expr_to_string)
8844                .collect::<Vec<_>>()
8845                .join(", ");
8846            format!("{name}({args_str})")
8847        }
8848        Expr::Transpose(e) => format!("{}'", expr_to_string(e)),
8849        Expr::PlainTranspose(e) => format!("{}.'", expr_to_string(e)),
8850        Expr::Range(start, step, stop) => {
8851            if let Some(step) = step {
8852                format!(
8853                    "{}:{}:{}",
8854                    expr_to_string(start),
8855                    expr_to_string(step),
8856                    expr_to_string(stop)
8857                )
8858            } else {
8859                format!("{}:{}", expr_to_string(start), expr_to_string(stop))
8860            }
8861        }
8862        Expr::StrLiteral(s) => format!("'{s}'"),
8863        Expr::StringObjLiteral(s) => format!("\"{s}\""),
8864        Expr::Lambda { params, body, .. } => {
8865            format!("@({}) {}", params.join(", "), expr_to_string(body))
8866        }
8867        Expr::FuncHandle(name) => format!("@{name}"),
8868        Expr::Matrix(_) => "[...]".to_string(),
8869        Expr::CellLiteral(_) => "{...}".to_string(),
8870        Expr::CellIndex(e, i) => format!("{}{{{}}}", expr_to_string(e), expr_to_string(i)),
8871        Expr::Colon => ":".to_string(),
8872        Expr::NaT => "NaT".to_string(),
8873        Expr::FieldGet(base, field) => format!("{}.{field}", expr_to_string(base)),
8874        Expr::DynFieldGet(base, field_expr) => {
8875            format!("{}.({})", expr_to_string(base), expr_to_string(field_expr))
8876        }
8877        Expr::DotCall(segs, args) => {
8878            let args_str = args
8879                .iter()
8880                .map(expr_to_string)
8881                .collect::<Vec<_>>()
8882                .join(", ");
8883            format!("{}({args_str})", segs.join("."))
8884        }
8885    }
8886}
8887
8888/// Formats a `Value` compactly: scalars as a number string, matrices as `[NxM double]`.
8889pub fn format_value(v: &Value, base: Base, mode: &FormatMode) -> String {
8890    match v {
8891        Value::Void => String::new(),
8892        Value::Scalar(n) => format_scalar(*n, base, mode),
8893        Value::Matrix(m) => format!("[{}x{} double]", m.nrows(), m.ncols()),
8894        Value::ComplexMatrix(m) => format!("[{}×{} complex]", m.nrows(), m.ncols()),
8895        Value::Complex(re, im) => format_complex(*re, *im, mode),
8896        Value::Str(s) => s.clone(),
8897        Value::StringObj(s) => s.clone(),
8898        Value::Lambda(lf) => lf.1.clone(),
8899        Value::Function(fd) => {
8900            let params_str = fd.params.join(", ");
8901            let out_str = match fd.outputs.len() {
8902                0 => String::new(),
8903                1 => format!("{} = ", fd.outputs[0]),
8904                _ => format!("[{}] = ", fd.outputs.join(", ")),
8905            };
8906            format!("@function {out_str}f({params_str})")
8907        }
8908        Value::Tuple(vals) => {
8909            let parts: Vec<String> = vals.iter().map(|v| format_value(v, base, mode)).collect();
8910            format!("({})", parts.join(", "))
8911        }
8912        Value::Cell(v) => format!("{{1×{} cell}}", v.len()),
8913        Value::Struct(_) => "[1×1 struct]".to_string(),
8914        Value::StructArray(arr) => format!("[1×{} struct]", arr.len()),
8915        Value::DateTime(ts) => crate::datetime::format_datetime(*ts),
8916        Value::Duration(s) => crate::datetime::format_duration(*s),
8917        Value::DateTimeArray(v) => format!("[{}×1 datetime]", v.len()),
8918        Value::DurationArray(v) => format!("[{}×1 duration]", v.len()),
8919        Value::Map(m) => format!("[Map with {} entries]", m.len()),
8920    }
8921}
8922
8923/// Returns `None` for scalars, complex numbers, strings, and void (displayed inline or suppressed);
8924/// `Some(full_string)` for matrices (MATLAB-style column-aligned display).
8925pub fn format_value_full(v: &Value, mode: &FormatMode) -> Option<String> {
8926    match v {
8927        Value::Void
8928        | Value::Scalar(_)
8929        | Value::Complex(_, _)
8930        | Value::Str(_)
8931        | Value::StringObj(_)
8932        | Value::Lambda(_)
8933        | Value::Function(_)
8934        | Value::Tuple(_)
8935        | Value::DateTime(_)
8936        | Value::Duration(_) => None,
8937        Value::Matrix(m) => Some(format_matrix(m, mode)),
8938        Value::ComplexMatrix(m) => Some(format_complex_matrix(m, mode)),
8939        Value::Cell(elems) => Some(format_cell(elems, mode)),
8940        Value::Struct(map) => Some(format_struct(map, mode)),
8941        Value::StructArray(arr) => Some(format_struct_array(arr, mode)),
8942        Value::DateTimeArray(v) => Some(format_datetime_array(v)),
8943        Value::DurationArray(v) => Some(format_duration_array(v)),
8944        Value::Map(m) => Some(format_map(m, mode)),
8945    }
8946}
8947
8948/// Formats a cell array in MATLAB-style multi-line display.
8949fn format_cell(elems: &[Value], mode: &FormatMode) -> String {
8950    if elems.is_empty() {
8951        return "  {}".to_string();
8952    }
8953    let mut lines = vec!["  {".to_string()];
8954    for (i, val) in elems.iter().enumerate() {
8955        let label = format!("    [1,{}]", i + 1);
8956        match val {
8957            Value::Matrix(_) => {
8958                lines.push(format!("{label}:"));
8959                if let Some(full) = format_value_full(val, mode) {
8960                    for line in full.lines() {
8961                        lines.push(format!("   {line}"));
8962                    }
8963                }
8964            }
8965            Value::Cell(_) => {
8966                lines.push(format!("{label}: {}", format_value(val, Base::Dec, mode)));
8967            }
8968            _ => {
8969                lines.push(format!("{label}: {}", format_value(val, Base::Dec, mode)));
8970            }
8971        }
8972    }
8973    lines.push("  }".to_string());
8974    lines.join("\n")
8975}
8976
8977/// Formats a struct in MATLAB 2014b+ multi-line style.
8978fn format_struct(map: &IndexMap<String, Value>, mode: &FormatMode) -> String {
8979    let mut lines = vec![
8980        String::new(),
8981        "  scalar structure containing the fields:".to_string(),
8982        String::new(),
8983    ];
8984    for (key, val) in map {
8985        let val_str = match val {
8986            Value::Struct(_) => "[1×1 struct]".to_string(),
8987            Value::StructArray(arr) => format!("[1×{} struct]", arr.len()),
8988            Value::Matrix(m) => format!("[{}×{} double]", m.nrows(), m.ncols()),
8989            Value::Cell(v) => format!("{{1×{} cell}}", v.len()),
8990            _ => format_value(val, Base::Dec, mode),
8991        };
8992        lines.push(format!("    {key}: {val_str}"));
8993    }
8994    lines.join("\n")
8995}
8996
8997/// Formats a 1×N struct array (shows each element's fields).
8998fn format_struct_array(arr: &[IndexMap<String, Value>], mode: &FormatMode) -> String {
8999    let n = arr.len();
9000    let mut lines = vec![
9001        String::new(),
9002        format!("  1×{n} struct array with fields:"),
9003        String::new(),
9004    ];
9005    // Collect field names from the first element
9006    if let Some(first) = arr.first() {
9007        for key in first.keys() {
9008            lines.push(format!("    {key}"));
9009        }
9010    }
9011    // Show first element's values if array has exactly 1 element
9012    if n == 1
9013        && let Some(first) = arr.first()
9014    {
9015        lines.clear();
9016        lines.push(String::new());
9017        lines.push("  scalar structure containing the fields:".to_string());
9018        lines.push(String::new());
9019        for (key, val) in first {
9020            let val_str = match val {
9021                Value::Struct(_) => "[1×1 struct]".to_string(),
9022                Value::StructArray(a) => format!("[1×{} struct]", a.len()),
9023                Value::Matrix(m) => format!("[{}×{} double]", m.nrows(), m.ncols()),
9024                Value::Cell(v) => format!("{{1×{} cell}}", v.len()),
9025                _ => format_value(val, Base::Dec, mode),
9026            };
9027            lines.push(format!("    {key}: {val_str}"));
9028        }
9029    }
9030    lines.join("\n")
9031}
9032
9033fn format_map(map: &IndexMap<String, Value>, mode: &FormatMode) -> String {
9034    let n = map.len();
9035    let mut lines = vec![
9036        String::new(),
9037        format!("  Map with {n} entries:"),
9038        String::new(),
9039    ];
9040    for (key, val) in map {
9041        let val_str = match val {
9042            Value::Struct(_) => "[1×1 struct]".to_string(),
9043            Value::Matrix(m) => format!("[{}×{} double]", m.nrows(), m.ncols()),
9044            Value::Cell(v) => format!("{{1×{} cell}}", v.len()),
9045            _ => format_value(val, Base::Dec, mode),
9046        };
9047        lines.push(format!("    '{key}' → {val_str}"));
9048    }
9049    lines.join("\n")
9050}
9051
9052fn format_datetime_array(v: &[f64]) -> String {
9053    let mut lines = Vec::with_capacity(v.len());
9054    for ts in v {
9055        lines.push(format!("  {}", crate::datetime::format_datetime(*ts)));
9056    }
9057    lines.join("\n")
9058}
9059
9060fn format_duration_array(v: &[f64]) -> String {
9061    let mut lines = Vec::with_capacity(v.len());
9062    for secs in v {
9063        lines.push(format!("  {}", crate::datetime::format_duration(*secs)));
9064    }
9065    lines.join("\n")
9066}
9067
9068/// Formats a complex matrix with right-aligned columns, 3-space indent, 3 spaces between columns.
9069/// Each element is formatted using [`format_complex`]; columns are aligned to the widest entry.
9070fn format_complex_matrix(m: &Array2<Complex<f64>>, mode: &FormatMode) -> String {
9071    if m.nrows() == 0 || m.ncols() == 0 {
9072        return "   []".to_string();
9073    }
9074    let ncols = m.ncols();
9075
9076    // Split each cell into (re_str, sign " + "/" - ", im_abs_str) so that the
9077    // real and imaginary parts can be right-aligned independently.  This keeps
9078    // the leading indent uniform regardless of how many digits each part needs.
9079    let parts: Vec<Vec<(String, &'static str, String)>> = m
9080        .rows()
9081        .into_iter()
9082        .map(|row| {
9083            row.iter()
9084                .map(|c| {
9085                    let re_str = format_decimal(c.re, mode);
9086                    let im_abs = format_decimal(c.im.abs(), mode);
9087                    let sign = if c.im < 0.0 { " - " } else { " + " };
9088                    (re_str, sign, im_abs)
9089                })
9090                .collect()
9091        })
9092        .collect();
9093
9094    // Per-column max widths for re and im parts independently.
9095    let re_widths: Vec<usize> = (0..ncols)
9096        .map(|c| parts.iter().map(|row| row[c].0.len()).max().unwrap_or(0))
9097        .collect();
9098    let im_widths: Vec<usize> = (0..ncols)
9099        .map(|c| parts.iter().map(|row| row[c].2.len()).max().unwrap_or(0))
9100        .collect();
9101
9102    let mut lines = Vec::new();
9103    for row in &parts {
9104        let mut line = String::from("   ");
9105        for (c, (re_str, sign, im_str)) in row.iter().enumerate() {
9106            if c > 0 {
9107                // Pad the previous column's im part so column boundaries stay fixed.
9108                // Inter-column gap is 3 spaces past the widest im in this column.
9109                let prev_im_pad = im_widths[c - 1].saturating_sub(row[c - 1].2.len());
9110                for _ in 0..prev_im_pad {
9111                    line.push(' ');
9112                }
9113                line.push_str("   ");
9114            }
9115            let re_pad = re_widths[c].saturating_sub(re_str.len());
9116            for _ in 0..re_pad {
9117                line.push(' ');
9118            }
9119            line.push_str(re_str);
9120            line.push_str(sign);
9121            line.push_str(im_str);
9122            line.push('i');
9123        }
9124        lines.push(line);
9125    }
9126    lines.join("\n")
9127}
9128
9129/// Formats a matrix with right-aligned columns, 3-space indent, 3 spaces between columns.
9130/// `FormatMode::Plus` renders a sign grid (`+`, `-`, `0`).
9131fn format_matrix(m: &Array2<f64>, mode: &FormatMode) -> String {
9132    if m.nrows() == 0 || m.ncols() == 0 {
9133        return "   []".to_string();
9134    }
9135    // Special rendering for format +
9136    if matches!(mode, FormatMode::Plus) {
9137        let lines: Vec<String> = m
9138            .rows()
9139            .into_iter()
9140            .map(|row| {
9141                let chars: String = row
9142                    .iter()
9143                    .map(|&x| {
9144                        if x > 0.0 {
9145                            '+'
9146                        } else if x < 0.0 {
9147                            '-'
9148                        } else {
9149                            '0'
9150                        }
9151                    })
9152                    .collect();
9153                format!("   {}", chars)
9154            })
9155            .collect();
9156        return lines.join("\n");
9157    }
9158    let ncols = m.ncols();
9159    let cells: Vec<Vec<String>> = m
9160        .rows()
9161        .into_iter()
9162        .map(|row| row.iter().map(|&x| format_decimal(x, mode)).collect())
9163        .collect();
9164    let col_widths: Vec<usize> = (0..ncols)
9165        .map(|c| cells.iter().map(|row| row[c].len()).max().unwrap_or(0))
9166        .collect();
9167    let mut lines = Vec::new();
9168    for row in &cells {
9169        let mut line = String::from("   ");
9170        for (c, cell) in row.iter().enumerate() {
9171            if c > 0 {
9172                line.push_str("   ");
9173            }
9174            let pad = col_widths[c].saturating_sub(cell.len());
9175            for _ in 0..pad {
9176                line.push(' ');
9177            }
9178            line.push_str(cell);
9179        }
9180        lines.push(line);
9181    }
9182    lines.join("\n")
9183}
9184
9185/// Formats a number in a non-decimal integer base (hex/bin/oct).
9186/// Rounds to the nearest integer before formatting.
9187pub fn format_non_dec(n: f64, base: Base) -> String {
9188    let i = n.round() as i64;
9189    let u = i.unsigned_abs();
9190    let sign = if i < 0 { "-" } else { "" };
9191    match base {
9192        Base::Hex => format!("{}0x{:X}", sign, u),
9193        Base::Bin => format!("{}0b{:b}", sign, u),
9194        Base::Oct => format!("{}0o{:o}", sign, u),
9195        Base::Dec => format_decimal(n, &FormatMode::default()),
9196    }
9197}
9198
9199// ---------------------------------------------------------------------------
9200// Internal decimal formatters
9201// ---------------------------------------------------------------------------
9202
9203fn format_decimal(n: f64, mode: &FormatMode) -> String {
9204    if n.is_nan() {
9205        return "NaN".to_string();
9206    }
9207    if n.is_infinite() {
9208        return if n > 0.0 { "Inf" } else { "-Inf" }.to_string();
9209    }
9210    match mode {
9211        FormatMode::Short | FormatMode::ShortG => fmt_auto_sig(n, 5),
9212        FormatMode::Long | FormatMode::LongG => fmt_auto_sig(n, 15),
9213        FormatMode::ShortE => fmt_sci_dp(n, 4),
9214        FormatMode::LongE => fmt_sci_dp(n, 14),
9215        FormatMode::Bank => format!("{:.2}", n),
9216        FormatMode::Rat => fmt_rat(n),
9217        FormatMode::Hex => fmt_hex_ieee754(n),
9218        FormatMode::Plus => fmt_plus_sign(n),
9219        FormatMode::Custom(prec) => fmt_custom_prec(n, *prec),
9220    }
9221}
9222
9223/// Integer shortcut: fits in i64 without fractional part.
9224#[inline]
9225fn is_exact_int(n: f64) -> bool {
9226    n.fract() == 0.0 && n.abs() < 1e15
9227}
9228
9229/// Auto fixed/scientific with `sig` significant digits (MATLAB-compatible).
9230/// Uses fixed notation for exponents in [-3, sig), scientific otherwise.
9231/// Integers are shown without a decimal point.
9232fn fmt_auto_sig(n: f64, sig: usize) -> String {
9233    if is_exact_int(n) {
9234        return format!("{}", n as i64);
9235    }
9236    let abs_n = n.abs();
9237    let exp = if abs_n == 0.0 {
9238        0i32
9239    } else {
9240        abs_n.log10().floor() as i32
9241    };
9242    if exp >= -3 && exp < sig as i32 {
9243        let dp = (sig as i32 - 1 - exp) as usize;
9244        let s = format!("{:.prec$}", n, prec = dp);
9245        // Only strip trailing zeros when there is a decimal point.
9246        if s.contains('.') {
9247            s.trim_end_matches('0').trim_end_matches('.').to_string()
9248        } else {
9249            s
9250        }
9251    } else {
9252        let s = format!("{:.prec$e}", n, prec = sig - 1);
9253        trim_sci(&s)
9254    }
9255}
9256
9257/// Always scientific notation with `dp` decimal places.
9258fn fmt_sci_dp(n: f64, dp: usize) -> String {
9259    let s = format!("{:.prec$e}", n, prec = dp);
9260    trim_sci(&s)
9261}
9262
9263/// Legacy custom-precision: N decimal places, auto fixed/scientific.
9264fn fmt_custom_prec(n: f64, prec: usize) -> String {
9265    if is_exact_int(n) {
9266        return format!("{}", n as i64);
9267    }
9268    if n.abs() >= 1e15 || (n != 0.0 && n.abs() < 1e-9) {
9269        let s = format!("{:.prec$e}", n, prec = prec);
9270        trim_sci(&s)
9271    } else {
9272        let s = format!("{:.prec$}", n, prec = prec);
9273        s.trim_end_matches('0').trim_end_matches('.').to_string()
9274    }
9275}
9276
9277/// Rational approximation via continued fractions. Returns `"p/q"` or `"p"` if denominator is 1.
9278fn fmt_rat(n: f64) -> String {
9279    if is_exact_int(n) {
9280        return format!("{}", n as i64);
9281    }
9282    let sign = if n < 0.0 { -1i64 } else { 1i64 };
9283    let x = n.abs();
9284    let (mut h1, mut h2): (i64, i64) = (1, 0);
9285    let (mut k1, mut k2): (i64, i64) = (0, 1);
9286    let mut b = x;
9287    for _ in 0..64 {
9288        let a = b.floor() as i64;
9289        let (nh, nk) = (a * h1 + h2, a * k1 + k2);
9290        if nk > 10_000 {
9291            break;
9292        }
9293        h2 = h1;
9294        h1 = nh;
9295        k2 = k1;
9296        k1 = nk;
9297        let frac = b - a as f64;
9298        if frac < 1e-12 || (h1 as f64 / k1 as f64 - x).abs() < 1e-6 {
9299            break;
9300        }
9301        b = 1.0 / frac;
9302    }
9303    let p = sign * h1;
9304    if k1 == 1 {
9305        format!("{}", p)
9306    } else {
9307        format!("{}/{}", p, k1)
9308    }
9309}
9310
9311/// IEEE 754 double-precision bit pattern as 16 uppercase hex digits.
9312fn fmt_hex_ieee754(n: f64) -> String {
9313    format!("{:016X}", n.to_bits())
9314}
9315
9316/// Sign indicator: `+`, `-`, or ` ` for zero.
9317fn fmt_plus_sign(n: f64) -> String {
9318    if n > 0.0 {
9319        "+".to_string()
9320    } else if n < 0.0 {
9321        "-".to_string()
9322    } else {
9323        " ".to_string()
9324    }
9325}
9326
9327fn trim_sci(s: &str) -> String {
9328    if let Some(e_pos) = s.find('e') {
9329        let mantissa = s[..e_pos].trim_end_matches('0').trim_end_matches('.');
9330        let exp_str = &s[e_pos + 1..];
9331        let (sign, digits) = if let Some(d) = exp_str.strip_prefix('-') {
9332            ("-", d)
9333        } else if let Some(d) = exp_str.strip_prefix('+') {
9334            ("+", d)
9335        } else {
9336            ("+", exp_str)
9337        };
9338        let exp_num: i32 = digits.parse().unwrap_or(0);
9339        format!("{}e{}{:02}", mantissa, sign, exp_num)
9340    } else {
9341        s.to_string()
9342    }
9343}
9344
9345// --- MAT built-in helpers ---
9346
9347/// Loads a MATLAB Level 5/7 MAT file and returns a [`Value::Struct`].
9348///
9349/// Requires the `mat` Cargo feature; without it, always returns an error.
9350pub fn load_mat_file(path: &str) -> Result<Value, String> {
9351    load_mat_file_impl(path)
9352}
9353
9354#[cfg(feature = "mat")]
9355fn load_mat_file_impl(path: &str) -> Result<Value, String> {
9356    crate::mat::mat_load(path)
9357}
9358
9359#[cfg(not(feature = "mat"))]
9360fn load_mat_file_impl(_path: &str) -> Result<Value, String> {
9361    Err("load: .mat support not available — rebuild with --features mat".to_string())
9362}
9363
9364// --- Regex built-in helpers ---
9365
9366#[cfg(feature = "regex")]
9367fn regexp_impl(
9368    fname: &str,
9369    s: &str,
9370    pat: &str,
9371    ignore_case: bool,
9372    return_match: bool,
9373) -> Result<Value, String> {
9374    use ndarray::Array2;
9375    let full_pat = if ignore_case {
9376        format!("(?i){pat}")
9377    } else {
9378        pat.to_string()
9379    };
9380    let re = regex::Regex::new(&full_pat).map_err(|e| format!("{fname}: invalid pattern: {e}"))?;
9381    if return_match {
9382        let matches: Vec<Value> = re
9383            .find_iter(s)
9384            .map(|m| Value::Str(m.as_str().to_string()))
9385            .collect();
9386        Ok(Value::Cell(Box::new(matches)))
9387    } else {
9388        match re.find(s) {
9389            Some(m) => Ok(Value::Scalar((s[..m.start()].chars().count() + 1) as f64)),
9390            None => Ok(Value::Matrix(Box::new(Array2::zeros((0, 0))))),
9391        }
9392    }
9393}
9394
9395#[cfg(not(feature = "regex"))]
9396fn regexp_impl(
9397    fname: &str,
9398    _s: &str,
9399    _pat: &str,
9400    _ignore_case: bool,
9401    _return_match: bool,
9402) -> Result<Value, String> {
9403    Err(format!(
9404        "{fname}: not available — rebuild with --features regex"
9405    ))
9406}
9407
9408#[cfg(feature = "regex")]
9409fn regexprep_impl(s: &str, pat: &str, rep: &str) -> Result<Value, String> {
9410    let re = regex::Regex::new(pat).map_err(|e| format!("regexprep: invalid pattern: {e}"))?;
9411    let result = re.replace_all(s, regex::NoExpand(rep));
9412    Ok(Value::Str(result.into_owned()))
9413}
9414
9415#[cfg(not(feature = "regex"))]
9416fn regexprep_impl(_s: &str, _pat: &str, _rep: &str) -> Result<Value, String> {
9417    Err("regexprep: not available — rebuild with --features regex".to_string())
9418}
9419
9420// ── Phase 26 — FFT built-in helpers ─────────────────────────────────────────
9421
9422/// Extracts a flat real vector from a Scalar or 1-D Matrix (row or column).
9423#[cfg(feature = "fft")]
9424fn extract_real_vec(v: &Value, name: &str) -> Result<Vec<f64>, String> {
9425    match v {
9426        Value::Scalar(s) => Ok(vec![*s]),
9427        Value::Matrix(m) if m.nrows() == 1 || m.ncols() == 1 => Ok(m.iter().copied().collect()),
9428        Value::Matrix(m) => Err(format!(
9429            "{name}: input must be a vector (got {}×{} matrix)",
9430            m.nrows(),
9431            m.ncols()
9432        )),
9433        _ => Err(format!("{name}: input must be a real numeric vector")),
9434    }
9435}
9436
9437/// Wraps a `Vec<(f64,f64)>` of FFT output into a 1×N `Value::ComplexMatrix`.
9438#[cfg(feature = "fft")]
9439fn complex_pairs_to_complex_matrix(data: Vec<(f64, f64)>) -> Value {
9440    let n = data.len();
9441    if n == 0 {
9442        return Value::ComplexMatrix(Box::new(Array2::zeros((1, 0))));
9443    }
9444    let elems: Vec<Complex<f64>> = data
9445        .into_iter()
9446        .map(|(re, im)| Complex::new(re, im))
9447        .collect();
9448    Value::ComplexMatrix(Box::new(Array2::from_shape_vec((1, n), elems).unwrap()))
9449}
9450
9451/// Extracts a flat complex vector from a [`Value::ComplexMatrix`], `Cell`, or real matrix.
9452#[cfg(feature = "fft")]
9453fn extract_complex_vec(v: &Value, name: &str) -> Result<Vec<(f64, f64)>, String> {
9454    match v {
9455        Value::Scalar(s) => Ok(vec![(*s, 0.0)]),
9456        Value::Matrix(m) => Ok(m.iter().copied().map(|x| (x, 0.0)).collect()),
9457        Value::ComplexMatrix(m) => Ok(m.iter().map(|c| (c.re, c.im)).collect()),
9458        Value::Cell(elems) => elems
9459            .iter()
9460            .enumerate()
9461            .map(|(i, e)| match e {
9462                Value::Complex(re, im) => Ok((*re, *im)),
9463                Value::Scalar(s) => Ok((*s, 0.0)),
9464                _ => Err(format!(
9465                    "{name}: cell element {} must be a complex or real number",
9466                    i + 1
9467                )),
9468            })
9469            .collect(),
9470        _ => Err(format!(
9471            "{name}: input must be a complex matrix, cell array, or numeric vector"
9472        )),
9473    }
9474}
9475
9476#[cfg(feature = "fft")]
9477fn fft_call(v: &Value, n_opt: Option<usize>) -> Result<Value, String> {
9478    let real = extract_real_vec(v, "fft")?;
9479    let n = n_opt.unwrap_or(real.len());
9480    if n == 0 {
9481        return Err("fft: length must be positive".to_string());
9482    }
9483    let out = crate::fft::fft_forward(&real, n);
9484    Ok(complex_pairs_to_complex_matrix(out))
9485}
9486
9487#[cfg(not(feature = "fft"))]
9488fn fft_call(_v: &Value, _n_opt: Option<usize>) -> Result<Value, String> {
9489    Err("fft: not available — rebuild with --features fft".to_string())
9490}
9491
9492#[cfg(feature = "fft")]
9493fn ifft_call(v: &Value) -> Result<Value, String> {
9494    let complex = extract_complex_vec(v, "ifft")?;
9495    if complex.is_empty() {
9496        return Ok(Value::Matrix(Box::new(ndarray::Array2::zeros((1, 0)))));
9497    }
9498    let out = crate::fft::fft_inverse(&complex);
9499    if out.iter().all(|(_, im)| im.abs() < 1e-12) {
9500        let real: Vec<f64> = out.iter().map(|(re, _)| *re).collect();
9501        let n = real.len();
9502        Ok(Value::Matrix(Box::new(
9503            ndarray::Array2::from_shape_vec((1, n), real).unwrap(),
9504        )))
9505    } else {
9506        Ok(complex_pairs_to_complex_matrix(out))
9507    }
9508}
9509
9510#[cfg(not(feature = "fft"))]
9511fn ifft_call(_v: &Value) -> Result<Value, String> {
9512    Err("ifft: not available — rebuild with --features fft".to_string())
9513}
9514
9515// --- JSON built-in helpers ---
9516
9517#[cfg(feature = "json")]
9518fn jsondecode_impl(arg: &Value) -> Result<Value, String> {
9519    let s = match arg {
9520        Value::Str(s) | Value::StringObj(s) => s.as_str(),
9521        _ => return Err("jsondecode: argument must be a string".to_string()),
9522    };
9523    let jval: serde_json::Value =
9524        serde_json::from_str(s).map_err(|e| format!("jsondecode: invalid JSON: {e}"))?;
9525    Ok(crate::json::json_to_value(&jval))
9526}
9527
9528#[cfg(not(feature = "json"))]
9529fn jsondecode_impl(_arg: &Value) -> Result<Value, String> {
9530    Err("jsondecode: not available — rebuild with --features json".to_string())
9531}
9532
9533#[cfg(feature = "json")]
9534fn jsonencode_impl(arg: &Value) -> Result<Value, String> {
9535    let jval = crate::json::value_to_json(arg)?;
9536    let s = serde_json::to_string(&jval)
9537        .map_err(|e| format!("jsonencode: serialization error: {e}"))?;
9538    Ok(Value::Str(s))
9539}
9540
9541#[cfg(not(feature = "json"))]
9542fn jsonencode_impl(_arg: &Value) -> Result<Value, String> {
9543    Err("jsonencode: not available — rebuild with --features json".to_string())
9544}
9545
9546// ---------------------------------------------------------------------------
9547// Phase 24 — Polynomial helpers
9548// ---------------------------------------------------------------------------
9549
9550/// Evaluates a polynomial with real coefficients at a complex point using Horner's method.
9551fn cpoly_eval(coeffs: &[f64], z: (f64, f64)) -> (f64, f64) {
9552    let mut acc = (0.0_f64, 0.0_f64);
9553    for &c in coeffs {
9554        // acc = acc * z + c
9555        acc = (acc.0 * z.0 - acc.1 * z.1 + c, acc.0 * z.1 + acc.1 * z.0);
9556    }
9557    acc
9558}
9559
9560/// Evaluates a polynomial at a real point using Horner's method.
9561fn horner(coeffs: &[f64], x: f64) -> f64 {
9562    coeffs.iter().fold(0.0, |acc, &c| acc * x + c)
9563}
9564
9565/// Extracts polynomial (or 1-D knot) coefficients from a scalar or row/column vector `Value`.
9566fn poly_coeffs(v: &Value, fname: &str) -> Result<Vec<f64>, String> {
9567    match v {
9568        Value::Scalar(s) => Ok(vec![*s]),
9569        Value::Matrix(m) => {
9570            if m.nrows() == 1 {
9571                Ok(m.row(0).iter().copied().collect())
9572            } else if m.ncols() == 1 {
9573                Ok(m.column(0).iter().copied().collect())
9574            } else {
9575                Err(format!(
9576                    "{fname}: argument must be a vector, got {}×{}",
9577                    m.nrows(),
9578                    m.ncols()
9579                ))
9580            }
9581        }
9582        _ => Err(format!("{fname}: argument must be a real numeric vector")),
9583    }
9584}
9585
9586/// Discrete linear convolution of two sequences. Result length = `a.len() + b.len() − 1`.
9587fn poly_conv(a: &[f64], b: &[f64]) -> Vec<f64> {
9588    if a.is_empty() || b.is_empty() {
9589        return vec![];
9590    }
9591    let mut result = vec![0.0_f64; a.len() + b.len() - 1];
9592    for (i, &ai) in a.iter().enumerate() {
9593        for (j, &bj) in b.iter().enumerate() {
9594            result[i + j] += ai * bj;
9595        }
9596    }
9597    result
9598}
9599
9600/// Polynomial long division `c / b` → `(quotient, remainder)`.
9601///
9602/// The remainder has the same length as `c` (MATLAB convention), satisfying
9603/// `conv(b, q) + r == c` element-wise.
9604fn poly_deconv(c: &[f64], b: &[f64]) -> Result<(Vec<f64>, Vec<f64>), String> {
9605    if b.is_empty() || b.iter().all(|&x| x == 0.0) {
9606        return Err("deconv: divisor polynomial must not be zero".to_string());
9607    }
9608    let mc = c.len();
9609    let mb = b.len();
9610    if mb > mc {
9611        return Ok((vec![0.0], c.to_vec()));
9612    }
9613    let q_len = mc - mb + 1;
9614    let mut remainder = c.to_vec();
9615    let mut q = vec![0.0_f64; q_len];
9616    for i in 0..q_len {
9617        let coeff = remainder[i] / b[0];
9618        q[i] = coeff;
9619        for j in 0..mb {
9620            remainder[i + j] -= coeff * b[j];
9621        }
9622    }
9623    // Zero out rounding residuals relative to the input scale
9624    let scale = c.iter().map(|v| v.abs()).fold(0.0_f64, f64::max).max(1.0);
9625    for x in &mut remainder {
9626        if x.abs() < 1e-10 * scale {
9627            *x = 0.0;
9628        }
9629    }
9630    Ok((q, remainder))
9631}
9632
9633/// Finds all roots of `coeffs` (degree = `coeffs.len() − 1`) using the
9634/// Durand–Kerner (Weierstrass) iteration.
9635///
9636/// Returns roots as `(re, im)` pairs sorted by descending real part, then
9637/// descending imaginary part.
9638fn durand_kerner(coeffs: &[f64]) -> Result<Vec<(f64, f64)>, String> {
9639    let n = coeffs.len() - 1; // degree
9640    if n == 0 {
9641        return Ok(vec![]);
9642    }
9643    let lc = coeffs[0];
9644    if lc == 0.0 {
9645        return Err("roots: leading coefficient must not be zero".to_string());
9646    }
9647    // Normalize to monic polynomial
9648    let monic: Vec<f64> = coeffs.iter().map(|&c| c / lc).collect();
9649
9650    // Cauchy root bound: all roots have |z| ≤ r
9651    let r = 1.0 + monic[1..].iter().map(|c| c.abs()).fold(0.0_f64, f64::max);
9652
9653    // Initial guesses on a circle, rotated by 0.25/n turns to avoid the real axis
9654    // (a purely real start can stall for polynomials with purely imaginary roots).
9655    let mut z: Vec<(f64, f64)> = (0..n)
9656        .map(|k| {
9657            let angle = 2.0 * std::f64::consts::PI * (k as f64 + 0.25) / n as f64;
9658            (r * angle.cos(), r * angle.sin())
9659        })
9660        .collect();
9661
9662    const MAX_ITER: usize = 2000;
9663    const EPS: f64 = 1e-12;
9664
9665    for _ in 0..MAX_ITER {
9666        let z_old = z.clone();
9667        let mut max_corr = 0.0_f64;
9668        for i in 0..n {
9669            let (pre, pim) = cpoly_eval(&monic, z_old[i]);
9670            // denominator = Π_{j≠i}(z_i − z_j)
9671            let mut dre = 1.0_f64;
9672            let mut dim = 0.0_f64;
9673            for j in 0..n {
9674                if j == i {
9675                    continue;
9676                }
9677                let (dr, di) = (z_old[i].0 - z_old[j].0, z_old[i].1 - z_old[j].1);
9678                (dre, dim) = (dre * dr - dim * di, dre * di + dim * dr);
9679            }
9680            // correction = p(z_i) / denom
9681            let d2 = dre * dre + dim * dim;
9682            let (cre, cim) = if d2 > 0.0 {
9683                ((pre * dre + pim * dim) / d2, (pim * dre - pre * dim) / d2)
9684            } else {
9685                (pre, pim)
9686            };
9687            let corr_abs = (cre * cre + cim * cim).sqrt();
9688            max_corr = max_corr.max(corr_abs);
9689            z[i] = (z_old[i].0 - cre, z_old[i].1 - cim);
9690        }
9691        if max_corr < EPS {
9692            break;
9693        }
9694    }
9695
9696    // Sort by descending real part, then descending imaginary part
9697    z.sort_by(|a, b| {
9698        b.0.partial_cmp(&a.0)
9699            .unwrap_or(std::cmp::Ordering::Equal)
9700            .then(b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal))
9701    });
9702
9703    Ok(z)
9704}
9705
9706/// Converts a list of complex roots into a `Value`.
9707///
9708/// Returns a real `Matrix` (column vector) when all imaginary parts are below
9709/// `1e-9`; otherwise returns a `Cell` of `Scalar`/`Complex` elements.
9710fn roots_to_value(roots: &[(f64, f64)]) -> Value {
9711    const IMAG_TOL: f64 = 1e-9;
9712    let all_real = roots.iter().all(|(_, im)| im.abs() < IMAG_TOL);
9713    if all_real {
9714        let data: Vec<f64> = roots.iter().map(|(re, _)| *re).collect();
9715        let n = data.len();
9716        Value::Matrix(Box::new(Array2::from_shape_vec((n, 1), data).unwrap()))
9717    } else {
9718        let vals: Vec<Value> = roots
9719            .iter()
9720            .map(|&(re, im)| {
9721                if im.abs() < IMAG_TOL {
9722                    Value::Scalar(re)
9723                } else {
9724                    Value::Complex(re, im)
9725                }
9726            })
9727            .collect();
9728        Value::Cell(Box::new(vals))
9729    }
9730}
9731
9732/// Computes the characteristic polynomial of a square matrix using the
9733/// Faddeev-LeVerrier algorithm.
9734///
9735/// Returns coefficients `[1, c_{n-1}, …, c_0]` in descending degree order.
9736fn characteristic_poly(a: &Array2<f64>) -> Result<Vec<f64>, String> {
9737    let n = a.nrows();
9738    if a.ncols() != n {
9739        return Err("poly: matrix must be square".to_string());
9740    }
9741    if n == 0 {
9742        return Ok(vec![1.0]);
9743    }
9744    let mut coeffs = vec![0.0_f64; n + 1];
9745    coeffs[0] = 1.0;
9746    let mut nk = Array2::<f64>::eye(n); // N_0 = I
9747    for (k, coeff) in coeffs.iter_mut().enumerate().skip(1) {
9748        let ank = a.dot(&nk); // A * N_{k-1}
9749        let tr: f64 = (0..n).map(|i| ank[[i, i]]).sum();
9750        let ak = -tr / k as f64;
9751        *coeff = ak;
9752        nk = ank; // N_k = A*N_{k-1} + a_k*I
9753        for i in 0..n {
9754            nk[[i, i]] += ak;
9755        }
9756    }
9757    Ok(coeffs)
9758}
9759
9760/// Back-substitution solver for upper-triangular system `R * x = b`.
9761fn poly_back_sub(r: &Array2<f64>, b: &[f64]) -> Result<Vec<f64>, String> {
9762    let n = r.nrows();
9763    let mut x = vec![0.0_f64; n];
9764    for i in (0..n).rev() {
9765        let mut s = b[i];
9766        for j in (i + 1)..n {
9767            s -= r[[i, j]] * x[j];
9768        }
9769        if r[[i, i]].abs() < 1e-14 {
9770            return Err(
9771                "polyfit: Vandermonde matrix is rank-deficient; reduce polynomial degree"
9772                    .to_string(),
9773            );
9774        }
9775        x[i] = s / r[[i, i]];
9776    }
9777    Ok(x)
9778}
9779
9780/// Evaluates `interp1` at a single query point `xi` using the given `method`.
9781///
9782/// Returns `NaN` for queries outside `[x[0], x[n-1]]`.
9783fn interp1_at(x: &[f64], y: &[f64], xi: f64, method: &str) -> f64 {
9784    let n = x.len();
9785    if xi < x[0] || xi > x[n - 1] {
9786        return f64::NAN;
9787    }
9788    // Index of the leftmost knot ≤ xi (in [0, n-1])
9789    let lo = x.partition_point(|&xk| xk <= xi).saturating_sub(1);
9790    // For methods that need a right neighbour, clamp to n-2
9791    let lo2 = lo.min(n - 2);
9792    match method {
9793        "nearest" => {
9794            if lo == n - 1 {
9795                return y[n - 1];
9796            }
9797            if (xi - x[lo2]) <= (x[lo2 + 1] - xi) {
9798                y[lo2]
9799            } else {
9800                y[lo2 + 1]
9801            }
9802        }
9803        "previous" => y[lo],
9804        "next" => {
9805            if lo == n - 1 || xi == x[lo] {
9806                y[lo]
9807            } else {
9808                y[lo2 + 1]
9809            }
9810        }
9811        _ => {
9812            // "linear" (default)
9813            if lo == n - 1 {
9814                return y[n - 1];
9815            }
9816            let t = (xi - x[lo2]) / (x[lo2 + 1] - x[lo2]);
9817            y[lo2] + t * (y[lo2 + 1] - y[lo2])
9818        }
9819    }
9820}
9821
9822#[cfg(test)]
9823#[path = "eval_tests.rs"]
9824mod tests;