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    /// Package-qualified function call: `pkg.func(args)` or `pkg.sub.func(args)`.
456    ///
457    /// `segments` holds the dot-separated name components, e.g. `["utils", "my_function"]`.
458    /// At eval time:
459    /// - If `segments[0]` is in the environment (a struct or callable), the chain is followed
460    ///   as field accesses and the final value is called with the given arguments.
461    /// - Otherwise, the segments are treated as a package call: the autoload hook searches
462    ///   for `+utils/my_function.calc` (or `+utils/+sub/func.calc` for nested packages)
463    ///   on the session path and loads the function on demand.
464    DotCall(Vec<String>, Vec<Expr>),
465    /// Not-a-Time sentinel: `NaT`. Evaluates to `Value::DateTime(f64::NAN)`.
466    NaT,
467}
468
469/// A binary operator used in [`Expr::BinOp`].
470#[derive(Debug, Clone)]
471pub enum Op {
472    /// Addition: `a + b` or element-wise matrix addition.
473    Add,
474    /// Subtraction: `a - b` or element-wise matrix subtraction.
475    Sub,
476    /// Multiplication: scalar `a * b` or matrix product `A * B`.
477    Mul,
478    /// Division: scalar `a / b` or matrix right-division `A / B` (solves `X * B = A`).
479    Div,
480    /// Exponentiation: scalar `a ^ b` or matrix power `A ^ n`.
481    Pow,
482    /// Element-wise multiplication: `A .* B`.
483    ElemMul,
484    /// Element-wise division: `A ./ B`.
485    ElemDiv,
486    /// Element-wise exponentiation: `A .^ B`.
487    ElemPow,
488    // --- Comparison (element-wise, return 0.0/1.0) ---
489    /// Equality comparison: `a == b`. Returns 1.0 if equal, 0.0 otherwise.
490    Eq,
491    /// Inequality comparison: `a ~= b`. Returns 1.0 if not equal, 0.0 otherwise.
492    NotEq,
493    /// Less-than comparison: `a < b`.
494    Lt,
495    /// Greater-than comparison: `a > b`.
496    Gt,
497    /// Less-than-or-equal comparison: `a <= b`.
498    LtEq,
499    /// Greater-than-or-equal comparison: `a >= b`.
500    GtEq,
501    // --- Short-circuit logical (scalars only) ---
502    /// Short-circuit logical AND: `a && b`. Only evaluates `b` if `a` is truthy.
503    And,
504    /// Short-circuit logical OR: `a || b`. Only evaluates `b` if `a` is falsy.
505    Or,
506    // --- Element-wise logical (matrices allowed, no short-circuit) ---
507    /// Element-wise logical AND: `A & B`. Evaluates both sides; works on matrices.
508    ElemAnd,
509    /// Element-wise logical OR: `A | B`. Evaluates both sides; works on matrices.
510    ElemOr,
511    /// Left division: `A \ b` solves `A*x = b`. Scalar: `a \ b = b / a`.
512    LDiv,
513}
514
515/// The numeric base used when displaying integer-valued scalars.
516#[derive(Debug, Clone, Copy, PartialEq, Default)]
517pub enum Base {
518    /// Decimal (base 10) — the default.
519    #[default]
520    Dec,
521    /// Hexadecimal (base 16), prefix `0x` (e.g. `0xff`).
522    Hex,
523    /// Binary (base 2), prefix `0b` (e.g. `0b1010`).
524    Bin,
525    /// Octal (base 8), prefix `0o` (e.g. `0o17`).
526    Oct,
527}
528
529/// Controls how numbers are displayed (MATLAB-compatible format modes).
530#[derive(Debug, Clone, PartialEq)]
531pub enum FormatMode {
532    /// 5 significant digits, auto fixed/scientific (MATLAB `format short`).
533    Short,
534    /// 15 significant digits, auto fixed/scientific (MATLAB `format long`).
535    Long,
536    /// Always scientific notation, 4 decimal places — 5 sig digits.
537    ShortE,
538    /// Always scientific notation, 14 decimal places — 15 sig digits.
539    LongE,
540    /// Same as `Short` for scalars (MATLAB `format shortG`).
541    ShortG,
542    /// Same as `Long` for scalars (MATLAB `format longG`).
543    LongG,
544    /// Fixed 2 decimal places — currency (MATLAB `format bank`).
545    Bank,
546    /// Rational approximation `p/q` (MATLAB `format rat`).
547    Rat,
548    /// IEEE 754 hexadecimal bit pattern, 16 uppercase hex digits (MATLAB `format hex`).
549    Hex,
550    /// Sign character only: `+`, `-`, or ` ` for zero (MATLAB `format +`).
551    Plus,
552    /// N decimal places, auto fixed/scientific — legacy precision= setting.
553    Custom(usize),
554}
555
556impl Default for FormatMode {
557    fn default() -> Self {
558        FormatMode::Custom(10)
559    }
560}
561
562impl FormatMode {
563    /// Human-readable name for display in `config` / status messages.
564    pub fn name(&self) -> String {
565        match self {
566            FormatMode::Short => "short".to_string(),
567            FormatMode::Long => "long".to_string(),
568            FormatMode::ShortE => "shortE".to_string(),
569            FormatMode::LongE => "longE".to_string(),
570            FormatMode::ShortG => "shortG".to_string(),
571            FormatMode::LongG => "longG".to_string(),
572            FormatMode::Bank => "bank".to_string(),
573            FormatMode::Rat => "rat".to_string(),
574            FormatMode::Hex => "hex".to_string(),
575            FormatMode::Plus => "+".to_string(),
576            FormatMode::Custom(n) => format!("custom({n})"),
577        }
578    }
579}
580
581/// Evaluates an expression without file I/O context.
582/// This is the public API used by tests and non-I/O evaluation paths.
583pub fn eval(expr: &Expr, env: &Env) -> Result<Value, String> {
584    eval_inner(expr, env, None)
585}
586
587/// Evaluates an expression with an I/O context (file descriptor table).
588/// Used by the REPL to support `fopen`/`fclose`/`fgetl`/`fgets`/`fprintf(fd,...)`.
589pub fn eval_with_io(expr: &Expr, env: &Env, io: &mut IoContext) -> Result<Value, String> {
590    eval_inner(expr, env, Some(io))
591}
592
593fn eval_inner(expr: &Expr, env: &Env, mut io: Option<&mut IoContext>) -> Result<Value, String> {
594    match expr {
595        Expr::Number(n) => Ok(Value::Scalar(*n)),
596        Expr::Var(name) => env.get(name).cloned().ok_or(()).or_else(|_| {
597            // Check the shared global store when the name is declared global in this scope.
598            if is_global(name)
599                && let Some(val) = global_get(name)
600            {
601                return Ok(val);
602            }
603            // 'e' falls back to Euler's number if not defined in env
604            if name == "e" {
605                return Ok(Value::Scalar(std::f64::consts::E));
606            }
607            // Try as a zero-argument built-in call (e.g., `tic`, `toc` written without parens).
608            if let Ok(val) = call_builtin(name, &[], env, io.as_deref_mut()) {
609                return Ok(val);
610            }
611            let hint = suggest_similar(name, env);
612            match hint {
613                Some(s) => Err(format!("Undefined variable '{name}'; did you mean '{s}'?")),
614                None => Err(format!("Undefined variable: '{name}'")),
615            }
616        }),
617        Expr::UnaryMinus(e) => match eval_inner(e, env, io)? {
618            Value::Void => Err("Unary minus is not applicable to void".to_string()),
619            Value::Scalar(n) => Ok(Value::Scalar(-n)),
620            Value::Matrix(m) => Ok(Value::Matrix(m.mapv(|x| -x))),
621            Value::Complex(re, im) => Ok(Value::Complex(-re, -im)),
622            Value::ComplexMatrix(m) => Ok(Value::ComplexMatrix(m.mapv(|c| -c))),
623            Value::Str(s) => match str_to_numeric(&s) {
624                Value::Scalar(n) => Ok(Value::Scalar(-n)),
625                Value::Matrix(m) => Ok(Value::Matrix(m.mapv(|x| -x))),
626                _ => unreachable!(),
627            },
628            Value::StringObj(_) => {
629                Err("Unary minus is not applicable to string objects".to_string())
630            }
631            Value::Lambda(_)
632            | Value::Function { .. }
633            | Value::Tuple(_)
634            | Value::Cell(_)
635            | Value::Struct(_)
636            | Value::StructArray(_)
637            | Value::DateTime(_)
638            | Value::Duration(_)
639            | Value::DateTimeArray(_)
640            | Value::DurationArray(_) => {
641                Err("Unary minus is not applicable to this type".to_string())
642            }
643        },
644        Expr::UnaryNot(e) => match eval_inner(e, env, io)? {
645            Value::Void => Err("Logical NOT is not applicable to void".to_string()),
646            Value::Scalar(n) => Ok(Value::Scalar(if n == 0.0 { 1.0 } else { 0.0 })),
647            Value::Matrix(m) => Ok(Value::Matrix(m.mapv(|x| if x == 0.0 { 1.0 } else { 0.0 }))),
648            Value::Complex(re, im) => Ok(Value::Scalar(if re == 0.0 && im == 0.0 {
649                1.0
650            } else {
651                0.0
652            })),
653            Value::ComplexMatrix(m) => {
654                Ok(Value::Matrix(m.mapv(|c| {
655                    if c.re == 0.0 && c.im == 0.0 { 1.0 } else { 0.0 }
656                })))
657            }
658            Value::Str(s) => match str_to_numeric(&s) {
659                Value::Scalar(n) => Ok(Value::Scalar(if n == 0.0 { 1.0 } else { 0.0 })),
660                Value::Matrix(m) => Ok(Value::Matrix(m.mapv(|x| if x == 0.0 { 1.0 } else { 0.0 }))),
661                _ => unreachable!(),
662            },
663            Value::StringObj(_) => {
664                Err("Logical NOT is not applicable to string objects".to_string())
665            }
666            Value::Lambda(_)
667            | Value::Function { .. }
668            | Value::Tuple(_)
669            | Value::Cell(_)
670            | Value::Struct(_)
671            | Value::StructArray(_)
672            | Value::DateTime(_)
673            | Value::Duration(_)
674            | Value::DateTimeArray(_)
675            | Value::DurationArray(_) => {
676                Err("Logical NOT is not applicable to this type".to_string())
677            }
678        },
679        Expr::BinOp(left, op, right) => {
680            let l = eval_inner(left, env, io.as_deref_mut())?;
681            let r = eval_inner(right, env, io)?;
682            eval_binop(l, op, r)
683        }
684        Expr::Call(name, args) => {
685            // try(expr, default) — special form: evaluate expr; on error evaluate default.
686            // Arguments are NOT pre-evaluated; lazy semantics.
687            if name == "try" && args.len() == 2 {
688                return match eval_inner(&args[0], env, io.as_deref_mut()) {
689                    Ok(v) => Ok(v),
690                    Err(msg) => {
691                        set_last_err(&msg);
692                        eval_inner(&args[1], env, io.as_deref_mut())
693                    }
694                };
695            }
696
697            // If the name resolves to a variable in env, check its type.
698            // User functions (Lambda, Function) are called; other values are indexed.
699            // Variables shadow built-in function names (Octave semantics).
700            //
701            // Non-function variables are forwarded via borrow (no clone) to avoid
702            // copying large matrix values on every indexed read (e.g. x(k) in a loop).
703            if let Some(env_val) = env.get(name) {
704                if !matches!(env_val, Value::Lambda(_) | Value::Function { .. }) {
705                    return eval_index(env_val, args, env);
706                }
707                // Lambda/Function: clone is cheap (Rc for Lambda, Strings for Function).
708                let val = env_val.clone();
709                match &val {
710                    Value::Lambda(f) => {
711                        // Evaluate arguments and call the closure directly.
712                        // Empty call → inject ans (convenience: sq() = sq(ans)).
713                        let mut evaled = Vec::with_capacity(args.len().max(1));
714                        for a in args {
715                            evaled.push(eval_inner(a, env, io.as_deref_mut())?);
716                        }
717                        if evaled.is_empty() {
718                            evaled.push(env.get("ans").cloned().unwrap_or(Value::Scalar(0.0)));
719                        }
720                        let f = f.clone();
721                        return f.0(&evaled, io);
722                    }
723                    Value::Function { .. } => {
724                        // Evaluate arguments and dispatch to the registered hook in exec.rs.
725                        // User functions receive the raw arg list — NO ans injection. Empty call
726                        // means no arguments (varargin = {}), matching MATLAB semantics.
727                        let mut evaled = Vec::with_capacity(args.len());
728                        for a in args {
729                            evaled.push(eval_inner(a, env, io.as_deref_mut())?);
730                        }
731                        return match io.as_deref_mut() {
732                            Some(io_ref) => FN_CALL_HOOK.with(|c| match c.get() {
733                                Some(hook) => hook(name, &val, &evaled, env, io_ref),
734                                None => Err(format!(
735                                    "'{name}': user function execution not initialized \
736                                         (call exec::init() first)"
737                                )),
738                            }),
739                            None => {
740                                // No I/O context — create a temporary one (functions that do
741                                // file I/O in this path will silently fail to open files).
742                                let mut tmp_io = IoContext::new();
743                                FN_CALL_HOOK.with(|c| match c.get() {
744                                    Some(hook) => hook(name, &val, &evaled, env, &mut tmp_io),
745                                    None => Err(format!(
746                                        "'{name}': user function execution not initialized"
747                                    )),
748                                })
749                            }
750                        };
751                    }
752                    _ => unreachable!(),
753                }
754            }
755            // Autoload: search for <name>.calc / <name>.m if not in env.
756            // Check positive cache → negative (miss) cache → fire filesystem hook.
757            // Names that fail the hook are recorded in the miss cache so the
758            // filesystem is not searched again within the same session.
759            let autoloaded_val = AUTOLOAD_CACHE
760                .with(|c| c.borrow().get(name).cloned())
761                .or_else(|| {
762                    if AUTOLOAD_MISS_CACHE.with(|c| c.borrow().contains(name.as_str())) {
763                        return None;
764                    }
765                    let loaded = AUTOLOAD_HOOK
766                        .with(|c| c.get())
767                        .is_some_and(|hook| hook(name));
768                    if loaded {
769                        AUTOLOAD_CACHE.with(|c| c.borrow().get(name).cloned())
770                    } else {
771                        AUTOLOAD_MISS_CACHE.with(|c| c.borrow_mut().insert(name.to_string()));
772                        None
773                    }
774                });
775            if let Some(val) = autoloaded_val {
776                let mut evaled = Vec::with_capacity(args.len());
777                for a in args {
778                    evaled.push(eval_inner(a, env, io.as_deref_mut())?);
779                }
780                return match io.as_deref_mut() {
781                    Some(io_ref) => FN_CALL_HOOK.with(|c| match c.get() {
782                        Some(hook) => hook(name, &val, &evaled, env, io_ref),
783                        None => Err(format!("'{name}': exec::init() not called")),
784                    }),
785                    None => {
786                        let mut tmp_io = IoContext::new();
787                        FN_CALL_HOOK.with(|c| match c.get() {
788                            Some(hook) => hook(name, &val, &evaled, env, &mut tmp_io),
789                            None => Err(format!("'{name}': exec::init() not called")),
790                        })
791                    }
792                };
793            }
794
795            // Builtin path: empty call → inject ans (sqrt() = sqrt(ans)).
796            let mut evaled = Vec::with_capacity(args.len().max(1));
797            for a in args {
798                evaled.push(eval_inner(a, env, io.as_deref_mut())?);
799            }
800            // Don't inject ans for functions that take explicit struct/cell args
801            // or constructors where zero args is meaningful.
802            let no_ans_inject = matches!(
803                name.as_str(),
804                "struct"
805                    | "fieldnames"
806                    | "isfield"
807                    | "rmfield"
808                    | "isstruct"
809                    | "cell"
810                    | "iscell"
811                    | "isempty"
812                    | "cellfun"
813                    | "error"
814                    | "warning"
815                    | "lasterr"
816                    | "pcall"
817                    | "rand"
818                    | "randn"
819                    | "rng"
820                    | "tic"
821                    | "toc"
822            );
823            if evaled.is_empty() && !no_ans_inject {
824                evaled.push(env.get("ans").cloned().unwrap_or(Value::Scalar(0.0)));
825            }
826            call_builtin(name, &evaled, env, io)
827        }
828
829        Expr::Lambda {
830            params,
831            body,
832            source,
833        } => {
834            // Capture the current environment and body expression at definition time.
835            // The resulting Value::Lambda is a closure that binds params on each call.
836            let captured_env = env.clone();
837            let captured_params = params.clone();
838            let captured_body = *body.clone();
839            let src = source.clone();
840            let lambda = LambdaFn(
841                std::rc::Rc::new(move |args: &[Value], io: Option<&mut IoContext>| {
842                    // Allow up to params.len()+1 args: the parser injects `ans` for empty f() calls.
843                    let effective = if args.len() > captured_params.len() {
844                        if args.len() > captured_params.len() + 1 {
845                            return Err(format!(
846                                "Lambda: too many arguments (expected at most {}, got {})",
847                                captured_params.len(),
848                                args.len()
849                            ));
850                        }
851                        &args[..captured_params.len()]
852                    } else {
853                        args
854                    };
855                    let mut local_env = captured_env.clone();
856                    for (p, a) in captured_params.iter().zip(effective.iter()) {
857                        local_env.insert(p.clone(), a.clone());
858                    }
859                    local_env.insert("nargin".to_string(), Value::Scalar(effective.len() as f64));
860                    eval_inner(&captured_body, &local_env, io)
861                }),
862                src,
863            );
864            Ok(Value::Lambda(lambda))
865        }
866        Expr::CellLiteral(elems) => {
867            let mut vals = Vec::with_capacity(elems.len());
868            for e in elems {
869                vals.push(eval_inner(e, env, io.as_deref_mut())?);
870            }
871            Ok(Value::Cell(vals))
872        }
873        Expr::CellIndex(cell_expr, idx_expr) => {
874            let cell = eval_inner(cell_expr, env, io.as_deref_mut())?;
875            let idx = eval_inner(idx_expr, env, io)?;
876            match (cell, idx) {
877                (Value::Cell(v), Value::Scalar(i)) => {
878                    let i = i as isize;
879                    if i < 1 || i as usize > v.len() {
880                        Err(format!("Cell index {} out of range (1..{})", i, v.len()))
881                    } else {
882                        Ok(v[(i - 1) as usize].clone())
883                    }
884                }
885                (Value::Cell(_), _) => Err("Cell index must be a scalar integer".to_string()),
886                _ => Err("Brace indexing '{}' is only valid on cell arrays".to_string()),
887            }
888        }
889        Expr::FieldGet(base_expr, field) => {
890            let base_val = eval_inner(base_expr, env, io)?;
891            match base_val {
892                Value::Struct(map) => map
893                    .get(field)
894                    .cloned()
895                    .ok_or_else(|| format!("No field '{field}' in struct")),
896                // s.field on a struct array — collect field values across all elements
897                Value::StructArray(arr) => {
898                    let mut values: Vec<Value> = Vec::with_capacity(arr.len());
899                    for (idx, elem) in arr.iter().enumerate() {
900                        let v = elem.get(field).cloned().ok_or_else(|| {
901                            format!("No field '{field}' in struct array element {}", idx + 1)
902                        })?;
903                        values.push(v);
904                    }
905                    // If all values are scalars, return a 1×N matrix; otherwise a cell.
906                    let all_scalar = values.iter().all(|v| matches!(v, Value::Scalar(_)));
907                    if all_scalar {
908                        let nums: Vec<f64> = values
909                            .into_iter()
910                            .map(|v| {
911                                if let Value::Scalar(n) = v {
912                                    n
913                                } else {
914                                    unreachable!()
915                                }
916                            })
917                            .collect();
918                        let n = nums.len();
919                        Ok(Value::Matrix(Array2::from_shape_vec((1, n), nums).unwrap()))
920                    } else {
921                        Ok(Value::Cell(values))
922                    }
923                }
924                _ => Err(format!(
925                    "Cannot access field '{field}' on a non-struct value"
926                )),
927            }
928        }
929        Expr::DotCall(segs, args) => {
930            let qualified = segs.join(".");
931            // If the head segment is a variable, follow the field chain and call the result.
932            if let Some(head_val) = env.get(&segs[0]).cloned() {
933                let mut val = head_val;
934                for field in &segs[1..] {
935                    val = match val {
936                        Value::Struct(ref map) => map
937                            .get(field)
938                            .cloned()
939                            .ok_or_else(|| format!("No field '{field}' in struct"))?,
940                        _ => {
941                            return Err(format!(
942                                "Cannot access field '{field}' on a non-struct value"
943                            ));
944                        }
945                    };
946                }
947                let mut evaled = Vec::with_capacity(args.len());
948                for a in args {
949                    evaled.push(eval_inner(a, env, io.as_deref_mut())?);
950                }
951                return match val {
952                    Value::Lambda(f) => {
953                        if evaled.is_empty() {
954                            evaled.push(env.get("ans").cloned().unwrap_or(Value::Scalar(0.0)));
955                        }
956                        f.0(&evaled, io)
957                    }
958                    Value::Function { .. } => match io.as_deref_mut() {
959                        Some(io_ref) => FN_CALL_HOOK.with(|c| match c.get() {
960                            Some(hook) => hook(&qualified, &val, &evaled, env, io_ref),
961                            None => Err(format!("'{qualified}': exec::init() not called")),
962                        }),
963                        None => {
964                            let mut tmp_io = IoContext::new();
965                            FN_CALL_HOOK.with(|c| match c.get() {
966                                Some(hook) => hook(&qualified, &val, &evaled, env, &mut tmp_io),
967                                None => Err(format!("'{qualified}': exec::init() not called")),
968                            })
969                        }
970                    },
971                    _ => Err(format!("'{qualified}': not a callable")),
972                };
973            }
974            // Package call: autoload from +pkg/func.calc then invoke.
975            let cached = AUTOLOAD_CACHE.with(|c| c.borrow().get(&qualified).cloned());
976            let autoloaded_val = cached.or_else(|| {
977                let loaded = AUTOLOAD_HOOK
978                    .with(|c| c.get())
979                    .is_some_and(|hook| hook(&qualified));
980                if loaded {
981                    AUTOLOAD_CACHE.with(|c| c.borrow().get(&qualified).cloned())
982                } else {
983                    None
984                }
985            });
986            if let Some(val) = autoloaded_val {
987                let mut evaled = Vec::with_capacity(args.len());
988                for a in args {
989                    evaled.push(eval_inner(a, env, io.as_deref_mut())?);
990                }
991                return 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            }
1005            Err(format!("Unknown package function: '{qualified}'"))
1006        }
1007        Expr::FuncHandle(name) => {
1008            let name = name.clone();
1009            let captured_env = env.clone();
1010            let src = format!("@{name}");
1011            let lambda = LambdaFn(
1012                std::rc::Rc::new(move |args: &[Value], io: Option<&mut IoContext>| {
1013                    // First try the environment (user-defined function), then fall back to builtin.
1014                    if let Some(f) = captured_env.get(&name) {
1015                        let f = f.clone();
1016                        call_function_value(&f, args, io)
1017                    } else {
1018                        call_builtin(&name, args, &captured_env, io)
1019                    }
1020                }),
1021                src,
1022            );
1023            Ok(Value::Lambda(lambda))
1024        }
1025        Expr::PlainTranspose(e) => match eval_inner(e, env, io)? {
1026            Value::Void => Err("Transpose is not applicable to void".to_string()),
1027            Value::Scalar(n) => Ok(Value::Scalar(n)),
1028            Value::Matrix(m) => Ok(Value::Matrix(m.t().to_owned())),
1029            // Plain transpose: no conjugation — imaginary part unchanged
1030            Value::Complex(re, im) => Ok(Value::Complex(re, im)),
1031            // Plain transpose of complex matrix: swap axes only, no conjugation
1032            Value::ComplexMatrix(m) => Ok(Value::ComplexMatrix(m.t().to_owned())),
1033            Value::Str(s) => Ok(Value::Str(s)),
1034            Value::StringObj(s) => Ok(Value::StringObj(s)),
1035            // Arrays: orientation is ignored (Vec<f64> is always 1-D), return as-is.
1036            v @ (Value::DateTimeArray(_) | Value::DurationArray(_)) => Ok(v),
1037            Value::Lambda(_)
1038            | Value::Function { .. }
1039            | Value::Tuple(_)
1040            | Value::Cell(_)
1041            | Value::Struct(_)
1042            | Value::StructArray(_)
1043            | Value::DateTime(_)
1044            | Value::Duration(_) => Err("Transpose is not applicable to this type".to_string()),
1045        },
1046        Expr::Colon => Err("':' is only valid inside index expressions".to_string()),
1047        Expr::NaT => Ok(Value::DateTime(f64::NAN)),
1048        Expr::Matrix(rows) => {
1049            if rows.is_empty() {
1050                return Ok(Value::Matrix(Array2::<f64>::zeros((0, 0))));
1051            }
1052
1053            // Pass 1: evaluate all elements, skipping empty rows.
1054            let mut evaluated: Vec<Vec<Value>> = Vec::with_capacity(rows.len());
1055            for row in rows {
1056                if row.is_empty() {
1057                    continue;
1058                }
1059                let mut ev_row: Vec<Value> = Vec::with_capacity(row.len());
1060                for elem_expr in row {
1061                    ev_row.push(eval_inner(elem_expr, env, io.as_deref_mut())?);
1062                }
1063                evaluated.push(ev_row);
1064            }
1065            if evaluated.is_empty() {
1066                return Ok(Value::Matrix(Array2::<f64>::zeros((0, 0))));
1067            }
1068
1069            // Pass 2: detect if any element is complex (scan the entire evaluated grid).
1070            let has_complex = evaluated
1071                .iter()
1072                .flat_map(|row| row.iter())
1073                .any(|v| matches!(v, Value::Complex(_, _) | Value::ComplexMatrix(_)));
1074
1075            // Dispatch on element kind (complex takes priority over numeric).
1076            enum MatKind {
1077                ComplexNumeric,
1078                Numeric,
1079                DateTime,
1080                Duration,
1081                Str,
1082            }
1083            let kind = if has_complex {
1084                MatKind::ComplexNumeric
1085            } else {
1086                match &evaluated[0][0] {
1087                    Value::Scalar(_) | Value::Matrix(_) => MatKind::Numeric,
1088                    Value::DateTime(_) | Value::DateTimeArray(_) => MatKind::DateTime,
1089                    Value::Duration(_) | Value::DurationArray(_) => MatKind::Duration,
1090                    Value::Str(_) | Value::StringObj(_) => MatKind::Str,
1091                    Value::Void => {
1092                        return Err("Void value cannot be used in matrix literal".to_string());
1093                    }
1094                    Value::Lambda(_)
1095                    | Value::Function { .. }
1096                    | Value::Tuple(_)
1097                    | Value::Cell(_)
1098                    | Value::Struct(_)
1099                    | Value::StructArray(_) => {
1100                        return Err("This type cannot be used in matrix literals".to_string());
1101                    }
1102                    // Cannot reach here — has_complex covers this
1103                    Value::Complex(_, _) | Value::ComplexMatrix(_) => unreachable!(),
1104                }
1105            };
1106
1107            match kind {
1108                MatKind::ComplexNumeric => {
1109                    // Build a ComplexMatrix by upcasting all elements.
1110                    // Each element can be Scalar, Complex, Matrix (real), or ComplexMatrix.
1111                    let mut row_blocks: Vec<Array2<Complex<f64>>> =
1112                        Vec::with_capacity(evaluated.len());
1113                    for ev_row in &evaluated {
1114                        let mut elem_mats: Vec<Array2<Complex<f64>>> =
1115                            Vec::with_capacity(ev_row.len());
1116                        for val in ev_row {
1117                            let block: Array2<Complex<f64>> = match val {
1118                                Value::Scalar(n) => {
1119                                    Array2::from_elem((1, 1), Complex::new(*n, 0.0))
1120                                }
1121                                Value::Complex(re, im) => {
1122                                    Array2::from_elem((1, 1), Complex::new(*re, *im))
1123                                }
1124                                Value::Matrix(m) => cm_from_real(m),
1125                                Value::ComplexMatrix(m) => m.clone(),
1126                                _ => {
1127                                    return Err(
1128                                        "This type cannot be used in a complex matrix literal"
1129                                            .to_string(),
1130                                    );
1131                                }
1132                            };
1133                            elem_mats.push(block);
1134                        }
1135                        let nrows = elem_mats[0].nrows();
1136                        for (i, m) in elem_mats.iter().enumerate().skip(1) {
1137                            if m.nrows() != nrows {
1138                                return Err(format!(
1139                                    "Matrix row height mismatch: expected {} rows, element {} has {} rows",
1140                                    nrows,
1141                                    i + 1,
1142                                    m.nrows()
1143                                ));
1144                            }
1145                        }
1146                        let ncols: usize = elem_mats.iter().map(|m| m.ncols()).sum();
1147                        let mut flat: Vec<Complex<f64>> = Vec::with_capacity(nrows * ncols);
1148                        for r in 0..nrows {
1149                            for m in &elem_mats {
1150                                flat.extend(m.row(r).iter().copied());
1151                            }
1152                        }
1153                        row_blocks.push(
1154                            Array2::from_shape_vec((nrows, ncols), flat)
1155                                .map_err(|e| format!("Matrix shape error: {e}"))?,
1156                        );
1157                    }
1158                    if row_blocks.is_empty() {
1159                        return Ok(Value::ComplexMatrix(Array2::zeros((0, 0))));
1160                    }
1161                    let ncols = row_blocks[0].ncols();
1162                    for (i, blk) in row_blocks.iter().enumerate().skip(1) {
1163                        if blk.ncols() != ncols {
1164                            return Err(format!(
1165                                "Matrix column count mismatch: expected {} columns, row {} has {} columns",
1166                                ncols,
1167                                i + 1,
1168                                blk.ncols()
1169                            ));
1170                        }
1171                    }
1172                    let total_rows: usize = row_blocks.iter().map(|b| b.nrows()).sum();
1173                    let mut flat: Vec<Complex<f64>> = Vec::with_capacity(total_rows * ncols);
1174                    for blk in &row_blocks {
1175                        flat.extend(blk.iter().copied());
1176                    }
1177                    let m = Array2::from_shape_vec((total_rows, ncols), flat)
1178                        .map_err(|e| format!("Matrix shape error: {e}"))?;
1179                    Ok(Value::ComplexMatrix(m))
1180                }
1181                MatKind::DateTime => {
1182                    let mut ts: Vec<f64> = Vec::new();
1183                    for ev_row in &evaluated {
1184                        for val in ev_row {
1185                            match val {
1186                                Value::DateTime(t) => ts.push(*t),
1187                                Value::DateTimeArray(v) => ts.extend_from_slice(v),
1188                                _ => {
1189                                    return Err(
1190                                        "Matrix literal: cannot mix datetime with other types"
1191                                            .to_string(),
1192                                    );
1193                                }
1194                            }
1195                        }
1196                    }
1197                    Ok(Value::DateTimeArray(ts))
1198                }
1199                MatKind::Duration => {
1200                    let mut sv: Vec<f64> = Vec::new();
1201                    for ev_row in &evaluated {
1202                        for val in ev_row {
1203                            match val {
1204                                Value::Duration(s) => sv.push(*s),
1205                                Value::DurationArray(v) => sv.extend_from_slice(v),
1206                                _ => {
1207                                    return Err(
1208                                        "Matrix literal: cannot mix duration with other types"
1209                                            .to_string(),
1210                                    );
1211                                }
1212                            }
1213                        }
1214                    }
1215                    Ok(Value::DurationArray(sv))
1216                }
1217                MatKind::Numeric => {
1218                    // Each row is horizontally concatenated into an Array2 block;
1219                    // blocks are then vertically concatenated.
1220                    let mut row_blocks: Vec<Array2<f64>> = Vec::with_capacity(evaluated.len());
1221                    for ev_row in &evaluated {
1222                        let mut elem_mats: Vec<Array2<f64>> = Vec::with_capacity(ev_row.len());
1223                        for val in ev_row {
1224                            match val {
1225                                Value::Scalar(n) => {
1226                                    elem_mats.push(Array2::from_elem((1, 1), *n));
1227                                }
1228                                Value::Matrix(m) => elem_mats.push(m.clone()),
1229                                Value::Void => {
1230                                    return Err(
1231                                        "Void value cannot be used in matrix literal".to_string()
1232                                    );
1233                                }
1234                                // In numeric context, char arrays contribute their
1235                                // Unicode code values — MATLAB compatible: [65 'b'] = [65 98]
1236                                Value::Str(s) | Value::StringObj(s) => {
1237                                    let codes: Vec<f64> =
1238                                        s.chars().map(|c| c as u32 as f64).collect();
1239                                    let mat = if codes.is_empty() {
1240                                        Array2::<f64>::zeros((1, 0))
1241                                    } else {
1242                                        Array2::from_shape_vec((1, codes.len()), codes)
1243                                            .map_err(|e| format!("Matrix shape error: {e}"))?
1244                                    };
1245                                    elem_mats.push(mat);
1246                                }
1247                                _ => {
1248                                    return Err(
1249                                        "This type cannot be used in matrix literals".to_string()
1250                                    );
1251                                }
1252                            }
1253                        }
1254                        let nrows = elem_mats[0].nrows();
1255                        for (i, m) in elem_mats.iter().enumerate().skip(1) {
1256                            if m.nrows() != nrows {
1257                                return Err(format!(
1258                                    "Matrix row height mismatch: expected {} rows, element {} has {} rows",
1259                                    nrows,
1260                                    i + 1,
1261                                    m.nrows()
1262                                ));
1263                            }
1264                        }
1265                        let ncols: usize = elem_mats.iter().map(|m| m.ncols()).sum();
1266                        let mut flat: Vec<f64> = Vec::with_capacity(nrows * ncols);
1267                        for r in 0..nrows {
1268                            for m in &elem_mats {
1269                                flat.extend(m.row(r).iter().copied());
1270                            }
1271                        }
1272                        row_blocks.push(
1273                            Array2::from_shape_vec((nrows, ncols), flat)
1274                                .map_err(|e| format!("Matrix shape error: {e}"))?,
1275                        );
1276                    }
1277                    if row_blocks.is_empty() {
1278                        return Ok(Value::Matrix(Array2::<f64>::zeros((0, 0))));
1279                    }
1280                    let ncols = row_blocks[0].ncols();
1281                    if ncols == 0 {
1282                        let total_rows: usize = row_blocks.iter().map(|b| b.nrows()).sum();
1283                        return Ok(Value::Matrix(Array2::zeros((total_rows, 0))));
1284                    }
1285                    for (i, blk) in row_blocks.iter().enumerate().skip(1) {
1286                        if blk.ncols() != ncols {
1287                            return Err(format!(
1288                                "Matrix column count mismatch: expected {} columns, row {} has {} columns",
1289                                ncols,
1290                                i + 1,
1291                                blk.ncols()
1292                            ));
1293                        }
1294                    }
1295                    let total_rows: usize = row_blocks.iter().map(|b| b.nrows()).sum();
1296                    let mut flat: Vec<f64> = Vec::with_capacity(total_rows * ncols);
1297                    for blk in &row_blocks {
1298                        flat.extend(blk.iter().copied());
1299                    }
1300                    let m = Array2::from_shape_vec((total_rows, ncols), flat)
1301                        .map_err(|e| format!("Matrix shape error: {e}"))?;
1302                    Ok(Value::Matrix(m))
1303                }
1304                MatKind::Str => {
1305                    if evaluated.len() > 1 {
1306                        return Err("Multi-row char-array literals are not supported".to_string());
1307                    }
1308                    let mut out = String::new();
1309                    for val in &evaluated[0] {
1310                        match val {
1311                            Value::Str(s) | Value::StringObj(s) => out.push_str(s),
1312                            Value::Scalar(n) => {
1313                                let code = n.round();
1314                                out.push(
1315                                    char::from_u32(code as u32)
1316                                        .ok_or_else(|| format!("char: invalid code {n}"))?,
1317                                );
1318                            }
1319                            Value::Matrix(m) => {
1320                                for &n in m.iter() {
1321                                    out.push(
1322                                        char::from_u32(n.round() as u32)
1323                                            .ok_or_else(|| format!("char: invalid code {n}"))?,
1324                                    );
1325                                }
1326                            }
1327                            _ => {
1328                                return Err(
1329                                    "This type cannot be used in a char-array literal".to_string()
1330                                );
1331                            }
1332                        }
1333                    }
1334                    Ok(Value::Str(out))
1335                }
1336            }
1337        }
1338        Expr::Transpose(e) => match eval_inner(e, env, io)? {
1339            Value::Void => Err("Transpose is not applicable to void".to_string()),
1340            Value::Scalar(n) => Ok(Value::Scalar(n)),
1341            Value::Matrix(m) => Ok(Value::Matrix(m.t().to_owned())),
1342            Value::Complex(re, im) => Ok(Value::Complex(re, -im)),
1343            // Conjugate transpose (Hermitian): transpose axes + conjugate each element
1344            Value::ComplexMatrix(m) => Ok(Value::ComplexMatrix(m.t().mapv(|c| c.conj()))),
1345            // Transpose of a char array or string object: return as-is (1×N not fully supported)
1346            Value::Str(s) => Ok(Value::Str(s)),
1347            Value::StringObj(s) => Ok(Value::StringObj(s)),
1348            // Arrays: orientation is ignored (Vec<f64> is always 1-D), return as-is.
1349            v @ (Value::DateTimeArray(_) | Value::DurationArray(_)) => Ok(v),
1350            Value::Lambda(_)
1351            | Value::Function { .. }
1352            | Value::Tuple(_)
1353            | Value::Cell(_)
1354            | Value::Struct(_)
1355            | Value::StructArray(_)
1356            | Value::DateTime(_)
1357            | Value::Duration(_) => Err("Transpose is not applicable to this type".to_string()),
1358        },
1359        Expr::StrLiteral(s) => Ok(Value::Str(s.clone())),
1360        Expr::StringObjLiteral(s) => Ok(Value::StringObj(s.clone())),
1361        Expr::Range(start_expr, step_expr, stop_expr) => {
1362            let start = match eval_inner(start_expr, env, io.as_deref_mut())? {
1363                Value::Scalar(n) => n,
1364                _ => return Err("Range bounds must be real scalars".to_string()),
1365            };
1366            let stop = match eval_inner(stop_expr, env, io.as_deref_mut())? {
1367                Value::Scalar(n) => n,
1368                _ => return Err("Range bounds must be real scalars".to_string()),
1369            };
1370            let step = match step_expr {
1371                None => 1.0,
1372                Some(s) => match eval_inner(s, env, io)? {
1373                    Value::Scalar(n) => n,
1374                    _ => return Err("Range step must be a real scalar".to_string()),
1375                },
1376            };
1377            if step == 0.0 {
1378                return Err("Range step cannot be zero".to_string());
1379            }
1380            let n_float = (stop - start) / step;
1381            if n_float < -1e-10 {
1382                // Empty range: step points in the wrong direction
1383                return Ok(Value::Matrix(Array2::zeros((1, 0))));
1384            }
1385            let n = (n_float + 1e-10).floor() as usize + 1;
1386            let vals: Vec<f64> = (0..n).map(|i| start + i as f64 * step).collect();
1387            let m =
1388                Array2::from_shape_vec((1, n), vals).map_err(|e| format!("Range error: {e}"))?;
1389            Ok(Value::Matrix(m))
1390        }
1391    }
1392}
1393
1394fn eval_binop(l: Value, op: &Op, r: Value) -> Result<Value, String> {
1395    match (l, r) {
1396        (Value::Void, _) | (_, Value::Void) => {
1397            Err("Cannot apply operator to void value".to_string())
1398        }
1399        // --- String object operations ---
1400        (Value::StringObj(a), Value::StringObj(b)) => match op {
1401            Op::Add => Ok(Value::StringObj(a + &b)),
1402            Op::Eq => Ok(Value::Scalar(bool_to_f64(a == b))),
1403            Op::NotEq => Ok(Value::Scalar(bool_to_f64(a != b))),
1404            _ => Err("Operator not supported on string objects".to_string()),
1405        },
1406        // Char array: convert to numeric, re-dispatch
1407        (Value::Str(s), r) => eval_binop(str_to_numeric(&s), op, r),
1408        (l, Value::Str(s)) => eval_binop(l, op, str_to_numeric(&s)),
1409        // String object mixed with other types: error
1410        (Value::StringObj(_), _) | (_, Value::StringObj(_)) => {
1411            Err("String object cannot be combined with non-string values".to_string())
1412        }
1413        // Functions, tuples, cell arrays, structs, and struct arrays are not numeric
1414        (Value::Lambda(_), _)
1415        | (_, Value::Lambda(_))
1416        | (Value::Function { .. }, _)
1417        | (_, Value::Function { .. })
1418        | (Value::Tuple(_), _)
1419        | (_, Value::Tuple(_))
1420        | (Value::Cell(_), _)
1421        | (_, Value::Cell(_))
1422        | (Value::Struct(_), _)
1423        | (_, Value::Struct(_))
1424        | (Value::StructArray(_), _)
1425        | (_, Value::StructArray(_)) => Err("Cannot apply operator to a struct value".to_string()),
1426        // --- DateTime / Duration arithmetic ---
1427        // datetime + duration → datetime
1428        (Value::DateTime(t), Value::Duration(d)) => match op {
1429            Op::Add => Ok(Value::DateTime(t + d)),
1430            Op::Sub => Ok(Value::DateTime(t - d)),
1431            _ => Err("Unsupported operator between datetime and duration".to_string()),
1432        },
1433        // duration + datetime → datetime (commutative add only)
1434        (Value::Duration(d), Value::DateTime(t)) => match op {
1435            Op::Add => Ok(Value::DateTime(t + d)),
1436            _ => Err("Unsupported operator between duration and datetime".to_string()),
1437        },
1438        // datetime - datetime → duration
1439        (Value::DateTime(t1), Value::DateTime(t2)) => match op {
1440            Op::Sub => Ok(Value::Duration(t1 - t2)),
1441            Op::Eq => Ok(Value::Scalar(bool_to_f64(
1442                (t1 - t2).abs() < 1e-9 || (t1.is_nan() && t2.is_nan()),
1443            ))),
1444            Op::NotEq => Ok(Value::Scalar(bool_to_f64(
1445                (t1 - t2).abs() >= 1e-9 && !(t1.is_nan() && t2.is_nan()),
1446            ))),
1447            Op::Lt => Ok(Value::Scalar(bool_to_f64(t1 < t2))),
1448            Op::Gt => Ok(Value::Scalar(bool_to_f64(t1 > t2))),
1449            Op::LtEq => Ok(Value::Scalar(bool_to_f64(t1 <= t2))),
1450            Op::GtEq => Ok(Value::Scalar(bool_to_f64(t1 >= t2))),
1451            _ => Err("Unsupported operator between two datetimes".to_string()),
1452        },
1453        // duration ± duration → duration; duration */ scalar → duration; duration / duration → scalar
1454        (Value::Duration(d1), Value::Duration(d2)) => match op {
1455            Op::Add => Ok(Value::Duration(d1 + d2)),
1456            Op::Sub => Ok(Value::Duration(d1 - d2)),
1457            Op::Div | Op::ElemDiv => Ok(Value::Scalar(d1 / d2)),
1458            Op::Eq => Ok(Value::Scalar(bool_to_f64((d1 - d2).abs() < 1e-9))),
1459            Op::NotEq => Ok(Value::Scalar(bool_to_f64((d1 - d2).abs() >= 1e-9))),
1460            Op::Lt => Ok(Value::Scalar(bool_to_f64(d1 < d2))),
1461            Op::Gt => Ok(Value::Scalar(bool_to_f64(d1 > d2))),
1462            Op::LtEq => Ok(Value::Scalar(bool_to_f64(d1 <= d2))),
1463            Op::GtEq => Ok(Value::Scalar(bool_to_f64(d1 >= d2))),
1464            _ => Err("Unsupported operator between two durations".to_string()),
1465        },
1466        (Value::Duration(d), Value::Scalar(s)) => match op {
1467            Op::Mul | Op::ElemMul => Ok(Value::Duration(d * s)),
1468            Op::Div | Op::ElemDiv => Ok(Value::Duration(d / s)),
1469            _ => Err("Unsupported operator between duration and scalar".to_string()),
1470        },
1471        (Value::Scalar(s), Value::Duration(d)) => match op {
1472            Op::Mul | Op::ElemMul => Ok(Value::Duration(s * d)),
1473            _ => Err("Unsupported operator between scalar and duration".to_string()),
1474        },
1475        // DateTime/Duration + arrays
1476        (Value::DateTime(t), Value::DurationArray(dv)) => match op {
1477            Op::Add => Ok(Value::DateTimeArray(dv.iter().map(|d| t + d).collect())),
1478            Op::Sub => Ok(Value::DateTimeArray(dv.iter().map(|d| t - d).collect())),
1479            _ => Err("Unsupported operator between datetime and duration array".to_string()),
1480        },
1481        (Value::DurationArray(dv), Value::DateTime(t)) => match op {
1482            Op::Add => Ok(Value::DateTimeArray(dv.iter().map(|d| t + d).collect())),
1483            _ => Err("Unsupported operator between duration array and datetime".to_string()),
1484        },
1485        (Value::DateTimeArray(tv), Value::Duration(d)) => match op {
1486            Op::Add => Ok(Value::DateTimeArray(tv.iter().map(|t| t + d).collect())),
1487            Op::Sub => Ok(Value::DateTimeArray(tv.iter().map(|t| t - d).collect())),
1488            _ => Err("Unsupported operator between datetime array and duration".to_string()),
1489        },
1490        (Value::DateTimeArray(tv), Value::DurationArray(dv)) => match op {
1491            Op::Add if tv.len() == dv.len() => Ok(Value::DateTimeArray(
1492                tv.iter().zip(&dv).map(|(t, d)| t + d).collect(),
1493            )),
1494            Op::Sub if tv.len() == dv.len() => Ok(Value::DateTimeArray(
1495                tv.iter().zip(&dv).map(|(t, d)| t - d).collect(),
1496            )),
1497            _ => Err("Unsupported or mismatched datetime/duration array operation".to_string()),
1498        },
1499        (Value::DateTimeArray(tv1), Value::DateTimeArray(tv2)) => match op {
1500            Op::Sub if tv1.len() == tv2.len() => Ok(Value::DurationArray(
1501                tv1.iter().zip(&tv2).map(|(a, b)| a - b).collect(),
1502            )),
1503            _ => Err("Unsupported operator between two datetime arrays".to_string()),
1504        },
1505        (Value::DurationArray(dv), Value::Scalar(s)) => match op {
1506            Op::Mul | Op::ElemMul => Ok(Value::DurationArray(dv.iter().map(|d| d * s).collect())),
1507            Op::Div | Op::ElemDiv => Ok(Value::DurationArray(dv.iter().map(|d| d / s).collect())),
1508            _ => Err("Unsupported operator between duration array and scalar".to_string()),
1509        },
1510        (Value::Scalar(s), Value::DurationArray(dv)) => match op {
1511            Op::Mul | Op::ElemMul => Ok(Value::DurationArray(dv.iter().map(|d| s * d).collect())),
1512            _ => Err("Unsupported operator between scalar and duration array".to_string()),
1513        },
1514        // Catch-all: DateTime/Duration mixed with unsupported types
1515        (Value::DateTime(_), _)
1516        | (_, Value::DateTime(_))
1517        | (Value::Duration(_), _)
1518        | (_, Value::Duration(_))
1519        | (Value::DateTimeArray(_), _)
1520        | (_, Value::DateTimeArray(_))
1521        | (Value::DurationArray(_), _)
1522        | (_, Value::DurationArray(_)) => {
1523            Err("Unsupported operation on datetime or duration value".to_string())
1524        }
1525        // --- Complex arithmetic ---
1526        (Value::Complex(re1, im1), Value::Complex(re2, im2)) => {
1527            complex_binop(re1, im1, op, re2, im2)
1528        }
1529        (Value::Complex(re, im), Value::Scalar(s)) => complex_binop(re, im, op, s, 0.0),
1530        (Value::Scalar(s), Value::Complex(re, im)) => complex_binop(s, 0.0, op, re, im),
1531        // Complex scalar × real matrix → upcast matrix to ComplexMatrix, broadcast scalar
1532        (Value::Complex(re, im), Value::Matrix(m)) => {
1533            complex_binop_cm(re, im, op, cm_from_real(&m))
1534        }
1535        (Value::Matrix(m), Value::Complex(re, im)) => {
1536            cm_binop_complex(cm_from_real(&m), op, re, im)
1537        }
1538        // ComplexMatrix combinations
1539        (Value::ComplexMatrix(a), Value::ComplexMatrix(b)) => complex_matrix_binop(a, op, b),
1540        (Value::ComplexMatrix(cm), Value::Matrix(m)) => {
1541            complex_matrix_binop(cm, op, cm_from_real(&m))
1542        }
1543        (Value::Matrix(m), Value::ComplexMatrix(cm)) => {
1544            complex_matrix_binop(cm_from_real(&m), op, cm)
1545        }
1546        (Value::ComplexMatrix(cm), Value::Scalar(s)) => cm_binop_scalar(cm, op, s),
1547        (Value::Scalar(s), Value::ComplexMatrix(cm)) => scalar_binop_cm(s, op, cm),
1548        (Value::ComplexMatrix(cm), Value::Complex(re, im)) => cm_binop_complex(cm, op, re, im),
1549        (Value::Complex(re, im), Value::ComplexMatrix(cm)) => complex_binop_cm(re, im, op, cm),
1550        (Value::Scalar(lv), Value::Scalar(rv)) => {
1551            let result = match op {
1552                Op::Add => lv + rv,
1553                Op::Sub => lv - rv,
1554                Op::Mul | Op::ElemMul => lv * rv,
1555                Op::Div | Op::ElemDiv => lv / rv,
1556                Op::LDiv => rv / lv,
1557                Op::Pow | Op::ElemPow => lv.powf(rv),
1558                Op::Eq => bool_to_f64(lv == rv),
1559                Op::NotEq => bool_to_f64(lv != rv),
1560                Op::Lt => bool_to_f64(lv < rv),
1561                Op::Gt => bool_to_f64(lv > rv),
1562                Op::LtEq => bool_to_f64(lv <= rv),
1563                Op::GtEq => bool_to_f64(lv >= rv),
1564                Op::And | Op::ElemAnd => bool_to_f64(lv != 0.0 && rv != 0.0),
1565                Op::Or | Op::ElemOr => bool_to_f64(lv != 0.0 || rv != 0.0),
1566            };
1567            Ok(Value::Scalar(result))
1568        }
1569        (Value::Matrix(lm), Value::Matrix(rm)) => match op {
1570            Op::Add => {
1571                check_same_shape(&lm, &rm)?;
1572                Ok(Value::Matrix(&lm + &rm))
1573            }
1574            Op::Sub => {
1575                check_same_shape(&lm, &rm)?;
1576                Ok(Value::Matrix(&lm - &rm))
1577            }
1578            Op::Mul => {
1579                if lm.ncols() != rm.nrows() {
1580                    return Err(format!(
1581                        "Inner dimensions must agree: {}x{} * {}x{}",
1582                        lm.nrows(),
1583                        lm.ncols(),
1584                        rm.nrows(),
1585                        rm.ncols()
1586                    ));
1587                }
1588                Ok(Value::Matrix(lm.dot(&rm)))
1589            }
1590            Op::ElemMul => {
1591                check_same_shape(&lm, &rm)?;
1592                Ok(Value::Matrix(&lm * &rm))
1593            }
1594            Op::ElemDiv => {
1595                check_same_shape(&lm, &rm)?;
1596                Ok(Value::Matrix(&lm / &rm))
1597            }
1598            Op::ElemPow => {
1599                check_same_shape(&lm, &rm)?;
1600                Ok(Value::Matrix(
1601                    ndarray::Zip::from(&lm)
1602                        .and(&rm)
1603                        .map_collect(|a, b| a.powf(*b)),
1604                ))
1605            }
1606            Op::Eq | Op::NotEq | Op::Lt | Op::Gt | Op::LtEq | Op::GtEq => {
1607                check_same_shape(&lm, &rm)?;
1608                Ok(Value::Matrix(
1609                    ndarray::Zip::from(&lm)
1610                        .and(&rm)
1611                        .map_collect(|a, b| bool_to_f64(cmp_op(op, *a, *b))),
1612                ))
1613            }
1614            Op::And | Op::Or | Op::ElemAnd | Op::ElemOr => {
1615                check_same_shape(&lm, &rm)?;
1616                Ok(Value::Matrix(
1617                    ndarray::Zip::from(&lm)
1618                        .and(&rm)
1619                        .map_collect(|a, b| bool_to_f64(cmp_op(op, *a, *b))),
1620                ))
1621            }
1622            Op::Div => Err("Matrix / Matrix: use inv(B)*A or A*inv(B)".to_string()),
1623            Op::LDiv => Ok(Value::Matrix(solve_linear(&lm, &rm)?)),
1624            Op::Pow => Err("Matrix ^ Matrix: not supported".to_string()),
1625        },
1626        (Value::Scalar(s), Value::Matrix(m)) => match op {
1627            Op::Add => Ok(Value::Matrix(s + &m)),
1628            Op::Sub => Ok(Value::Matrix(m.mapv(|x| s - x))),
1629            Op::Mul | Op::ElemMul => Ok(Value::Matrix(s * &m)),
1630            Op::Div => Err("Scalar / Matrix: not supported".to_string()),
1631            Op::ElemDiv => Err("Scalar ./ Matrix: not supported".to_string()),
1632            Op::LDiv => {
1633                if s == 0.0 {
1634                    return Err("Left division by zero (a \\ B requires a ≠ 0)".to_string());
1635                }
1636                Ok(Value::Matrix(m.mapv(|x| x / s)))
1637            }
1638            Op::Pow | Op::ElemPow => Ok(Value::Matrix(m.mapv(|x| s.powf(x)))),
1639            Op::Eq
1640            | Op::NotEq
1641            | Op::Lt
1642            | Op::Gt
1643            | Op::LtEq
1644            | Op::GtEq
1645            | Op::And
1646            | Op::Or
1647            | Op::ElemAnd
1648            | Op::ElemOr => Ok(Value::Matrix(m.mapv(|x| bool_to_f64(cmp_op(op, s, x))))),
1649        },
1650        (Value::Matrix(m), Value::Scalar(s)) => match op {
1651            Op::Add => Ok(Value::Matrix(&m + s)),
1652            Op::Sub => Ok(Value::Matrix(&m - s)),
1653            Op::Mul | Op::ElemMul => Ok(Value::Matrix(&m * s)),
1654            Op::Div | Op::ElemDiv => Ok(Value::Matrix(m.mapv(|x| x / s))),
1655            Op::LDiv => {
1656                let b = Array2::from_elem((m.nrows(), 1), s);
1657                Ok(Value::Matrix(solve_linear(&m, &b)?))
1658            }
1659            Op::Pow | Op::ElemPow => Ok(Value::Matrix(m.mapv(|x| x.powf(s)))),
1660            Op::Eq
1661            | Op::NotEq
1662            | Op::Lt
1663            | Op::Gt
1664            | Op::LtEq
1665            | Op::GtEq
1666            | Op::And
1667            | Op::Or
1668            | Op::ElemAnd
1669            | Op::ElemOr => Ok(Value::Matrix(m.mapv(|x| bool_to_f64(cmp_op(op, x, s))))),
1670        },
1671    }
1672}
1673
1674#[inline]
1675fn bool_to_f64(b: bool) -> f64 {
1676    if b { 1.0 } else { 0.0 }
1677}
1678
1679/// Applies a comparison or logical op to two scalar values.
1680fn cmp_op(op: &Op, a: f64, b: f64) -> bool {
1681    match op {
1682        Op::Eq => a == b,
1683        Op::NotEq => a != b,
1684        Op::Lt => a < b,
1685        Op::Gt => a > b,
1686        Op::LtEq => a <= b,
1687        Op::GtEq => a >= b,
1688        Op::And | Op::ElemAnd => a != 0.0 && b != 0.0,
1689        Op::Or | Op::ElemOr => a != 0.0 || b != 0.0,
1690        _ => unreachable!(),
1691    }
1692}
1693
1694/// Performs binary operations on two complex numbers `(re1+im1*i) OP (re2+im2*i)`.
1695fn complex_binop(re1: f64, im1: f64, op: &Op, re2: f64, im2: f64) -> Result<Value, String> {
1696    match op {
1697        Op::Add => Ok(make_complex(re1 + re2, im1 + im2)),
1698        Op::Sub => Ok(make_complex(re1 - re2, im1 - im2)),
1699        Op::Mul | Op::ElemMul => {
1700            // (a+bi)(c+di) = (ac-bd) + (ad+bc)i
1701            Ok(make_complex(re1 * re2 - im1 * im2, re1 * im2 + im1 * re2))
1702        }
1703        Op::Div | Op::ElemDiv => {
1704            // (a+bi)/(c+di) = ((ac+bd) + (bc-ad)i) / (c²+d²)
1705            // When denom == 0 let IEEE 754 produce Inf/NaN naturally.
1706            let denom = re2 * re2 + im2 * im2;
1707            if denom == 0.0 {
1708                return Ok(make_complex(re1 / 0.0_f64, im1 / 0.0_f64));
1709            }
1710            Ok(make_complex(
1711                (re1 * re2 + im1 * im2) / denom,
1712                (im1 * re2 - re1 * im2) / denom,
1713            ))
1714        }
1715        Op::Pow | Op::ElemPow => {
1716            let r1 = (re1 * re1 + im1 * im1).sqrt();
1717            if r1 == 0.0 {
1718                if re2 > 0.0 {
1719                    return Ok(Value::Scalar(0.0));
1720                }
1721                return Ok(Value::Complex(f64::NAN, f64::NAN));
1722            }
1723            // For integer exponents with zero imaginary part, use repeated multiplication
1724            // to avoid polar-form floating-point error (e.g. i^2 = -1 exactly).
1725            if im2 == 0.0 && re2.fract() == 0.0 && re2.abs() < 1_000_000.0 {
1726                let n = re2 as i64;
1727                if n == 0 {
1728                    return Ok(Value::Scalar(1.0));
1729                }
1730                // positive power: repeated squaring
1731                let abs_n = n.unsigned_abs();
1732                let (mut rr, mut ri) = (1.0_f64, 0.0_f64);
1733                let (mut br, mut bi) = (re1, im1);
1734                let mut exp = abs_n;
1735                while exp > 0 {
1736                    if exp & 1 == 1 {
1737                        let nr = rr * br - ri * bi;
1738                        let ni = rr * bi + ri * br;
1739                        rr = nr;
1740                        ri = ni;
1741                    }
1742                    let nr = br * br - bi * bi;
1743                    let ni = 2.0 * br * bi;
1744                    br = nr;
1745                    bi = ni;
1746                    exp >>= 1;
1747                }
1748                if n < 0 {
1749                    // invert: 1/(rr+ri*i)
1750                    let denom = rr * rr + ri * ri;
1751                    return Ok(make_complex(rr / denom, -ri / denom));
1752                }
1753                return Ok(make_complex(rr, ri));
1754            }
1755            // General case: via polar form exp((c+di) * ln(a+bi))
1756            let theta1 = im1.atan2(re1);
1757            let ln_r1 = r1.ln();
1758            let exp_re = re2 * ln_r1 - im2 * theta1;
1759            let exp_im = im2 * ln_r1 + re2 * theta1;
1760            let mag = exp_re.exp();
1761            Ok(make_complex(mag * exp_im.cos(), mag * exp_im.sin()))
1762        }
1763        Op::Eq => Ok(Value::Scalar(bool_to_f64(re1 == re2 && im1 == im2))),
1764        Op::NotEq => Ok(Value::Scalar(bool_to_f64(re1 != re2 || im1 != im2))),
1765        Op::Lt | Op::Gt | Op::LtEq | Op::GtEq => {
1766            Err("Ordering is not defined for complex numbers".to_string())
1767        }
1768        Op::And | Op::ElemAnd => Ok(Value::Scalar(bool_to_f64(
1769            (re1 != 0.0 || im1 != 0.0) && (re2 != 0.0 || im2 != 0.0),
1770        ))),
1771        Op::Or | Op::ElemOr => Ok(Value::Scalar(bool_to_f64(
1772            re1 != 0.0 || im1 != 0.0 || re2 != 0.0 || im2 != 0.0,
1773        ))),
1774        Op::LDiv => Err("Left division (\\) is not supported for complex numbers".to_string()),
1775    }
1776}
1777
1778/// Constructs a `Value::Complex` or collapses to `Value::Scalar` when `im` is exactly zero.
1779#[inline]
1780fn make_complex(re: f64, im: f64) -> Value {
1781    if im == 0.0 {
1782        Value::Scalar(re)
1783    } else {
1784        Value::Complex(re, im)
1785    }
1786}
1787
1788/// Upcasts a real matrix to a complex matrix by setting all imaginary parts to zero.
1789#[inline]
1790fn cm_from_real(m: &Array2<f64>) -> Array2<Complex<f64>> {
1791    m.mapv(|x| Complex::new(x, 0.0))
1792}
1793
1794/// Performs binary operations between two `Array2<Complex<f64>>` matrices.
1795fn complex_matrix_binop(
1796    a: Array2<Complex<f64>>,
1797    op: &Op,
1798    b: Array2<Complex<f64>>,
1799) -> Result<Value, String> {
1800    let same_shape = || {
1801        if a.shape() != b.shape() {
1802            Err(format!(
1803                "Matrix dimensions must agree: {}×{} vs {}×{}",
1804                a.nrows(),
1805                a.ncols(),
1806                b.nrows(),
1807                b.ncols()
1808            ))
1809        } else {
1810            Ok(())
1811        }
1812    };
1813    match op {
1814        Op::Add => {
1815            same_shape()?;
1816            Ok(Value::ComplexMatrix(a + b))
1817        }
1818        Op::Sub => {
1819            same_shape()?;
1820            Ok(Value::ComplexMatrix(a - b))
1821        }
1822        Op::Mul => {
1823            if a.ncols() != b.nrows() {
1824                return Err(format!(
1825                    "Inner dimensions must agree: {}×{} * {}×{}",
1826                    a.nrows(),
1827                    a.ncols(),
1828                    b.nrows(),
1829                    b.ncols()
1830                ));
1831            }
1832            Ok(Value::ComplexMatrix(a.dot(&b)))
1833        }
1834        Op::ElemMul => {
1835            same_shape()?;
1836            Ok(Value::ComplexMatrix(a * b))
1837        }
1838        Op::ElemDiv => {
1839            same_shape()?;
1840            Ok(Value::ComplexMatrix(a / b))
1841        }
1842        Op::ElemPow => {
1843            same_shape()?;
1844            Ok(Value::ComplexMatrix(
1845                ndarray::Zip::from(&a)
1846                    .and(&b)
1847                    .map_collect(|x, y| x.powc(*y)),
1848            ))
1849        }
1850        Op::Pow => Err(
1851            "ComplexMatrix ^ ComplexMatrix: not supported; use .^ for element-wise power"
1852                .to_string(),
1853        ),
1854        Op::Div | Op::LDiv => {
1855            Err("Complex matrix / and \\ not supported; use inv(A)*B".to_string())
1856        }
1857        Op::Eq => {
1858            same_shape()?;
1859            Ok(Value::Matrix(
1860                ndarray::Zip::from(&a)
1861                    .and(&b)
1862                    .map_collect(|x, y| bool_to_f64(x == y)),
1863            ))
1864        }
1865        Op::NotEq => {
1866            same_shape()?;
1867            Ok(Value::Matrix(
1868                ndarray::Zip::from(&a)
1869                    .and(&b)
1870                    .map_collect(|x, y| bool_to_f64(x != y)),
1871            ))
1872        }
1873        Op::Lt | Op::Gt | Op::LtEq | Op::GtEq => {
1874            Err("Ordering comparison not defined for complex matrices".to_string())
1875        }
1876        Op::And | Op::ElemAnd => {
1877            same_shape()?;
1878            Ok(Value::Matrix(ndarray::Zip::from(&a).and(&b).map_collect(
1879                |x, y| bool_to_f64((x.re != 0.0 || x.im != 0.0) && (y.re != 0.0 || y.im != 0.0)),
1880            )))
1881        }
1882        Op::Or | Op::ElemOr => {
1883            same_shape()?;
1884            Ok(Value::Matrix(ndarray::Zip::from(&a).and(&b).map_collect(
1885                |x, y| bool_to_f64(x.re != 0.0 || x.im != 0.0 || y.re != 0.0 || y.im != 0.0),
1886            )))
1887        }
1888    }
1889}
1890
1891/// Broadcasts a scalar to every element of a complex matrix.
1892fn cm_binop_scalar(cm: Array2<Complex<f64>>, op: &Op, s: f64) -> Result<Value, String> {
1893    let c = Complex::new(s, 0.0);
1894    match op {
1895        Op::Add => Ok(Value::ComplexMatrix(cm.mapv(|x| x + c))),
1896        Op::Sub => Ok(Value::ComplexMatrix(cm.mapv(|x| x - c))),
1897        Op::Mul | Op::ElemMul => Ok(Value::ComplexMatrix(cm.mapv(|x| x * c))),
1898        Op::Div | Op::ElemDiv => Ok(Value::ComplexMatrix(cm.mapv(|x| x / c))),
1899        Op::Pow | Op::ElemPow => Ok(Value::ComplexMatrix(cm.mapv(|x| x.powf(s)))),
1900        Op::Eq => Ok(Value::Matrix(cm.mapv(|x| bool_to_f64(x == c)))),
1901        Op::NotEq => Ok(Value::Matrix(cm.mapv(|x| bool_to_f64(x != c)))),
1902        _ => Err("Unsupported operator between complex matrix and scalar".to_string()),
1903    }
1904}
1905
1906/// Broadcasts a scalar to every element of a complex matrix (scalar on the left).
1907fn scalar_binop_cm(s: f64, op: &Op, cm: Array2<Complex<f64>>) -> Result<Value, String> {
1908    let c = Complex::new(s, 0.0);
1909    match op {
1910        Op::Add => Ok(Value::ComplexMatrix(cm.mapv(|x| c + x))),
1911        Op::Sub => Ok(Value::ComplexMatrix(cm.mapv(|x| c - x))),
1912        Op::Mul | Op::ElemMul => Ok(Value::ComplexMatrix(cm.mapv(|x| c * x))),
1913        Op::Pow | Op::ElemPow => Ok(Value::ComplexMatrix(cm.mapv(|x| c.powc(x)))),
1914        Op::Eq => Ok(Value::Matrix(cm.mapv(|x| bool_to_f64(c == x)))),
1915        Op::NotEq => Ok(Value::Matrix(cm.mapv(|x| bool_to_f64(c != x)))),
1916        _ => Err("Unsupported operator between scalar and complex matrix".to_string()),
1917    }
1918}
1919
1920/// Broadcasts a complex scalar to every element of a complex matrix.
1921fn cm_binop_complex(cm: Array2<Complex<f64>>, op: &Op, re: f64, im: f64) -> Result<Value, String> {
1922    let c = Complex::new(re, im);
1923    match op {
1924        Op::Add => Ok(Value::ComplexMatrix(cm.mapv(|x| x + c))),
1925        Op::Sub => Ok(Value::ComplexMatrix(cm.mapv(|x| x - c))),
1926        Op::Mul | Op::ElemMul => Ok(Value::ComplexMatrix(cm.mapv(|x| x * c))),
1927        Op::Div | Op::ElemDiv => Ok(Value::ComplexMatrix(cm.mapv(|x| x / c))),
1928        Op::Pow | Op::ElemPow => Ok(Value::ComplexMatrix(cm.mapv(|x| x.powc(c)))),
1929        Op::Eq => Ok(Value::Matrix(cm.mapv(|x| bool_to_f64(x == c)))),
1930        Op::NotEq => Ok(Value::Matrix(cm.mapv(|x| bool_to_f64(x != c)))),
1931        _ => Err("Unsupported operator between complex matrix and complex scalar".to_string()),
1932    }
1933}
1934
1935/// Broadcasts a complex scalar (on the left) to every element of a complex matrix.
1936fn complex_binop_cm(re: f64, im: f64, op: &Op, cm: Array2<Complex<f64>>) -> Result<Value, String> {
1937    let c = Complex::new(re, im);
1938    match op {
1939        Op::Add => Ok(Value::ComplexMatrix(cm.mapv(|x| c + x))),
1940        Op::Sub => Ok(Value::ComplexMatrix(cm.mapv(|x| c - x))),
1941        Op::Mul | Op::ElemMul => Ok(Value::ComplexMatrix(cm.mapv(|x| c * x))),
1942        Op::Pow | Op::ElemPow => Ok(Value::ComplexMatrix(cm.mapv(|x| c.powc(x)))),
1943        Op::Eq => Ok(Value::Matrix(cm.mapv(|x| bool_to_f64(c == x)))),
1944        Op::NotEq => Ok(Value::Matrix(cm.mapv(|x| bool_to_f64(c != x)))),
1945        _ => Err("Unsupported operator between complex scalar and complex matrix".to_string()),
1946    }
1947}
1948
1949/// Converts a char array string to its numeric representation.
1950/// Single char → Scalar(code), multi-char → 1×N Matrix, empty → 1×0 Matrix.
1951fn str_to_numeric(s: &str) -> Value {
1952    let codes: Vec<f64> = s.chars().map(|c| c as u32 as f64).collect();
1953    match codes.len() {
1954        0 => Value::Matrix(Array2::zeros((1, 0))),
1955        1 => Value::Scalar(codes[0]),
1956        n => Value::Matrix(Array2::from_shape_vec((1, n), codes).unwrap()),
1957    }
1958}
1959
1960/// Extracts a string slice from a Str or StringObj value.
1961fn string_arg<'a>(v: &'a Value, fname: &str, pos: usize) -> Result<&'a str, String> {
1962    match v {
1963        Value::Str(s) | Value::StringObj(s) => Ok(s.as_str()),
1964        _ => Err(format!(
1965            "Function '{fname}' argument {pos} must be a string"
1966        )),
1967    }
1968}
1969
1970fn check_same_shape(lm: &Array2<f64>, rm: &Array2<f64>) -> Result<(), String> {
1971    if lm.shape() != rm.shape() {
1972        return Err(format!(
1973            "Matrix size mismatch: {}x{} vs {}x{}",
1974            lm.nrows(),
1975            lm.ncols(),
1976            rm.nrows(),
1977            rm.ncols()
1978        ));
1979    }
1980    Ok(())
1981}
1982
1983fn scalar_arg(v: &Value, fname: &str, pos: usize) -> Result<f64, String> {
1984    match v {
1985        Value::Void => Err(format!(
1986            "Function '{fname}' argument {pos} must be a scalar, got void"
1987        )),
1988        Value::Scalar(n) => Ok(*n),
1989        Value::Complex(re, im) if *im == 0.0 => Ok(*re),
1990        Value::Complex(_, _) => Err(format!(
1991            "Function '{fname}' argument {pos} must be real, got a complex number"
1992        )),
1993        Value::Matrix(_) => Err(format!(
1994            "Function '{fname}' argument {pos} must be a scalar, got a matrix"
1995        )),
1996        Value::ComplexMatrix(_) => Err(format!(
1997            "Function '{fname}' argument {pos} must be a scalar, got a complex matrix"
1998        )),
1999        Value::Str(s) if s.chars().count() == 1 => Ok(s.chars().next().unwrap() as u32 as f64),
2000        Value::Str(_) | Value::StringObj(_) => Err(format!(
2001            "Function '{fname}' argument {pos} must be a scalar, got a string"
2002        )),
2003        Value::Lambda(_)
2004        | Value::Function { .. }
2005        | Value::Tuple(_)
2006        | Value::Cell(_)
2007        | Value::Struct(_)
2008        | Value::StructArray(_)
2009        | Value::DateTime(_)
2010        | Value::Duration(_)
2011        | Value::DateTimeArray(_)
2012        | Value::DurationArray(_) => Err(format!(
2013            "Function '{fname}' argument {pos} must be a scalar, got a non-numeric value"
2014        )),
2015    }
2016}
2017
2018/// Interprets a single size argument for matrix constructors like `zeros`, `ones`,
2019/// `rand`, `randn`, and `nan`.
2020///
2021/// Accepts:
2022/// - `Scalar(n)` → `(n, n)` square matrix
2023/// - 1×1 `Matrix([n])` → `(n, n)` square matrix
2024/// - 1×2 or 2×1 `Matrix([r, c])` → `(r, c)` — enables `zeros(size(A))`
2025///
2026/// Returns `Err` for all other shapes.
2027fn size_arg(v: &Value, fname: &str) -> Result<(usize, usize), String> {
2028    match v {
2029        Value::Scalar(n) => Ok((*n as usize, *n as usize)),
2030        Value::Matrix(m) => {
2031            let elems: Vec<f64> = m.iter().copied().collect();
2032            match elems.as_slice() {
2033                [n] => Ok((*n as usize, *n as usize)),
2034                [r, c] => Ok((*r as usize, *c as usize)),
2035                _ => Err(format!(
2036                    "{fname}: size argument must be a scalar or a 1×2 vector, \
2037                     got a {}×{} matrix",
2038                    m.nrows(),
2039                    m.ncols()
2040                )),
2041            }
2042        }
2043        _ => Err(format!(
2044            "{fname}: size argument must be a scalar or a [rows cols] vector"
2045        )),
2046    }
2047}
2048
2049/// Applies a scalar function element-wise to a scalar or matrix.
2050/// Parses the first argument of `randi` into an inclusive `[lo, hi]` integer range.
2051///
2052/// Accepts either a scalar `max` (→ `[1, max]`) or a 1×2 / 2×1 vector `[min, max]`.
2053fn randi_range(v: &Value) -> Result<(i64, i64), String> {
2054    match v {
2055        Value::Scalar(n) => {
2056            let hi = *n as i64;
2057            if hi < 1 {
2058                return Err("randi: max must be a positive integer".to_string());
2059            }
2060            Ok((1, hi))
2061        }
2062        Value::Matrix(m) if m.len() == 2 => {
2063            let vals: Vec<f64> = m.iter().copied().collect();
2064            let lo = vals[0] as i64;
2065            let hi = vals[1] as i64;
2066            if lo > hi {
2067                return Err("randi: [min, max] range is empty".to_string());
2068            }
2069            Ok((lo, hi))
2070        }
2071        _ => Err("randi: first argument must be a scalar max or a [min, max] vector".to_string()),
2072    }
2073}
2074
2075// ── Descriptive statistics helpers ───────────────────────────────────────────
2076
2077/// Extracts a flat `Vec<f64>` from a `Scalar` or `Matrix` value.
2078fn numeric_vec(v: &Value, fname: &str) -> Result<Vec<f64>, String> {
2079    match v {
2080        Value::Scalar(n) => Ok(vec![*n]),
2081        Value::Matrix(m) => Ok(m.iter().copied().collect()),
2082        _ => Err(format!("{fname}: argument must be numeric")),
2083    }
2084}
2085
2086/// Computes the variance of a slice.  Returns `NaN` for empty, `0.0` for singletons.
2087/// `population = true` divides by N; `false` divides by N-1.
2088fn stat_var_vec(vals: &[f64], population: bool) -> f64 {
2089    let n = vals.len();
2090    if n == 0 {
2091        return f64::NAN;
2092    }
2093    if n == 1 {
2094        return 0.0;
2095    }
2096    let mean = vals.iter().sum::<f64>() / n as f64;
2097    let ss: f64 = vals.iter().map(|&x| (x - mean).powi(2)).sum();
2098    let denom = if population { n as f64 } else { (n - 1) as f64 };
2099    ss / denom
2100}
2101
2102/// Applies a column-wise statistical closure returning one scalar per column.
2103///
2104/// - Scalar → passes `[n]` to `f`.
2105/// - Vector (1×N or N×1) → scalar.
2106/// - M×N matrix (M>1, N>1) → 1×N row vector.
2107fn apply_stat<F>(v: &Value, mut f: F, fname: &str) -> Result<Value, String>
2108where
2109    F: FnMut(&[f64]) -> f64,
2110{
2111    match v {
2112        Value::Scalar(n) => Ok(Value::Scalar(f(&[*n]))),
2113        Value::Matrix(m) => {
2114            if m.nrows() == 1 || m.ncols() == 1 {
2115                let vals: Vec<f64> = m.iter().copied().collect();
2116                Ok(Value::Scalar(f(&vals)))
2117            } else {
2118                let ncols = m.ncols();
2119                let result: Vec<f64> = (0..ncols)
2120                    .map(|c| {
2121                        let col: Vec<f64> = m.column(c).iter().copied().collect();
2122                        f(&col)
2123                    })
2124                    .collect();
2125                Ok(Value::Matrix(
2126                    Array2::from_shape_vec((1, ncols), result).unwrap(),
2127                ))
2128            }
2129        }
2130        _ => Err(format!("{fname}: argument must be numeric")),
2131    }
2132}
2133
2134/// Computes the p-th percentile (0–100) of a pre-sorted slice via linear interpolation.
2135fn percentile_sorted(sorted: &[f64], p: f64) -> f64 {
2136    let n = sorted.len();
2137    if n == 0 {
2138        return f64::NAN;
2139    }
2140    if n == 1 {
2141        return sorted[0];
2142    }
2143    let p = p.clamp(0.0, 100.0);
2144    // Octave/MATLAB Type 5 (Hazen): r = n*p/100 + 0.5 (1-indexed), clamped to [1,n].
2145    let idx = (p / 100.0 * n as f64 - 0.5).max(0.0).min((n - 1) as f64);
2146    let lo = idx.floor() as usize;
2147    let hi = idx.ceil() as usize;
2148    let frac = idx - lo as f64;
2149    sorted[lo] * (1.0 - frac) + sorted[hi] * frac
2150}
2151
2152fn apply_elem<F: Fn(f64) -> f64>(v: &Value, f: F) -> Result<Value, String> {
2153    match v {
2154        Value::Void => Err("Element-wise function not applicable to void".to_string()),
2155        Value::Scalar(n) => Ok(Value::Scalar(f(*n))),
2156        Value::Matrix(m) => Ok(Value::Matrix(m.mapv(f))),
2157        Value::Complex(re, im) if *im == 0.0 => Ok(Value::Scalar(f(*re))),
2158        Value::Complex(_, _) => {
2159            Err("Element-wise real function not applicable to complex values".to_string())
2160        }
2161        Value::ComplexMatrix(_) => {
2162            Err("Element-wise real function not applicable to complex matrices".to_string())
2163        }
2164        Value::Str(_) | Value::StringObj(_) => {
2165            Err("Element-wise function not applicable to strings".to_string())
2166        }
2167        Value::Lambda(_)
2168        | Value::Function { .. }
2169        | Value::Tuple(_)
2170        | Value::Cell(_)
2171        | Value::Struct(_)
2172        | Value::StructArray(_)
2173        | Value::DateTime(_)
2174        | Value::Duration(_)
2175        | Value::DateTimeArray(_)
2176        | Value::DurationArray(_) => {
2177            Err("Element-wise function not applicable to this type".to_string())
2178        }
2179    }
2180}
2181
2182/// Reduces a scalar or matrix to a scalar (for vectors) or 1×N row vector (for M×N matrices).
2183///
2184/// - Scalar → apply `f` to `[n]`.
2185/// - Vector (1×N or N×1) → apply `f` to all elements, return scalar.
2186/// - M×N matrix (M>1, N>1) → apply `f` column-wise, return 1×N row vector.
2187fn apply_reduction<F>(v: &Value, f: F) -> Result<Value, String>
2188where
2189    F: Fn(&[f64]) -> f64,
2190{
2191    match v {
2192        Value::Void => Err("Reduction not applicable to void".to_string()),
2193        Value::Scalar(n) => Ok(Value::Scalar(f(&[*n]))),
2194        Value::Complex(_, _) => Err("Reduction not applicable to complex values".to_string()),
2195        Value::ComplexMatrix(_) => Err("Reduction not applicable to complex matrices".to_string()),
2196        Value::Str(_) | Value::StringObj(_) => {
2197            Err("Reduction not applicable to strings".to_string())
2198        }
2199        Value::Lambda(_)
2200        | Value::Function { .. }
2201        | Value::Tuple(_)
2202        | Value::Cell(_)
2203        | Value::Struct(_)
2204        | Value::StructArray(_)
2205        | Value::DateTime(_)
2206        | Value::Duration(_)
2207        | Value::DateTimeArray(_)
2208        | Value::DurationArray(_) => Err("Reduction not applicable to this type".to_string()),
2209        Value::Matrix(m) => {
2210            if m.nrows() == 1 || m.ncols() == 1 {
2211                let vals: Vec<f64> = m.iter().copied().collect();
2212                Ok(Value::Scalar(f(&vals)))
2213            } else {
2214                let ncols = m.ncols();
2215                let result: Vec<f64> = (0..ncols)
2216                    .map(|c| {
2217                        let col: Vec<f64> = m.column(c).iter().copied().collect();
2218                        f(&col)
2219                    })
2220                    .collect();
2221                Ok(Value::Matrix(
2222                    Array2::from_shape_vec((1, ncols), result).unwrap(),
2223                ))
2224            }
2225        }
2226    }
2227}
2228
2229/// Column-wise reduction over a `ComplexMatrix` (or any numeric value treated as complex).
2230///
2231/// For vectors (1×N or N×1) returns a `Complex`/`Scalar`; for M×N matrices returns a
2232/// `ComplexMatrix` 1×N row vector.
2233fn apply_cm_reduction<F>(v: &Value, f: F) -> Result<Value, String>
2234where
2235    F: Fn(&[Complex<f64>]) -> Complex<f64>,
2236{
2237    let make_scalar = |c: Complex<f64>| -> Value {
2238        if c.im == 0.0 {
2239            Value::Scalar(c.re)
2240        } else {
2241            Value::Complex(c.re, c.im)
2242        }
2243    };
2244    match v {
2245        Value::Scalar(n) => Ok(make_scalar(f(&[Complex::new(*n, 0.0)]))),
2246        Value::Complex(re, im) => Ok(make_scalar(f(&[Complex::new(*re, *im)]))),
2247        Value::Matrix(m) => {
2248            if m.nrows() == 1 || m.ncols() == 1 {
2249                let vals: Vec<Complex<f64>> = m.iter().map(|&x| Complex::new(x, 0.0)).collect();
2250                Ok(make_scalar(f(&vals)))
2251            } else {
2252                let ncols = m.ncols();
2253                let result: Vec<Complex<f64>> = (0..ncols)
2254                    .map(|c| {
2255                        let col: Vec<Complex<f64>> =
2256                            m.column(c).iter().map(|&x| Complex::new(x, 0.0)).collect();
2257                        f(&col)
2258                    })
2259                    .collect();
2260                if result.iter().all(|c| c.im == 0.0) {
2261                    let reals: Vec<f64> = result.iter().map(|c| c.re).collect();
2262                    Ok(Value::Matrix(
2263                        Array2::from_shape_vec((1, ncols), reals).unwrap(),
2264                    ))
2265                } else {
2266                    Ok(Value::ComplexMatrix(
2267                        Array2::from_shape_vec((1, ncols), result).unwrap(),
2268                    ))
2269                }
2270            }
2271        }
2272        Value::ComplexMatrix(m) => {
2273            if m.nrows() == 1 || m.ncols() == 1 {
2274                let vals: Vec<Complex<f64>> = m.iter().copied().collect();
2275                Ok(make_scalar(f(&vals)))
2276            } else {
2277                let ncols = m.ncols();
2278                let result: Vec<Complex<f64>> = (0..ncols)
2279                    .map(|c| {
2280                        let col: Vec<Complex<f64>> = m.column(c).iter().copied().collect();
2281                        f(&col)
2282                    })
2283                    .collect();
2284                if result.iter().all(|c| c.im == 0.0) {
2285                    let reals: Vec<f64> = result.iter().map(|c| c.re).collect();
2286                    Ok(Value::Matrix(
2287                        Array2::from_shape_vec((1, ncols), reals).unwrap(),
2288                    ))
2289                } else {
2290                    Ok(Value::ComplexMatrix(
2291                        Array2::from_shape_vec((1, ncols), result).unwrap(),
2292                    ))
2293                }
2294            }
2295        }
2296        _ => Err("Reduction not applicable to this type".to_string()),
2297    }
2298}
2299
2300/// Computes a cumulative scan (cumsum / cumprod) along a vector or column-wise on a matrix.
2301///
2302/// `combine(accumulator, element) -> new_accumulator` — e.g. `|a, x| a + x` for cumsum.
2303fn apply_cumulative<F>(v: &Value, combine: F) -> Result<Value, String>
2304where
2305    F: Fn(f64, f64) -> f64,
2306{
2307    match v {
2308        Value::Void => Err("Cumulative reduction not applicable to void".to_string()),
2309        Value::Scalar(n) => Ok(Value::Scalar(*n)),
2310        Value::Complex(_, _) => {
2311            Err("Cumulative reduction not applicable to complex values".to_string())
2312        }
2313        Value::ComplexMatrix(_) => {
2314            Err("Cumulative reduction not applicable to complex matrices".to_string())
2315        }
2316        Value::Str(_) | Value::StringObj(_) => {
2317            Err("Cumulative reduction not applicable to strings".to_string())
2318        }
2319        Value::Lambda(_)
2320        | Value::Function { .. }
2321        | Value::Tuple(_)
2322        | Value::Cell(_)
2323        | Value::Struct(_)
2324        | Value::StructArray(_)
2325        | Value::DateTime(_)
2326        | Value::Duration(_)
2327        | Value::DateTimeArray(_)
2328        | Value::DurationArray(_) => {
2329            Err("Cumulative reduction not applicable to this type".to_string())
2330        }
2331        Value::Matrix(m) => {
2332            let initial = combine(0.0, 0.0); // detect identity: 0+0=0 or 0*0=0
2333            // Use 0.0 as additive identity, 1.0 as multiplicative identity.
2334            // We detect the identity from f(1.0, 1.0) vs f(0.0, 0.0).
2335            let identity = if (combine(1.0, 1.0) - 1.0).abs() < 1e-15 && initial == 0.0 {
2336                1.0 // product
2337            } else {
2338                0.0 // sum
2339            };
2340            let (nrows, ncols) = (m.nrows(), m.ncols());
2341            let mut result = m.clone();
2342            if nrows == 1 || ncols == 1 {
2343                // Vector: scan along all elements in order
2344                let mut acc = identity;
2345                for v in result.iter_mut() {
2346                    acc = combine(acc, *v);
2347                    *v = acc;
2348                }
2349            } else {
2350                // Matrix: scan each column independently
2351                for c in 0..ncols {
2352                    let mut acc = identity;
2353                    for r in 0..nrows {
2354                        acc = combine(acc, result[[r, c]]);
2355                        result[[r, c]] = acc;
2356                    }
2357                }
2358            }
2359            Ok(Value::Matrix(result))
2360        }
2361    }
2362}
2363
2364/// Returns column-major 1-based indices of non-zero elements, up to `max_k`.
2365fn find_nonzero(v: &Value, max_k: usize) -> Result<Value, String> {
2366    match v {
2367        Value::Void => Err("find: not applicable to void".to_string()),
2368        Value::ComplexMatrix(_) => Err("find: not applicable to complex matrices".to_string()),
2369        Value::Str(_) | Value::StringObj(_) => Err("find: not applicable to strings".to_string()),
2370        Value::Lambda(_)
2371        | Value::Function { .. }
2372        | Value::Tuple(_)
2373        | Value::Cell(_)
2374        | Value::Struct(_)
2375        | Value::StructArray(_)
2376        | Value::DateTime(_)
2377        | Value::Duration(_)
2378        | Value::DateTimeArray(_)
2379        | Value::DurationArray(_) => Err("find: not applicable to this type".to_string()),
2380        Value::Complex(re, im) => {
2381            if (*re != 0.0 || *im != 0.0) && max_k >= 1 {
2382                Ok(Value::Matrix(
2383                    Array2::from_shape_vec((1, 1), vec![1.0]).unwrap(),
2384                ))
2385            } else {
2386                Ok(Value::Matrix(Array2::zeros((1, 0))))
2387            }
2388        }
2389        Value::Scalar(n) => {
2390            if *n != 0.0 && max_k >= 1 {
2391                Ok(Value::Matrix(
2392                    Array2::from_shape_vec((1, 1), vec![1.0]).unwrap(),
2393                ))
2394            } else {
2395                Ok(Value::Matrix(Array2::zeros((1, 0))))
2396            }
2397        }
2398        Value::Matrix(m) => {
2399            let nrows = m.nrows();
2400            let total = m.len();
2401            let mut idxs: Vec<f64> = Vec::new();
2402            for i in 0..total {
2403                if idxs.len() >= max_k {
2404                    break;
2405                }
2406                let row = i % nrows;
2407                let col = i / nrows;
2408                if m[[row, col]] != 0.0 {
2409                    idxs.push((i + 1) as f64);
2410                }
2411            }
2412            let n = idxs.len();
2413            if n == 0 {
2414                Ok(Value::Matrix(Array2::zeros((1, 0))))
2415            } else {
2416                Ok(Value::Matrix(Array2::from_shape_vec((1, n), idxs).unwrap()))
2417            }
2418        }
2419    }
2420}
2421
2422// ---------------------------------------------------------------------------
2423// C-style printf format engine
2424// ---------------------------------------------------------------------------
2425
2426/// Formats `args` using a C-style `fmt` string.
2427///
2428/// Supported specifiers: `%d` `%i` `%f` `%e` `%g` `%s` `%%`.
2429/// Flags: `-` (left-align), `+` (force sign), `0` (zero-pad), ` ` (space sign).
2430/// Width and `.precision` follow standard C `printf` conventions.
2431/// Escape sequences `\n` `\t` `\\` are also processed.
2432///
2433/// Octave behaviour: if `args` is longer than the number of specifiers the
2434/// format string is repeated until all args are consumed.
2435pub fn format_printf(fmt: &str, args: &[Value]) -> Result<String, String> {
2436    let mut result = String::new();
2437    let mut arg_idx = 0;
2438
2439    loop {
2440        let consumed_before = arg_idx;
2441        let mut chars = fmt.chars().peekable();
2442
2443        while let Some(c) = chars.next() {
2444            if c == '\\' {
2445                match chars.next() {
2446                    Some('n') => result.push('\n'),
2447                    Some('t') => result.push('\t'),
2448                    Some('\\') => result.push('\\'),
2449                    Some('\'') => result.push('\''),
2450                    Some('"') => result.push('"'),
2451                    Some(other) => {
2452                        result.push('\\');
2453                        result.push(other);
2454                    }
2455                    None => result.push('\\'),
2456                }
2457                continue;
2458            }
2459
2460            if c != '%' {
2461                result.push(c);
2462                continue;
2463            }
2464
2465            // `%%` → literal `%`
2466            if chars.peek() == Some(&'%') {
2467                chars.next();
2468                result.push('%');
2469                continue;
2470            }
2471
2472            // Parse flags
2473            let mut flag_minus = false;
2474            let mut flag_plus = false;
2475            let mut flag_zero = false;
2476            let mut flag_space = false;
2477            loop {
2478                match chars.peek() {
2479                    Some('-') => {
2480                        flag_minus = true;
2481                        chars.next();
2482                    }
2483                    Some('+') => {
2484                        flag_plus = true;
2485                        chars.next();
2486                    }
2487                    Some('0') => {
2488                        flag_zero = true;
2489                        chars.next();
2490                    }
2491                    Some(' ') => {
2492                        flag_space = true;
2493                        chars.next();
2494                    }
2495                    _ => break,
2496                }
2497            }
2498
2499            // Parse width
2500            let mut width_str = String::new();
2501            while let Some(&d) = chars.peek() {
2502                if d.is_ascii_digit() {
2503                    width_str.push(d);
2504                    chars.next();
2505                } else {
2506                    break;
2507                }
2508            }
2509            let width: usize = width_str.parse().unwrap_or(0);
2510
2511            // Parse precision
2512            let mut precision: Option<usize> = None;
2513            if chars.peek() == Some(&'.') {
2514                chars.next();
2515                let mut p = String::new();
2516                while let Some(&d) = chars.peek() {
2517                    if d.is_ascii_digit() {
2518                        p.push(d);
2519                        chars.next();
2520                    } else {
2521                        break;
2522                    }
2523                }
2524                precision = Some(p.parse().unwrap_or(0));
2525            }
2526
2527            // Specifier character
2528            let spec = match chars.next() {
2529                Some(s) => s,
2530                None => {
2531                    return Err("fprintf: incomplete format specifier at end of string".to_string());
2532                }
2533            };
2534
2535            // No more args — silently skip remaining specifiers
2536            if arg_idx >= args.len() {
2537                continue;
2538            }
2539
2540            let arg = &args[arg_idx];
2541            arg_idx += 1;
2542
2543            let formatted = match spec {
2544                'd' | 'i' => {
2545                    let n = printf_scalar(arg, spec)?;
2546                    let i = n.trunc() as i64;
2547                    let s = printf_sign_str(i >= 0, flag_plus, flag_space, format!("{}", i.abs()));
2548                    printf_pad(s, width, flag_minus, flag_zero)
2549                }
2550                'f' => {
2551                    let n = printf_scalar(arg, spec)?;
2552                    let prec = precision.unwrap_or(6);
2553                    let s = printf_sign_str(
2554                        n >= 0.0,
2555                        flag_plus,
2556                        flag_space,
2557                        format!("{:.prec$}", n.abs(), prec = prec),
2558                    );
2559                    printf_pad(s, width, flag_minus, flag_zero)
2560                }
2561                'e' | 'E' => {
2562                    let n = printf_scalar(arg, spec)?;
2563                    let prec = precision.unwrap_or(6);
2564                    let s = printf_format_sci(n, prec, flag_plus, flag_space, spec == 'E');
2565                    printf_pad(s, width, flag_minus, flag_zero)
2566                }
2567                'g' | 'G' => {
2568                    let n = printf_scalar(arg, spec)?;
2569                    let prec = precision.unwrap_or(6).max(1);
2570                    let s = printf_format_g(n, prec, flag_plus, flag_space, spec == 'G');
2571                    printf_pad(s, width, flag_minus, flag_zero)
2572                }
2573                'x' | 'X' => {
2574                    let n = printf_scalar(arg, spec)?;
2575                    let i = n.trunc() as u64;
2576                    let hex = if spec == 'X' {
2577                        format!("{:X}", i)
2578                    } else {
2579                        format!("{:x}", i)
2580                    };
2581                    printf_pad(hex, width, flag_minus, flag_zero)
2582                }
2583                's' => {
2584                    let s = printf_string(arg)?;
2585                    let s = if let Some(max_len) = precision {
2586                        s.chars().take(max_len).collect::<String>()
2587                    } else {
2588                        s
2589                    };
2590                    printf_pad(s, width, flag_minus, false)
2591                }
2592                other => return Err(format!("fprintf: unknown format specifier '%{other}'")),
2593            };
2594
2595            result.push_str(&formatted);
2596        }
2597
2598        // Stop if all args consumed or no specifiers were found (infinite loop guard)
2599        if arg_idx >= args.len() || arg_idx == consumed_before {
2600            break;
2601        }
2602    }
2603
2604    Ok(result)
2605}
2606
2607/// Extracts a scalar f64 from a Value for use in numeric printf specifiers.
2608fn printf_scalar(v: &Value, spec: char) -> Result<f64, String> {
2609    match v {
2610        Value::Scalar(n) => Ok(*n),
2611        Value::Complex(re, im) if *im == 0.0 => Ok(*re),
2612        Value::Str(s) if s.chars().count() == 1 => Ok(s.chars().next().unwrap() as u32 as f64),
2613        _ => Err(format!(
2614            "fprintf: expected numeric argument for '%{spec}', got {:?}",
2615            std::mem::discriminant(v)
2616        )),
2617    }
2618}
2619
2620/// Extracts a string from a Value for use in `%s`.
2621fn printf_string(v: &Value) -> Result<String, String> {
2622    match v {
2623        Value::Str(s) | Value::StringObj(s) => Ok(s.clone()),
2624        Value::Scalar(n) => Ok(format_number(*n)),
2625        Value::Complex(re, im) => Ok(format_complex(*re, *im, &FormatMode::Custom(6))),
2626        Value::Void => Err("fprintf: cannot format void as string".to_string()),
2627        Value::Matrix(_) => Err("fprintf: cannot format matrix as string".to_string()),
2628        Value::ComplexMatrix(_) => {
2629            Err("fprintf: cannot format complex matrix as string".to_string())
2630        }
2631        Value::DateTime(ts) => Ok(crate::datetime::format_datetime(*ts)),
2632        Value::Duration(s) => Ok(crate::datetime::format_duration(*s)),
2633        Value::Lambda(_)
2634        | Value::Function { .. }
2635        | Value::Tuple(_)
2636        | Value::Cell(_)
2637        | Value::Struct(_)
2638        | Value::StructArray(_)
2639        | Value::DateTimeArray(_)
2640        | Value::DurationArray(_) => Err("fprintf: cannot format this type as string".to_string()),
2641    }
2642}
2643
2644/// Builds a sign-prefixed string: `+n`, ` n`, `-n`, or bare `n`.
2645fn printf_sign_str(positive: bool, flag_plus: bool, flag_space: bool, digits: String) -> String {
2646    if positive {
2647        if flag_plus {
2648            format!("+{digits}")
2649        } else if flag_space {
2650            format!(" {digits}")
2651        } else {
2652            digits
2653        }
2654    } else {
2655        format!("-{digits}")
2656    }
2657}
2658
2659/// Right- or left-pads `s` to at least `width` chars, optionally zero-pads.
2660fn printf_pad(s: String, width: usize, left_align: bool, zero_pad: bool) -> String {
2661    if s.len() >= width {
2662        return s;
2663    }
2664    let pad_len = width - s.len();
2665    if left_align {
2666        format!("{s}{}", " ".repeat(pad_len))
2667    } else if zero_pad {
2668        // Insert zeros after optional sign
2669        let (prefix, rest) = if s.starts_with(['+', '-', ' ']) {
2670            s.split_at(1)
2671        } else {
2672            ("", s.as_str())
2673        };
2674        format!("{prefix}{}{rest}", "0".repeat(pad_len))
2675    } else {
2676        format!("{}{s}", " ".repeat(pad_len))
2677    }
2678}
2679
2680/// Formats `n` in scientific notation matching C `%e` / `%E`.
2681/// Always produces at least 2 exponent digits with an explicit sign: `1.23e+04`.
2682fn printf_format_sci(
2683    n: f64,
2684    prec: usize,
2685    flag_plus: bool,
2686    flag_space: bool,
2687    upper: bool,
2688) -> String {
2689    if n == 0.0 {
2690        let zeros = "0".repeat(prec);
2691        let sep = if prec > 0 {
2692            format!(".{zeros}")
2693        } else {
2694            String::new()
2695        };
2696        let e_char = if upper { 'E' } else { 'e' };
2697        let sign = if flag_plus {
2698            "+"
2699        } else if flag_space {
2700            " "
2701        } else {
2702            ""
2703        };
2704        return format!("{sign}0{sep}{e_char}+00");
2705    }
2706
2707    let neg = n < 0.0;
2708    let abs_n = n.abs();
2709    let exp = abs_n.log10().floor() as i32;
2710    let mantissa = abs_n / 10f64.powi(exp);
2711    let man_str = format!("{:.prec$}", mantissa, prec = prec);
2712
2713    let e_char = if upper { 'E' } else { 'e' };
2714    let exp_sign = if exp >= 0 { '+' } else { '-' };
2715    let exp_abs = exp.unsigned_abs();
2716    let exp_str = if exp_abs < 10 {
2717        format!("{e_char}{exp_sign}0{exp_abs}")
2718    } else {
2719        format!("{e_char}{exp_sign}{exp_abs}")
2720    };
2721
2722    let sign_str = if neg {
2723        "-"
2724    } else if flag_plus {
2725        "+"
2726    } else if flag_space {
2727        " "
2728    } else {
2729        ""
2730    };
2731    format!("{sign_str}{man_str}{exp_str}")
2732}
2733
2734/// Formats `n` using `%g` / `%G` rules:
2735/// uses `%e` if exponent < -4 or >= prec, otherwise `%f`; trims trailing zeros.
2736fn printf_format_g(n: f64, prec: usize, flag_plus: bool, flag_space: bool, upper: bool) -> String {
2737    if n == 0.0 {
2738        let sign = if flag_plus {
2739            "+"
2740        } else if flag_space {
2741            " "
2742        } else {
2743            ""
2744        };
2745        return format!("{sign}0");
2746    }
2747    let abs_n = n.abs();
2748    let exp = abs_n.log10().floor() as i32;
2749    if exp < -4 || exp >= prec as i32 {
2750        let s = printf_format_sci(n, prec.saturating_sub(1), flag_plus, flag_space, upper);
2751        trim_g_sci(s, upper)
2752    } else {
2753        let decimal_places = (prec as i32 - 1 - exp).max(0) as usize;
2754        let neg = n < 0.0;
2755        let s = format!("{:.prec$}", abs_n, prec = decimal_places);
2756        let s = if s.contains('.') {
2757            s.trim_end_matches('0').trim_end_matches('.').to_string()
2758        } else {
2759            s
2760        };
2761        let sign = if neg {
2762            "-"
2763        } else if flag_plus {
2764            "+"
2765        } else if flag_space {
2766            " "
2767        } else {
2768            ""
2769        };
2770        format!("{sign}{s}")
2771    }
2772}
2773
2774/// Trims trailing zeros from the mantissa of a scientific-notation string `1.230e+04` → `1.23e+04`.
2775fn trim_g_sci(s: String, upper: bool) -> String {
2776    let e_char = if upper { 'E' } else { 'e' };
2777    if let Some(e_pos) = s.find(e_char) {
2778        let mantissa = &s[..e_pos];
2779        let exp_part = &s[e_pos..];
2780        let trimmed = if mantissa.contains('.') {
2781            mantissa.trim_end_matches('0').trim_end_matches('.')
2782        } else {
2783            mantissa
2784        };
2785        format!("{trimmed}{exp_part}")
2786    } else {
2787        s
2788    }
2789}
2790
2791/// Calls a `Lambda` or `Function` value with the given arguments.
2792///
2793/// Used by `cellfun` and `arrayfun` to apply a function to each element
2794/// without going through the name-lookup path.
2795fn call_function_value(
2796    f: &Value,
2797    args: &[Value],
2798    io: Option<&mut IoContext>,
2799) -> Result<Value, String> {
2800    match f {
2801        Value::Lambda(lf) => {
2802            let lf = lf.clone();
2803            lf.0(args, io)
2804        }
2805        Value::Function { .. } => {
2806            // Named function called via cellfun/arrayfun — name is unknown at this point.
2807            // Use a minimal env that doesn't export any user variables to avoid
2808            // polluting the caller's scope. Functions see their own scope via exec.
2809            let empty_env = Env::new();
2810            match io {
2811                Some(io_ref) => FN_CALL_HOOK.with(|c| match c.get() {
2812                    Some(hook) => hook("<anonymous>", f, args, &empty_env, io_ref),
2813                    None => Err("User function execution not initialized".to_string()),
2814                }),
2815                None => {
2816                    let mut tmp_io = IoContext::new();
2817                    FN_CALL_HOOK.with(|c| match c.get() {
2818                        Some(hook) => hook("<anonymous>", f, args, &empty_env, &mut tmp_io),
2819                        None => Err("User function execution not initialized".to_string()),
2820                    })
2821                }
2822            }
2823        }
2824        _ => Err("cellfun/arrayfun: first argument must be a function or lambda (@fn)".to_string()),
2825    }
2826}
2827
2828/// Names of all built-in functions recognized by `call_builtin`.
2829///
2830/// Used for REPL tab completion and "did you mean?" suggestions.
2831pub fn builtin_names() -> &'static [&'static str] {
2832    &[
2833        "abs",
2834        "acos",
2835        "all",
2836        "angle",
2837        "any",
2838        "arrayfun",
2839        "asin",
2840        "assert",
2841        "atan",
2842        "atan2",
2843        "bitand",
2844        "bitnot",
2845        "bitor",
2846        "bitshift",
2847        "bitxor",
2848        "ceil",
2849        "cell",
2850        "cellfun",
2851        "chol",
2852        "complex",
2853        "cond",
2854        "cross",
2855        "conj",
2856        "contains",
2857        "conv",
2858        "cos",
2859        "cov",
2860        "cumprod",
2861        "cumsum",
2862        "datenum",
2863        "datestr",
2864        "datevec",
2865        "datetime",
2866        "day",
2867        "days",
2868        "deconv",
2869        "det",
2870        "diag",
2871        "diff",
2872        "dot",
2873        "disp",
2874        "dlmread",
2875        "dlmwrite",
2876        "eig",
2877        "endsWith",
2878        "erf",
2879        "eval",
2880        "erfc",
2881        "exist",
2882        "exp",
2883        "eye",
2884        "fclose",
2885        "fft",
2886        "fftfreq",
2887        "fftshift",
2888        "fgetl",
2889        "fgets",
2890        "fieldnames",
2891        "ifft",
2892        "ifftshift",
2893        "figure",
2894        "find",
2895        "fliplr",
2896        "flipud",
2897        "floor",
2898        "fopen",
2899        "fprintf",
2900        "genpath",
2901        "histc",
2902        "hour",
2903        "hours",
2904        "hypot",
2905        "imag",
2906        "ind2sub",
2907        "int2str",
2908        "interp1",
2909        "intersect",
2910        "inv",
2911        "iqr",
2912        "iscell",
2913        "ismember",
2914        "ischar",
2915        "isdatetime",
2916        "isduration",
2917        "isempty",
2918        "isfield",
2919        "isfile",
2920        "isfinite",
2921        "isfolder",
2922        "isinf",
2923        "isnan",
2924        "isnat",
2925        "isreal",
2926        "isstring",
2927        "isstruct",
2928        "jsonencode",
2929        "jsondecode",
2930        "kron",
2931        "kurtosis",
2932        "lasterr",
2933        "length",
2934        "linspace",
2935        "load",
2936        "log",
2937        "log10",
2938        "log2",
2939        "lower",
2940        "lu",
2941        "mat2str",
2942        "max",
2943        "mean",
2944        "median",
2945        "meshgrid",
2946        "milliseconds",
2947        "min",
2948        "minute",
2949        "minutes",
2950        "mod",
2951        "mode",
2952        "month",
2953        "nan",
2954        "norm",
2955        "normcdf",
2956        "normpdf",
2957        "not",
2958        "null",
2959        "num2str",
2960        "numel",
2961        "ones",
2962        "orth",
2963        "pinv",
2964        "poly",
2965        "polyfit",
2966        "polyval",
2967        "posixtime",
2968        "prctile",
2969        "prod",
2970        "qr",
2971        "rand",
2972        "randi",
2973        "randn",
2974        "rank",
2975        "readmatrix",
2976        "readtable",
2977        "real",
2978        "regexp",
2979        "regexpi",
2980        "regexprep",
2981        "rem",
2982        "repelem",
2983        "repmat",
2984        "reshape",
2985        "rmfield",
2986        "rng",
2987        "roots",
2988        "round",
2989        "setdiff",
2990        "second",
2991        "seconds",
2992        "sign",
2993        "sin",
2994        "size",
2995        "skewness",
2996        "sort",
2997        "sprintf",
2998        "sqrt",
2999        "startsWith",
3000        "std",
3001        "str2double",
3002        "str2num",
3003        "strcmp",
3004        "strcmpi",
3005        "strjoin",
3006        "strrep",
3007        "strsplit",
3008        "strtrim",
3009        "sub2ind",
3010        "sum",
3011        "svd",
3012        "tan",
3013        "tic",
3014        "toc",
3015        "trace",
3016        "tril",
3017        "triu",
3018        "union",
3019        "unique",
3020        "upper",
3021        "var",
3022        "writetable",
3023        "xor",
3024        "year",
3025        "years",
3026        "zeros",
3027        "zscore",
3028    ]
3029}
3030
3031/// Computes the Levenshtein edit distance between two strings.
3032fn levenshtein(a: &str, b: &str) -> usize {
3033    let a: Vec<char> = a.chars().collect();
3034    let b: Vec<char> = b.chars().collect();
3035    let (m, n) = (a.len(), b.len());
3036    let mut row: Vec<usize> = (0..=n).collect();
3037    for i in 1..=m {
3038        let mut prev = row[0];
3039        row[0] = i;
3040        for j in 1..=n {
3041            let next = if a[i - 1] == b[j - 1] {
3042                prev
3043            } else {
3044                1 + prev.min(row[j]).min(row[j - 1])
3045            };
3046            prev = row[j];
3047            row[j] = next;
3048        }
3049    }
3050    row[n]
3051}
3052
3053/// Finds the closest name in `env` keys and built-in names within Levenshtein distance 2.
3054fn suggest_similar(name: &str, env: &Env) -> Option<String> {
3055    const MAX_DIST: usize = 2;
3056    let mut best: Option<(String, usize)> = None;
3057    let mut update = |candidate: &str| {
3058        let d = levenshtein(name, candidate);
3059        if d <= MAX_DIST && best.as_ref().is_none_or(|(_, bd)| d < *bd) {
3060            best = Some((candidate.to_string(), d));
3061        }
3062    };
3063    for key in env.keys() {
3064        update(key);
3065    }
3066    for &bname in builtin_names() {
3067        update(bname);
3068    }
3069    best.map(|(s, _)| s)
3070}
3071
3072/// Checks equality of two values for `assert(a, b[, tol])`.
3073fn assert_values_equal(a: &Value, b: &Value, tol: Option<f64>) -> Result<Value, String> {
3074    match (a, b) {
3075        (Value::Scalar(x), Value::Scalar(y)) => {
3076            let ok = match tol {
3077                None => x == y,
3078                Some(t) => (x - y).abs() <= t,
3079            };
3080            if ok {
3081                Ok(Value::Void)
3082            } else if let Some(t) = tol {
3083                Err(format!(
3084                    "assert: |{x} - {y}| = {} exceeds tolerance {t}",
3085                    (x - y).abs()
3086                ))
3087            } else {
3088                Err(format!("assert: {x} ~= {y}"))
3089            }
3090        }
3091        (Value::Matrix(ma), Value::Matrix(mb)) => {
3092            if ma.shape() != mb.shape() {
3093                return Err(format!(
3094                    "assert: size mismatch [{}×{}] vs [{}×{}]",
3095                    ma.nrows(),
3096                    ma.ncols(),
3097                    mb.nrows(),
3098                    mb.ncols()
3099                ));
3100            }
3101            for (x, y) in ma.iter().zip(mb.iter()) {
3102                let ok = match tol {
3103                    None => x == y,
3104                    Some(t) => (x - y).abs() <= t,
3105                };
3106                if !ok {
3107                    if let Some(t) = tol {
3108                        return Err(format!(
3109                            "assert: difference {} exceeds tolerance {t}",
3110                            (x - y).abs()
3111                        ));
3112                    } else {
3113                        return Err(format!("assert: {x} ~= {y}"));
3114                    }
3115                }
3116            }
3117            Ok(Value::Void)
3118        }
3119        _ => {
3120            if tol.is_some() {
3121                return Err("assert: tolerance requires numeric arguments".to_string());
3122            }
3123            if a == b {
3124                Ok(Value::Void)
3125            } else {
3126                Err("assert: values not equal".to_string())
3127            }
3128        }
3129    }
3130}
3131
3132fn call_builtin(
3133    name: &str,
3134    args: &[Value],
3135    env: &Env,
3136    mut io: Option<&mut IoContext>,
3137) -> Result<Value, String> {
3138    // Plugins are checked first so they can shadow built-ins.
3139    if let Some(result) = crate::plugin::call_plugin(name, args, env) {
3140        return result;
3141    }
3142
3143    match (name, args.len()) {
3144        // --- 1-argument scalar functions ---
3145        ("sqrt", 1) => match &args[0] {
3146            Value::Scalar(x) if *x < 0.0 => Ok(make_complex(0.0, (-x).sqrt())),
3147            Value::Complex(re, im) => {
3148                let mag = (*re * *re + *im * *im).sqrt();
3149                let sqrt_mag = mag.sqrt();
3150                let arg = (*im).atan2(*re) / 2.0;
3151                Ok(make_complex(sqrt_mag * arg.cos(), sqrt_mag * arg.sin()))
3152            }
3153            _ => apply_elem(&args[0], |x| x.sqrt()),
3154        },
3155        ("floor", 1) => apply_elem(&args[0], |x| x.floor()),
3156        ("ceil", 1) => apply_elem(&args[0], |x| x.ceil()),
3157        ("round", 1) => apply_elem(&args[0], |x| x.round()),
3158        ("sign", 1) => apply_elem(&args[0], |x| x.signum()),
3159        ("log", 1) => apply_elem(&args[0], |x| x.ln()),
3160        ("log2", 1) => apply_elem(&args[0], |x| x.log2()),
3161        ("log10", 1) => apply_elem(&args[0], |x| x.log10()),
3162        ("exp", 1) => match &args[0] {
3163            Value::Complex(re, im) => {
3164                let e = re.exp();
3165                Ok(make_complex(e * im.cos(), e * im.sin()))
3166            }
3167            _ => apply_elem(&args[0], |x| x.exp()),
3168        },
3169        ("sin", 1) => apply_elem(&args[0], |x| x.sin()),
3170        ("cos", 1) => apply_elem(&args[0], |x| x.cos()),
3171        ("tan", 1) => apply_elem(&args[0], |x| x.tan()),
3172        ("asin", 1) => apply_elem(&args[0], |x| x.asin()),
3173        ("acos", 1) => apply_elem(&args[0], |x| x.acos()),
3174        ("atan", 1) => apply_elem(&args[0], |x| x.atan()),
3175        // --- Special functions (erf, normal distribution) ---
3176        ("erf", 1) => apply_elem(&args[0], libm::erf),
3177        ("erfc", 1) => apply_elem(&args[0], libm::erfc),
3178        ("normcdf", 1) => apply_elem(&args[0], |x| {
3179            0.5 * (1.0 + libm::erf(x / std::f64::consts::SQRT_2))
3180        }),
3181        ("normcdf", 3) => {
3182            let mu = scalar_arg(&args[1], name, 2)?;
3183            let s = scalar_arg(&args[2], name, 3)?;
3184            if s <= 0.0 {
3185                return Err("normcdf: sigma must be positive".to_string());
3186            }
3187            apply_elem(&args[0], move |x| {
3188                0.5 * (1.0 + libm::erf((x - mu) / (s * std::f64::consts::SQRT_2)))
3189            })
3190        }
3191        ("normpdf", 1) => apply_elem(&args[0], |x| {
3192            (-0.5 * x * x).exp() / (2.0 * std::f64::consts::PI).sqrt()
3193        }),
3194        ("normpdf", 3) => {
3195            let mu = scalar_arg(&args[1], name, 2)?;
3196            let s = scalar_arg(&args[2], name, 3)?;
3197            if s <= 0.0 {
3198                return Err("normpdf: sigma must be positive".to_string());
3199            }
3200            apply_elem(&args[0], move |x| {
3201                let z = (x - mu) / s;
3202                (-0.5 * z * z).exp() / (s * (2.0 * std::f64::consts::PI).sqrt())
3203            })
3204        }
3205        // --- 2-argument scalar functions ---
3206        ("atan2", 2) => Ok(Value::Scalar(
3207            scalar_arg(&args[0], name, 1)?.atan2(scalar_arg(&args[1], name, 2)?),
3208        )),
3209        ("mod", 2) => {
3210            let a = scalar_arg(&args[0], name, 1)?;
3211            let b = scalar_arg(&args[1], name, 2)?;
3212            Ok(Value::Scalar(a - b * (a / b).floor()))
3213        }
3214        ("rem", 2) => {
3215            let a = scalar_arg(&args[0], name, 1)?;
3216            let b = scalar_arg(&args[1], name, 2)?;
3217            Ok(Value::Scalar(a - b * (a / b).trunc()))
3218        }
3219        ("max", 2) => Ok(Value::Scalar(
3220            scalar_arg(&args[0], name, 1)?.max(scalar_arg(&args[1], name, 2)?),
3221        )),
3222        ("min", 2) => Ok(Value::Scalar(
3223            scalar_arg(&args[0], name, 1)?.min(scalar_arg(&args[1], name, 2)?),
3224        )),
3225        ("hypot", 2) => Ok(Value::Scalar(
3226            scalar_arg(&args[0], name, 1)?.hypot(scalar_arg(&args[1], name, 2)?),
3227        )),
3228        ("log", 2) => Ok(Value::Scalar(
3229            scalar_arg(&args[0], name, 1)?.log(scalar_arg(&args[1], name, 2)?),
3230        )),
3231        // --- Matrix constructors ---
3232        ("zeros", 1) => {
3233            let (r, c) = size_arg(&args[0], name)?;
3234            Ok(Value::Matrix(Array2::zeros((r, c))))
3235        }
3236        ("zeros", 2) => {
3237            let r = scalar_arg(&args[0], name, 1)? as usize;
3238            let c = scalar_arg(&args[1], name, 2)? as usize;
3239            Ok(Value::Matrix(Array2::zeros((r, c))))
3240        }
3241        ("ones", 1) => {
3242            let (r, c) = size_arg(&args[0], name)?;
3243            Ok(Value::Matrix(Array2::ones((r, c))))
3244        }
3245        ("ones", 2) => {
3246            let r = scalar_arg(&args[0], name, 1)? as usize;
3247            let c = scalar_arg(&args[1], name, 2)? as usize;
3248            Ok(Value::Matrix(Array2::ones((r, c))))
3249        }
3250        ("eye", 1) => {
3251            let n = scalar_arg(&args[0], name, 1)? as usize;
3252            let mut m = Array2::<f64>::zeros((n, n));
3253            for i in 0..n {
3254                m[[i, i]] = 1.0;
3255            }
3256            Ok(Value::Matrix(m))
3257        }
3258        // --- Matrix properties ---
3259        ("size", 1) => match &args[0] {
3260            Value::Void => Err("size: not applicable to void".to_string()),
3261            Value::Scalar(_) | Value::Complex(_, _) | Value::Struct(_) => Ok(Value::Matrix(
3262                Array2::from_shape_vec((1, 2), vec![1.0, 1.0]).unwrap(),
3263            )),
3264            Value::Matrix(m) => Ok(Value::Matrix(
3265                Array2::from_shape_vec((1, 2), vec![m.nrows() as f64, m.ncols() as f64]).unwrap(),
3266            )),
3267            Value::ComplexMatrix(m) => Ok(Value::Matrix(
3268                Array2::from_shape_vec((1, 2), vec![m.nrows() as f64, m.ncols() as f64]).unwrap(),
3269            )),
3270            Value::Str(s) => Ok(Value::Matrix(
3271                Array2::from_shape_vec((1, 2), vec![1.0, s.chars().count() as f64]).unwrap(),
3272            )),
3273            Value::StringObj(s) => Ok(Value::Matrix(
3274                Array2::from_shape_vec((1, 2), vec![1.0, s.chars().count() as f64]).unwrap(),
3275            )),
3276            Value::Cell(v) => Ok(Value::Matrix(
3277                Array2::from_shape_vec((1, 2), vec![1.0, v.len() as f64]).unwrap(),
3278            )),
3279            Value::StructArray(arr) => Ok(Value::Matrix(
3280                Array2::from_shape_vec((1, 2), vec![1.0, arr.len() as f64]).unwrap(),
3281            )),
3282            Value::Lambda(_)
3283            | Value::Function { .. }
3284            | Value::Tuple(_)
3285            | Value::DateTime(_)
3286            | Value::Duration(_)
3287            | Value::DateTimeArray(_)
3288            | Value::DurationArray(_) => Err("size: not applicable to this type".to_string()),
3289        },
3290        ("size", 2) => {
3291            let dim = scalar_arg(&args[1], name, 2)? as usize;
3292            match &args[0] {
3293                Value::Void => Err("size: not applicable to void".to_string()),
3294                Value::Scalar(_) | Value::Complex(_, _) | Value::Struct(_) => {
3295                    Ok(Value::Scalar(1.0))
3296                }
3297                Value::Matrix(m) => match dim {
3298                    1 => Ok(Value::Scalar(m.nrows() as f64)),
3299                    2 => Ok(Value::Scalar(m.ncols() as f64)),
3300                    _ => Err(format!("size: invalid dimension {dim}, must be 1 or 2")),
3301                },
3302                Value::ComplexMatrix(m) => match dim {
3303                    1 => Ok(Value::Scalar(m.nrows() as f64)),
3304                    2 => Ok(Value::Scalar(m.ncols() as f64)),
3305                    _ => Err(format!("size: invalid dimension {dim}, must be 1 or 2")),
3306                },
3307                Value::Str(s) => match dim {
3308                    1 => Ok(Value::Scalar(1.0)),
3309                    2 => Ok(Value::Scalar(s.chars().count() as f64)),
3310                    _ => Err(format!("size: invalid dimension {dim}")),
3311                },
3312                Value::StringObj(s) => match dim {
3313                    1 => Ok(Value::Scalar(1.0)),
3314                    2 => Ok(Value::Scalar(s.chars().count() as f64)),
3315                    _ => Err(format!("size: invalid dimension {dim}")),
3316                },
3317                Value::Cell(v) => match dim {
3318                    1 => Ok(Value::Scalar(1.0)),
3319                    2 => Ok(Value::Scalar(v.len() as f64)),
3320                    _ => Err(format!("size: invalid dimension {dim}")),
3321                },
3322                Value::StructArray(arr) => match dim {
3323                    1 => Ok(Value::Scalar(1.0)),
3324                    2 => Ok(Value::Scalar(arr.len() as f64)),
3325                    _ => Err(format!("size: invalid dimension {dim}")),
3326                },
3327                Value::Lambda(_)
3328                | Value::Function { .. }
3329                | Value::Tuple(_)
3330                | Value::DateTime(_)
3331                | Value::Duration(_)
3332                | Value::DateTimeArray(_)
3333                | Value::DurationArray(_) => Err("size: not applicable to this type".to_string()),
3334            }
3335        }
3336        ("length", 1) => match &args[0] {
3337            Value::Void => Err("length: not applicable to void".to_string()),
3338            Value::Scalar(_) | Value::Complex(_, _) | Value::Struct(_) => Ok(Value::Scalar(1.0)),
3339            Value::Matrix(m) => Ok(Value::Scalar(m.nrows().max(m.ncols()) as f64)),
3340            Value::ComplexMatrix(m) => Ok(Value::Scalar(m.nrows().max(m.ncols()) as f64)),
3341            Value::Str(s) => Ok(Value::Scalar(s.chars().count() as f64)),
3342            Value::StringObj(s) => Ok(Value::Scalar(s.chars().count() as f64)),
3343            Value::Cell(v) => Ok(Value::Scalar(v.len() as f64)),
3344            Value::StructArray(arr) => Ok(Value::Scalar(arr.len() as f64)),
3345            Value::DateTimeArray(v) | Value::DurationArray(v) => Ok(Value::Scalar(v.len() as f64)),
3346            Value::DateTime(_) | Value::Duration(_) => Ok(Value::Scalar(1.0)),
3347            Value::Lambda(_) | Value::Function { .. } | Value::Tuple(_) => {
3348                Err("length: not applicable to function values".to_string())
3349            }
3350        },
3351        ("numel", 1) => match &args[0] {
3352            Value::Void => Err("numel: not applicable to void".to_string()),
3353            Value::Scalar(_) | Value::Complex(_, _) | Value::Struct(_) => Ok(Value::Scalar(1.0)),
3354            Value::Matrix(m) => Ok(Value::Scalar(m.len() as f64)),
3355            Value::ComplexMatrix(m) => Ok(Value::Scalar(m.len() as f64)),
3356            Value::Str(s) => Ok(Value::Scalar(s.chars().count() as f64)),
3357            Value::StringObj(s) => Ok(Value::Scalar(s.chars().count() as f64)),
3358            Value::Cell(v) => Ok(Value::Scalar(v.len() as f64)),
3359            Value::StructArray(arr) => Ok(Value::Scalar(arr.len() as f64)),
3360            Value::DateTimeArray(v) | Value::DurationArray(v) => Ok(Value::Scalar(v.len() as f64)),
3361            Value::DateTime(_) | Value::Duration(_) => Ok(Value::Scalar(1.0)),
3362            Value::Lambda(_) | Value::Function { .. } | Value::Tuple(_) => {
3363                Err("numel: not applicable to function values".to_string())
3364            }
3365        },
3366        ("trace", 1) => match &args[0] {
3367            Value::Void => Err("trace: not applicable to void".to_string()),
3368            Value::Scalar(n) => Ok(Value::Scalar(*n)),
3369            Value::Complex(re, _) => Ok(Value::Scalar(*re)),
3370            Value::Matrix(m) => {
3371                let n = m.nrows().min(m.ncols());
3372                Ok(Value::Scalar((0..n).map(|i| m[[i, i]]).sum()))
3373            }
3374            Value::ComplexMatrix(m) => {
3375                let n = m.nrows().min(m.ncols());
3376                let s: Complex<f64> = (0..n).map(|i| m[[i, i]]).sum();
3377                Ok(if s.im == 0.0 {
3378                    Value::Scalar(s.re)
3379                } else {
3380                    Value::Complex(s.re, s.im)
3381                })
3382            }
3383            Value::Str(_)
3384            | Value::StringObj(_)
3385            | Value::Lambda(_)
3386            | Value::Function { .. }
3387            | Value::Tuple(_)
3388            | Value::Cell(_)
3389            | Value::Struct(_)
3390            | Value::StructArray(_)
3391            | Value::DateTime(_)
3392            | Value::Duration(_)
3393            | Value::DateTimeArray(_)
3394            | Value::DurationArray(_) => {
3395                Err("trace: not applicable to non-numeric values".to_string())
3396            }
3397        },
3398        ("det", 1) => match &args[0] {
3399            Value::Void => Err("det: not applicable to void".to_string()),
3400            Value::Scalar(n) => Ok(Value::Scalar(*n)),
3401            Value::Complex(_, _) => Err("det: not applicable to complex scalars".to_string()),
3402            Value::ComplexMatrix(_) => Err("det: not supported for complex matrices".to_string()),
3403            Value::Matrix(m) => Ok(Value::Scalar(det_matrix(m)?)),
3404            Value::Str(_)
3405            | Value::StringObj(_)
3406            | Value::Lambda(_)
3407            | Value::Function { .. }
3408            | Value::Tuple(_)
3409            | Value::Cell(_)
3410            | Value::Struct(_)
3411            | Value::StructArray(_)
3412            | Value::DateTime(_)
3413            | Value::Duration(_)
3414            | Value::DateTimeArray(_)
3415            | Value::DurationArray(_) => {
3416                Err("det: not applicable to non-numeric values".to_string())
3417            }
3418        },
3419        ("inv", 1) => match &args[0] {
3420            Value::Void => Err("inv: not applicable to void".to_string()),
3421            Value::Scalar(n) => {
3422                if *n == 0.0 {
3423                    Err("inv: singular (zero scalar)".to_string())
3424                } else {
3425                    Ok(Value::Scalar(1.0 / n))
3426                }
3427            }
3428            Value::Complex(re, im) => {
3429                // 1/(a+bi) = (a-bi)/(a²+b²)
3430                let denom = re * re + im * im;
3431                if denom == 0.0 {
3432                    Err("inv: singular (zero complex)".to_string())
3433                } else {
3434                    Ok(make_complex(re / denom, -im / denom))
3435                }
3436            }
3437            Value::Matrix(m) => Ok(Value::Matrix(inv_matrix(m)?)),
3438            Value::ComplexMatrix(_) => Err("inv: not supported for complex matrices".to_string()),
3439            Value::Str(_)
3440            | Value::StringObj(_)
3441            | Value::Lambda(_)
3442            | Value::Function { .. }
3443            | Value::Tuple(_)
3444            | Value::Cell(_)
3445            | Value::Struct(_)
3446            | Value::StructArray(_)
3447            | Value::DateTime(_)
3448            | Value::Duration(_)
3449            | Value::DateTimeArray(_)
3450            | Value::DurationArray(_) => {
3451                Err("inv: not applicable to non-numeric values".to_string())
3452            }
3453        },
3454        // --- Range / linspace ---
3455        ("linspace", 3) => {
3456            let a = scalar_arg(&args[0], name, 1)?;
3457            let b = scalar_arg(&args[1], name, 2)?;
3458            let n = scalar_arg(&args[2], name, 3)? as usize;
3459            if n == 0 {
3460                return Ok(Value::Matrix(Array2::zeros((1, 0))));
3461            }
3462            if n == 1 {
3463                return Ok(Value::Matrix(
3464                    Array2::from_shape_vec((1, 1), vec![b]).unwrap(),
3465                ));
3466            }
3467            let vals: Vec<f64> = (0..n)
3468                .map(|i| a + (b - a) * i as f64 / (n - 1) as f64)
3469                .collect();
3470            Ok(Value::Matrix(Array2::from_shape_vec((1, n), vals).unwrap()))
3471        }
3472        // --- Bitwise functions ---
3473        // All operands are truncated to i64. Results are non-negative integers
3474        // returned as f64.  For bitnot the bit-width defines the mask.
3475        ("bitand", 2) => {
3476            let a = to_bits(scalar_arg(&args[0], name, 1)?, name, 1)?;
3477            let b = to_bits(scalar_arg(&args[1], name, 2)?, name, 2)?;
3478            Ok(Value::Scalar((a & b) as f64))
3479        }
3480        ("bitor", 2) => {
3481            let a = to_bits(scalar_arg(&args[0], name, 1)?, name, 1)?;
3482            let b = to_bits(scalar_arg(&args[1], name, 2)?, name, 2)?;
3483            Ok(Value::Scalar((a | b) as f64))
3484        }
3485        ("bitxor", 2) => {
3486            let a = to_bits(scalar_arg(&args[0], name, 1)?, name, 1)?;
3487            let b = to_bits(scalar_arg(&args[1], name, 2)?, name, 2)?;
3488            Ok(Value::Scalar((a ^ b) as f64))
3489        }
3490        // bitshift(a, n): n > 0 → left shift; n < 0 → logical right shift.
3491        // Shifts of 64 or more return 0.
3492        ("bitshift", 2) => {
3493            let a = to_bits(scalar_arg(&args[0], name, 1)?, name, 1)?;
3494            let n = scalar_arg(&args[1], name, 2)?;
3495            if n.fract() != 0.0 {
3496                return Err("bitshift: shift amount must be an integer".to_string());
3497            }
3498            let n = n as i64;
3499            let result: u64 = if n >= 64 || n <= -64 {
3500                0
3501            } else if n >= 0 {
3502                a.wrapping_shl(n as u32)
3503            } else {
3504                a.wrapping_shr((-n) as u32)
3505            };
3506            Ok(Value::Scalar(result as f64))
3507        }
3508        // bitnot(a)        — NOT within 32-bit window (Octave uint32 default)
3509        // bitnot(a, bits)  — NOT within explicit bit-width window (1–53)
3510        ("bitnot", 1) => {
3511            let a = to_bits(scalar_arg(&args[0], name, 1)?, name, 1)?;
3512            let mask: u64 = 0xFFFF_FFFF;
3513            Ok(Value::Scalar(((a ^ mask) & mask) as f64))
3514        }
3515        ("bitnot", 2) => {
3516            let a = to_bits(scalar_arg(&args[0], name, 1)?, name, 1)?;
3517            let bits = scalar_arg(&args[1], name, 2)?;
3518            if bits.fract() != 0.0 || !(1.0..=53.0).contains(&bits) {
3519                return Err(format!(
3520                    "bitnot: bit-width must be an integer in [1, 53], got {bits}"
3521                ));
3522            }
3523            let mask: u64 = (1u64 << bits as u32) - 1;
3524            Ok(Value::Scalar(((a ^ mask) & mask) as f64))
3525        }
3526        // --- Special constant predicates (element-wise) ---
3527        ("isnan", 1) => apply_elem(&args[0], |x| if x.is_nan() { 1.0 } else { 0.0 }),
3528        ("isinf", 1) => apply_elem(&args[0], |x| if x.is_infinite() { 1.0 } else { 0.0 }),
3529        ("isfinite", 1) => apply_elem(&args[0], |x| if x.is_finite() { 1.0 } else { 0.0 }),
3530        // --- NaN matrix constructors ---
3531        ("nan", 1) => {
3532            let (r, c) = size_arg(&args[0], name)?;
3533            Ok(Value::Matrix(Array2::from_elem((r, c), f64::NAN)))
3534        }
3535        ("nan", 2) => {
3536            let r = scalar_arg(&args[0], name, 1)? as usize;
3537            let c = scalar_arg(&args[1], name, 2)? as usize;
3538            Ok(Value::Matrix(Array2::from_elem((r, c), f64::NAN)))
3539        }
3540        // --- Random number generation ---
3541        ("rand", 0) => Ok(Value::Scalar(rand_uniform())),
3542        ("rand", 1) => {
3543            let (r, c) = size_arg(&args[0], name)?;
3544            let data: Vec<f64> = (0..r * c).map(|_| rand_uniform()).collect();
3545            Ok(Value::Matrix(Array2::from_shape_vec((r, c), data).unwrap()))
3546        }
3547        ("rand", 2) => {
3548            let r = scalar_arg(&args[0], name, 1)? as usize;
3549            let c = scalar_arg(&args[1], name, 2)? as usize;
3550            let data: Vec<f64> = (0..r * c).map(|_| rand_uniform()).collect();
3551            Ok(Value::Matrix(Array2::from_shape_vec((r, c), data).unwrap()))
3552        }
3553        ("randn", 0) => Ok(Value::Scalar(rand_normal())),
3554        ("randn", 1) => {
3555            let (r, c) = size_arg(&args[0], name)?;
3556            let data: Vec<f64> = (0..r * c).map(|_| rand_normal()).collect();
3557            Ok(Value::Matrix(Array2::from_shape_vec((r, c), data).unwrap()))
3558        }
3559        ("randn", 2) => {
3560            let r = scalar_arg(&args[0], name, 1)? as usize;
3561            let c = scalar_arg(&args[1], name, 2)? as usize;
3562            let data: Vec<f64> = (0..r * c).map(|_| rand_normal()).collect();
3563            Ok(Value::Matrix(Array2::from_shape_vec((r, c), data).unwrap()))
3564        }
3565        ("randi", 1) => {
3566            let (lo, hi) = randi_range(&args[0])?;
3567            let v = RNG.with(|r| r.borrow_mut().gen_range(lo..=hi)) as f64;
3568            Ok(Value::Scalar(v))
3569        }
3570        ("randi", 2) => {
3571            let (lo, hi) = randi_range(&args[0])?;
3572            let n = scalar_arg(&args[1], name, 2)? as usize;
3573            let data: Vec<f64> = (0..n * n)
3574                .map(|_| RNG.with(|r| r.borrow_mut().gen_range(lo..=hi)) as f64)
3575                .collect();
3576            Ok(Value::Matrix(Array2::from_shape_vec((n, n), data).unwrap()))
3577        }
3578        ("randi", 3) => {
3579            let (lo, hi) = randi_range(&args[0])?;
3580            let r = scalar_arg(&args[1], name, 2)? as usize;
3581            let c = scalar_arg(&args[2], name, 3)? as usize;
3582            let data: Vec<f64> = (0..r * c)
3583                .map(|_| RNG.with(|rng| rng.borrow_mut().gen_range(lo..=hi)) as f64)
3584                .collect();
3585            Ok(Value::Matrix(Array2::from_shape_vec((r, c), data).unwrap()))
3586        }
3587        ("rng", 1) => match &args[0] {
3588            Value::Scalar(n) => {
3589                rng_seed(*n as u64);
3590                Ok(Value::Void)
3591            }
3592            Value::Str(s) | Value::StringObj(s) if s == "shuffle" => {
3593                rng_shuffle();
3594                Ok(Value::Void)
3595            }
3596            _ => Err("rng: argument must be a numeric seed or 'shuffle'".to_string()),
3597        },
3598        // --- Vector reductions ---
3599        // For vectors (1×N or N×1): reduce all elements to scalar.
3600        // For M×N matrices (M>1, N>1): reduce column-wise, return 1×N row vector.
3601        ("sum", 1) => {
3602            if matches!(&args[0], Value::Complex(_, _) | Value::ComplexMatrix(_)) {
3603                apply_cm_reduction(&args[0], |v| v.iter().copied().sum())
3604            } else {
3605                apply_reduction(&args[0], |v| v.iter().copied().sum())
3606            }
3607        }
3608        ("prod", 1) => {
3609            if matches!(&args[0], Value::Complex(_, _) | Value::ComplexMatrix(_)) {
3610                apply_cm_reduction(&args[0], |v| v.iter().copied().product())
3611            } else {
3612                apply_reduction(&args[0], |v| v.iter().copied().product())
3613            }
3614        }
3615        ("any", 1) => apply_reduction(&args[0], |v| {
3616            if v.iter().any(|&x| x != 0.0) {
3617                1.0
3618            } else {
3619                0.0
3620            }
3621        }),
3622        ("all", 1) => apply_reduction(&args[0], |v| {
3623            if v.iter().all(|&x| x != 0.0) {
3624                1.0
3625            } else {
3626                0.0
3627            }
3628        }),
3629        ("mean", 1) => {
3630            if matches!(&args[0], Value::Complex(_, _) | Value::ComplexMatrix(_)) {
3631                apply_cm_reduction(&args[0], |v| {
3632                    if v.is_empty() {
3633                        Complex::new(f64::NAN, 0.0)
3634                    } else {
3635                        v.iter().copied().sum::<Complex<f64>>() / v.len() as f64
3636                    }
3637                })
3638            } else {
3639                apply_reduction(&args[0], |v| {
3640                    if v.is_empty() {
3641                        f64::NAN
3642                    } else {
3643                        v.iter().copied().sum::<f64>() / v.len() as f64
3644                    }
3645                })
3646            }
3647        }
3648        // 1-arg min/max: reduce to scalar for vectors, column-wise for matrices.
3649        // 2-arg forms (element-wise scalar min/max) are already handled above.
3650        ("min", 1) => apply_reduction(&args[0], |v| {
3651            v.iter().copied().fold(f64::INFINITY, f64::min)
3652        }),
3653        ("max", 1) => apply_reduction(&args[0], |v| {
3654            v.iter().copied().fold(f64::NEG_INFINITY, f64::max)
3655        }),
3656        // --- Norms ---
3657        ("norm", 1) => match &args[0] {
3658            Value::Void => Err("norm: not applicable to void".to_string()),
3659            Value::Scalar(n) => Ok(Value::Scalar(n.abs())),
3660            Value::Complex(re, im) => Ok(Value::Scalar((re * re + im * im).sqrt())),
3661            Value::Matrix(m) => {
3662                if m.nrows() <= 1 || m.ncols() <= 1 {
3663                    // Vector: L2 norm.
3664                    Ok(Value::Scalar(m.iter().map(|x| x * x).sum::<f64>().sqrt()))
3665                } else {
3666                    // Matrix: 2-norm = largest singular value.
3667                    let (_, s, _) = svd_compute(m)?;
3668                    Ok(Value::Scalar(s.first().copied().unwrap_or(0.0)))
3669                }
3670            }
3671            Value::ComplexMatrix(m) => Ok(Value::Scalar(
3672                m.iter().map(|c| c.norm_sqr()).sum::<f64>().sqrt(),
3673            )),
3674            Value::Str(_)
3675            | Value::StringObj(_)
3676            | Value::Lambda(_)
3677            | Value::Function { .. }
3678            | Value::Tuple(_)
3679            | Value::Cell(_)
3680            | Value::Struct(_)
3681            | Value::StructArray(_)
3682            | Value::DateTime(_)
3683            | Value::Duration(_)
3684            | Value::DateTimeArray(_)
3685            | Value::DurationArray(_) => {
3686                Err("norm: not applicable to non-numeric values".to_string())
3687            }
3688        },
3689        ("norm", 2) => match &args[1] {
3690            Value::Str(s) | Value::StringObj(s) => match s.as_str() {
3691                "fro" => match &args[0] {
3692                    Value::Scalar(n) => Ok(Value::Scalar(n.abs())),
3693                    Value::Matrix(m) => {
3694                        Ok(Value::Scalar(m.iter().map(|x| x * x).sum::<f64>().sqrt()))
3695                    }
3696                    _ => Err("norm: first argument must be numeric".to_string()),
3697                },
3698                other => Err(format!("norm: unknown norm type '{other}'")),
3699            },
3700            _ => {
3701                let p = scalar_arg(&args[1], name, 2)?;
3702                match &args[0] {
3703                    Value::Void => Err("norm: not applicable to void".to_string()),
3704                    Value::Scalar(n) => Ok(Value::Scalar(n.abs())),
3705                    Value::Complex(re, im) => Ok(Value::Scalar((re * re + im * im).sqrt().powf(p))),
3706                    Value::Matrix(m) => {
3707                        if m.nrows() > 1 && m.ncols() > 1 {
3708                            // Matrix norms.
3709                            if (p - 2.0).abs() < 1e-15 {
3710                                let (_, s, _) = svd_compute(m)?;
3711                                return Ok(Value::Scalar(s.first().copied().unwrap_or(0.0)));
3712                            } else if (p - 1.0).abs() < 1e-15 {
3713                                // Maximum absolute column sum.
3714                                let v = (0..m.ncols())
3715                                    .map(|j| m.column(j).iter().map(|&x| x.abs()).sum::<f64>())
3716                                    .fold(0.0_f64, f64::max);
3717                                return Ok(Value::Scalar(v));
3718                            } else if p == f64::INFINITY {
3719                                // Maximum absolute row sum.
3720                                let v = (0..m.nrows())
3721                                    .map(|i| m.row(i).iter().map(|&x| x.abs()).sum::<f64>())
3722                                    .fold(0.0_f64, f64::max);
3723                                return Ok(Value::Scalar(v));
3724                            }
3725                        }
3726                        // Vector (or general Lp).
3727                        if p == f64::INFINITY {
3728                            Ok(Value::Scalar(
3729                                m.iter().copied().fold(0.0_f64, |acc, x| acc.max(x.abs())),
3730                            ))
3731                        } else {
3732                            Ok(Value::Scalar(
3733                                m.iter().map(|x| x.abs().powf(p)).sum::<f64>().powf(1.0 / p),
3734                            ))
3735                        }
3736                    }
3737                    Value::ComplexMatrix(m) => Ok(Value::Scalar(
3738                        m.iter().map(|c| c.norm_sqr()).sum::<f64>().sqrt().powf(p),
3739                    )),
3740                    Value::Str(_)
3741                    | Value::StringObj(_)
3742                    | Value::Lambda(_)
3743                    | Value::Function { .. }
3744                    | Value::Tuple(_)
3745                    | Value::Cell(_)
3746                    | Value::Struct(_)
3747                    | Value::StructArray(_)
3748                    | Value::DateTime(_)
3749                    | Value::Duration(_)
3750                    | Value::DateTimeArray(_)
3751                    | Value::DurationArray(_) => {
3752                        Err("norm: not applicable to non-numeric values".to_string())
3753                    }
3754                }
3755            }
3756        },
3757        // --- Cumulative reductions ---
3758        ("cumsum", 1) => apply_cumulative(&args[0], |acc, x| acc + x),
3759        ("cumprod", 1) => apply_cumulative(&args[0], |acc, x| acc * x),
3760        // --- Sort ---
3761        ("sort", 1) => match &args[0] {
3762            Value::Void => Err("sort: not applicable to void".to_string()),
3763            Value::Scalar(n) => Ok(Value::Scalar(*n)),
3764            Value::Complex(_, _) => Err("sort: not applicable to complex values".to_string()),
3765            Value::ComplexMatrix(_) => Err("sort: not applicable to complex values".to_string()),
3766            Value::Str(_)
3767            | Value::StringObj(_)
3768            | Value::Lambda(_)
3769            | Value::Function { .. }
3770            | Value::Tuple(_)
3771            | Value::Cell(_)
3772            | Value::Struct(_)
3773            | Value::StructArray(_)
3774            | Value::DateTime(_)
3775            | Value::Duration(_)
3776            | Value::DateTimeArray(_)
3777            | Value::DurationArray(_) => {
3778                Err("sort: not applicable to non-numeric values".to_string())
3779            }
3780            Value::Matrix(m) => {
3781                if m.nrows() > 1 && m.ncols() > 1 {
3782                    return Err("sort: input must be a vector".to_string());
3783                }
3784                let mut vals: Vec<f64> = m.iter().copied().collect();
3785                vals.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
3786                Ok(Value::Matrix(
3787                    Array2::from_shape_vec(m.raw_dim(), vals).unwrap(),
3788                ))
3789            }
3790        },
3791        // --- Reshape ---
3792        ("reshape", 3) => {
3793            let r = scalar_arg(&args[1], name, 2)? as usize;
3794            let c = scalar_arg(&args[2], name, 3)? as usize;
3795            match &args[0] {
3796                Value::Void => Err("reshape: not applicable to void".to_string()),
3797                Value::Scalar(n) => {
3798                    if r * c != 1 {
3799                        return Err(format!("reshape: cannot reshape 1 element into {r}x{c}"));
3800                    }
3801                    Ok(Value::Matrix(
3802                        Array2::from_shape_vec((1, 1), vec![*n]).unwrap(),
3803                    ))
3804                }
3805                Value::Complex(_, _) => {
3806                    Err("reshape: not applicable to complex values".to_string())
3807                }
3808                Value::ComplexMatrix(_) => {
3809                    Err("reshape: not supported for complex matrices".to_string())
3810                }
3811                Value::Str(_)
3812                | Value::StringObj(_)
3813                | Value::Lambda(_)
3814                | Value::Function { .. }
3815                | Value::Tuple(_)
3816                | Value::Cell(_)
3817                | Value::Struct(_)
3818                | Value::StructArray(_)
3819                | Value::DateTime(_)
3820                | Value::Duration(_)
3821                | Value::DateTimeArray(_)
3822                | Value::DurationArray(_) => {
3823                    Err("reshape: not applicable to non-numeric values".to_string())
3824                }
3825                Value::Matrix(m) => {
3826                    let total = m.len();
3827                    if r * c != total {
3828                        return Err(format!(
3829                            "reshape: cannot reshape {total} elements into {r}x{c}"
3830                        ));
3831                    }
3832                    // Column-major order (MATLAB convention)
3833                    let flat: Vec<f64> = (0..m.ncols())
3834                        .flat_map(|col| (0..m.nrows()).map(move |row| m[[row, col]]))
3835                        .collect();
3836                    let mut result = Array2::<f64>::zeros((r, c));
3837                    for (i, &v) in flat.iter().enumerate() {
3838                        result[[i % r, i / r]] = v;
3839                    }
3840                    Ok(Value::Matrix(result))
3841                }
3842            }
3843        }
3844        // --- Flip ---
3845        ("fliplr", 1) => match &args[0] {
3846            Value::Void => Err(format!("{name}: not applicable to void")),
3847            Value::Scalar(n) => Ok(Value::Scalar(*n)),
3848            Value::Complex(re, im) => Ok(Value::Complex(*re, *im)),
3849            Value::ComplexMatrix(_) => Err(format!("{name}: not supported for complex matrices")),
3850            Value::Str(_)
3851            | Value::StringObj(_)
3852            | Value::Lambda(_)
3853            | Value::Function { .. }
3854            | Value::Tuple(_)
3855            | Value::Cell(_)
3856            | Value::Struct(_)
3857            | Value::StructArray(_)
3858            | Value::DateTime(_)
3859            | Value::Duration(_)
3860            | Value::DateTimeArray(_)
3861            | Value::DurationArray(_) => {
3862                Err(format!("{name}: not applicable to non-numeric values"))
3863            }
3864            Value::Matrix(m) => {
3865                let (nrows, ncols) = (m.nrows(), m.ncols());
3866                let mut result = m.clone();
3867                for r in 0..nrows {
3868                    for c in 0..ncols / 2 {
3869                        let tmp = result[[r, c]];
3870                        result[[r, c]] = result[[r, ncols - 1 - c]];
3871                        result[[r, ncols - 1 - c]] = tmp;
3872                    }
3873                }
3874                Ok(Value::Matrix(result))
3875            }
3876        },
3877        ("flipud", 1) => match &args[0] {
3878            Value::Void => Err(format!("{name}: not applicable to void")),
3879            Value::Scalar(n) => Ok(Value::Scalar(*n)),
3880            Value::Complex(re, im) => Ok(Value::Complex(*re, *im)),
3881            Value::ComplexMatrix(_) => Err(format!("{name}: not supported for complex matrices")),
3882            Value::Str(_)
3883            | Value::StringObj(_)
3884            | Value::Lambda(_)
3885            | Value::Function { .. }
3886            | Value::Tuple(_)
3887            | Value::Cell(_)
3888            | Value::Struct(_)
3889            | Value::StructArray(_)
3890            | Value::DateTime(_)
3891            | Value::Duration(_)
3892            | Value::DateTimeArray(_)
3893            | Value::DurationArray(_) => {
3894                Err(format!("{name}: not applicable to non-numeric values"))
3895            }
3896            Value::Matrix(m) => {
3897                let (nrows, ncols) = (m.nrows(), m.ncols());
3898                let mut result = m.clone();
3899                for c in 0..ncols {
3900                    for r in 0..nrows / 2 {
3901                        let tmp = result[[r, c]];
3902                        result[[r, c]] = result[[nrows - 1 - r, c]];
3903                        result[[nrows - 1 - r, c]] = tmp;
3904                    }
3905                }
3906                Ok(Value::Matrix(result))
3907            }
3908        },
3909        // --- Find ---
3910        ("find", 1) => find_nonzero(&args[0], usize::MAX),
3911        ("find", 2) => {
3912            let k = scalar_arg(&args[1], name, 2)?;
3913            if k < 0.0 {
3914                return Err("find: k must be non-negative".to_string());
3915            }
3916            find_nonzero(&args[0], k as usize)
3917        }
3918        // --- Unique ---
3919        ("unique", 1) => match &args[0] {
3920            Value::Void => Err("unique: not applicable to void".to_string()),
3921            Value::Scalar(n) => Ok(Value::Scalar(*n)),
3922            Value::Matrix(m) => {
3923                let mut vals: Vec<f64> = m.iter().copied().collect();
3924                vals.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
3925                let mut unique: Vec<f64> = Vec::new();
3926                for v in vals {
3927                    if unique.last().is_none_or(|&last| last != v) {
3928                        unique.push(v);
3929                    }
3930                }
3931                let n = unique.len();
3932                Ok(Value::Matrix(
3933                    Array2::from_shape_vec((1, n), unique).unwrap(),
3934                ))
3935            }
3936            Value::Complex(_, _) => Err("unique: not applicable to complex values".to_string()),
3937            Value::ComplexMatrix(_) => Err("unique: not applicable to complex values".to_string()),
3938            Value::Str(_)
3939            | Value::StringObj(_)
3940            | Value::Lambda(_)
3941            | Value::Function { .. }
3942            | Value::Tuple(_)
3943            | Value::Cell(_)
3944            | Value::Struct(_)
3945            | Value::StructArray(_)
3946            | Value::DateTime(_)
3947            | Value::Duration(_)
3948            | Value::DateTimeArray(_)
3949            | Value::DurationArray(_) => {
3950                Err("unique: not applicable to non-numeric values".to_string())
3951            }
3952        },
3953        // --- Descriptive statistics ---
3954        ("std", 1) => apply_stat(&args[0], |s| stat_var_vec(s, false).sqrt(), "std"),
3955        ("std", 2) => {
3956            let w = scalar_arg(&args[1], name, 2)?;
3957            let population = w != 0.0;
3958            apply_stat(&args[0], |s| stat_var_vec(s, population).sqrt(), "std")
3959        }
3960        ("var", 1) => apply_stat(&args[0], |s| stat_var_vec(s, false), "var"),
3961        ("var", 2) => {
3962            let w = scalar_arg(&args[1], name, 2)?;
3963            let population = w != 0.0;
3964            apply_stat(&args[0], |s| stat_var_vec(s, population), "var")
3965        }
3966        ("cov", 1) => match &args[0] {
3967            Value::Scalar(_) => Ok(Value::Scalar(0.0)),
3968            Value::Matrix(m) => {
3969                if m.nrows() == 1 || m.ncols() == 1 {
3970                    let vals: Vec<f64> = m.iter().copied().collect();
3971                    Ok(Value::Scalar(stat_var_vec(&vals, false)))
3972                } else {
3973                    let (nobs, nvars) = (m.nrows(), m.ncols());
3974                    if nobs < 2 {
3975                        return Err("cov: need at least 2 observations".to_string());
3976                    }
3977                    let mut centered = m.clone();
3978                    for c in 0..nvars {
3979                        let col_mean: f64 = m.column(c).iter().sum::<f64>() / nobs as f64;
3980                        for r in 0..nobs {
3981                            centered[[r, c]] -= col_mean;
3982                        }
3983                    }
3984                    let denom = (nobs - 1) as f64;
3985                    let mut cov_mat = Array2::<f64>::zeros((nvars, nvars));
3986                    for i in 0..nvars {
3987                        for j in 0..nvars {
3988                            let dot: f64 =
3989                                (0..nobs).map(|r| centered[[r, i]] * centered[[r, j]]).sum();
3990                            cov_mat[[i, j]] = dot / denom;
3991                        }
3992                    }
3993                    Ok(Value::Matrix(cov_mat))
3994                }
3995            }
3996            _ => Err("cov: argument must be numeric".to_string()),
3997        },
3998        ("median", 1) => apply_stat(
3999            &args[0],
4000            |s| {
4001                if s.is_empty() {
4002                    return f64::NAN;
4003                }
4004                let mut v = s.to_vec();
4005                v.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
4006                let n = v.len();
4007                if n % 2 == 0 {
4008                    (v[n / 2 - 1] + v[n / 2]) / 2.0
4009                } else {
4010                    v[n / 2]
4011                }
4012            },
4013            "median",
4014        ),
4015        ("mode", 1) => apply_stat(
4016            &args[0],
4017            |s| {
4018                if s.is_empty() {
4019                    return f64::NAN;
4020                }
4021                let mut counts: std::collections::HashMap<u64, usize> =
4022                    std::collections::HashMap::new();
4023                for &x in s {
4024                    *counts.entry(x.to_bits()).or_insert(0) += 1;
4025                }
4026                let max_count = counts.values().copied().max().unwrap_or(0);
4027                let mut candidates: Vec<f64> = counts
4028                    .iter()
4029                    .filter(|&(_, &c)| c == max_count)
4030                    .map(|(&bits, _)| f64::from_bits(bits))
4031                    .collect();
4032                candidates.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
4033                candidates[0]
4034            },
4035            "mode",
4036        ),
4037        ("skewness", 1) => apply_stat(
4038            &args[0],
4039            |s| {
4040                let n = s.len();
4041                if n == 0 {
4042                    return f64::NAN;
4043                }
4044                if n == 1 {
4045                    return 0.0;
4046                }
4047                let mean = s.iter().sum::<f64>() / n as f64;
4048                let m2 = s.iter().map(|&x| (x - mean).powi(2)).sum::<f64>() / n as f64;
4049                if m2 == 0.0 {
4050                    return f64::NAN;
4051                }
4052                let m3 = s.iter().map(|&x| (x - mean).powi(3)).sum::<f64>() / n as f64;
4053                m3 / m2.powf(1.5)
4054            },
4055            "skewness",
4056        ),
4057        ("kurtosis", 1) => apply_stat(
4058            &args[0],
4059            |s| {
4060                let n = s.len();
4061                if n < 2 {
4062                    return f64::NAN;
4063                }
4064                let mean = s.iter().sum::<f64>() / n as f64;
4065                let m2 = s.iter().map(|&x| (x - mean).powi(2)).sum::<f64>() / n as f64;
4066                if m2 == 0.0 {
4067                    return f64::NAN;
4068                }
4069                let m4 = s.iter().map(|&x| (x - mean).powi(4)).sum::<f64>() / n as f64;
4070                m4 / m2.powi(2)
4071            },
4072            "kurtosis",
4073        ),
4074        ("histc", 2) => {
4075            let vals = numeric_vec(&args[0], name)?;
4076            let edges = numeric_vec(&args[1], name)?;
4077            if edges.is_empty() {
4078                return Err("histc: edges must not be empty".to_string());
4079            }
4080            let n_edges = edges.len();
4081            let mut counts = vec![0.0f64; n_edges];
4082            for &v in &vals {
4083                // Linear scan — fine for typical edge counts
4084                let last = n_edges - 1;
4085                if v == edges[last] {
4086                    counts[last] += 1.0;
4087                } else {
4088                    for i in 0..last {
4089                        if v >= edges[i] && v < edges[i + 1] {
4090                            counts[i] += 1.0;
4091                            break;
4092                        }
4093                    }
4094                }
4095            }
4096            Ok(Value::Matrix(
4097                Array2::from_shape_vec((1, n_edges), counts).unwrap(),
4098            ))
4099        }
4100        // --- Percentiles and distributions ---
4101        ("prctile", 2) => {
4102            let p_vals = numeric_vec(&args[1], name)?;
4103            let n_p = p_vals.len();
4104
4105            // Sort one column of floats and compute all requested percentiles.
4106            let compute_col = |vals: &[f64]| -> Vec<f64> {
4107                let mut s = vals.to_vec();
4108                s.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
4109                p_vals.iter().map(|&p| percentile_sorted(&s, p)).collect()
4110            };
4111
4112            match &args[0] {
4113                Value::Scalar(n) => {
4114                    let pr = compute_col(&[*n]);
4115                    if n_p == 1 {
4116                        Ok(Value::Scalar(pr[0]))
4117                    } else {
4118                        Ok(Value::Matrix(Array2::from_shape_vec((1, n_p), pr).unwrap()))
4119                    }
4120                }
4121                Value::Matrix(m) if m.nrows() == 1 || m.ncols() == 1 => {
4122                    let vals: Vec<f64> = m.iter().copied().collect();
4123                    let pr = compute_col(&vals);
4124                    if n_p == 1 {
4125                        Ok(Value::Scalar(pr[0]))
4126                    } else {
4127                        Ok(Value::Matrix(Array2::from_shape_vec((1, n_p), pr).unwrap()))
4128                    }
4129                }
4130                Value::Matrix(m) => {
4131                    // M×N matrix: column-wise → n_p × ncols result
4132                    let ncols = m.ncols();
4133                    let mut result = Array2::<f64>::zeros((n_p, ncols));
4134                    for j in 0..ncols {
4135                        let col: Vec<f64> = m.column(j).iter().copied().collect();
4136                        let pr = compute_col(&col);
4137                        for (i, &v) in pr.iter().enumerate() {
4138                            result[[i, j]] = v;
4139                        }
4140                    }
4141                    if n_p == 1 {
4142                        let row: Vec<f64> = result.row(0).iter().copied().collect();
4143                        Ok(Value::Matrix(
4144                            Array2::from_shape_vec((1, ncols), row).unwrap(),
4145                        ))
4146                    } else {
4147                        Ok(Value::Matrix(result))
4148                    }
4149                }
4150                _ => Err("prctile: first argument must be numeric".to_string()),
4151            }
4152        }
4153        ("iqr", 1) => apply_stat(
4154            &args[0],
4155            |s| {
4156                let mut sorted = s.to_vec();
4157                sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
4158                percentile_sorted(&sorted, 75.0) - percentile_sorted(&sorted, 25.0)
4159            },
4160            "iqr",
4161        ),
4162        ("zscore", 1) => match &args[0] {
4163            Value::Scalar(_) => Ok(Value::Scalar(0.0)),
4164            Value::Matrix(m) => {
4165                if m.nrows() == 1 || m.ncols() == 1 {
4166                    let vals: Vec<f64> = m.iter().copied().collect();
4167                    let n = vals.len() as f64;
4168                    let mean = vals.iter().sum::<f64>() / n;
4169                    let s = stat_var_vec(&vals, false).sqrt();
4170                    let result: Vec<f64> = vals
4171                        .iter()
4172                        .map(|&x| if s == 0.0 { 0.0 } else { (x - mean) / s })
4173                        .collect();
4174                    Ok(Value::Matrix(
4175                        Array2::from_shape_vec(m.raw_dim(), result).unwrap(),
4176                    ))
4177                } else {
4178                    let (nrows, ncols) = (m.nrows(), m.ncols());
4179                    let mut result = m.clone();
4180                    for j in 0..ncols {
4181                        let col: Vec<f64> = m.column(j).iter().copied().collect();
4182                        let mean = col.iter().sum::<f64>() / col.len() as f64;
4183                        let s = stat_var_vec(&col, false).sqrt();
4184                        for i in 0..nrows {
4185                            result[[i, j]] = if s == 0.0 {
4186                                0.0
4187                            } else {
4188                                (m[[i, j]] - mean) / s
4189                            };
4190                        }
4191                    }
4192                    Ok(Value::Matrix(result))
4193                }
4194            }
4195            _ => Err("zscore: argument must be numeric".to_string()),
4196        },
4197        // diag(v) — vector → diagonal matrix; diag(A) → column vector of main diagonal.
4198        ("diag", 1) => match &args[0] {
4199            Value::Scalar(n) => Ok(Value::Matrix(Array2::from_elem((1, 1), *n))),
4200            Value::Matrix(m) => {
4201                let (rows, cols) = (m.nrows(), m.ncols());
4202                if rows == 1 || cols == 1 {
4203                    // vector → N×N diagonal matrix
4204                    let v: Vec<f64> = m.iter().copied().collect();
4205                    let n = v.len();
4206                    let mut result = Array2::<f64>::zeros((n, n));
4207                    for (i, &val) in v.iter().enumerate() {
4208                        result[[i, i]] = val;
4209                    }
4210                    Ok(Value::Matrix(result))
4211                } else {
4212                    // matrix → extract main diagonal as N×1 column vector
4213                    let n = rows.min(cols);
4214                    let d: Vec<f64> = (0..n).map(|i| m[[i, i]]).collect();
4215                    Ok(Value::Matrix(Array2::from_shape_vec((n, 1), d).unwrap()))
4216                }
4217            }
4218            Value::Void => Err("diag: not applicable to void".to_string()),
4219            Value::Complex(re, im) => {
4220                let mut result = Array2::<Complex<f64>>::zeros((1, 1));
4221                result[[0, 0]] = Complex::new(*re, *im);
4222                Ok(Value::ComplexMatrix(result))
4223            }
4224            Value::ComplexMatrix(m) => {
4225                let (rows, cols) = (m.nrows(), m.ncols());
4226                if rows == 1 || cols == 1 {
4227                    let v: Vec<Complex<f64>> = m.iter().copied().collect();
4228                    let n = v.len();
4229                    let mut result = Array2::<Complex<f64>>::zeros((n, n));
4230                    for (i, &val) in v.iter().enumerate() {
4231                        result[[i, i]] = val;
4232                    }
4233                    Ok(Value::ComplexMatrix(result))
4234                } else {
4235                    let n = rows.min(cols);
4236                    let d: Vec<Complex<f64>> = (0..n).map(|i| m[[i, i]]).collect();
4237                    Ok(Value::ComplexMatrix(
4238                        Array2::from_shape_vec((n, 1), d).unwrap(),
4239                    ))
4240                }
4241            }
4242            Value::Str(_)
4243            | Value::StringObj(_)
4244            | Value::Lambda(_)
4245            | Value::Function { .. }
4246            | Value::Tuple(_)
4247            | Value::Cell(_)
4248            | Value::Struct(_)
4249            | Value::StructArray(_)
4250            | Value::DateTime(_)
4251            | Value::Duration(_)
4252            | Value::DateTimeArray(_)
4253            | Value::DurationArray(_) => {
4254                Err("diag: not applicable to non-numeric values".to_string())
4255            }
4256        },
4257
4258        // --- Complex built-ins ---
4259        // real(z) — real part; works on scalars, matrices, and complex matrices.
4260        ("real", 1) => match &args[0] {
4261            Value::Void => Err("real: not applicable to void".to_string()),
4262            Value::Scalar(n) => Ok(Value::Scalar(*n)),
4263            Value::Complex(re, _) => Ok(Value::Scalar(*re)),
4264            Value::Matrix(m) => Ok(Value::Matrix(m.clone())),
4265            Value::ComplexMatrix(m) => Ok(Value::Matrix(m.mapv(|c| c.re))),
4266            Value::Str(_)
4267            | Value::StringObj(_)
4268            | Value::Lambda(_)
4269            | Value::Function { .. }
4270            | Value::Tuple(_)
4271            | Value::Cell(_)
4272            | Value::Struct(_)
4273            | Value::StructArray(_)
4274            | Value::DateTime(_)
4275            | Value::Duration(_)
4276            | Value::DateTimeArray(_)
4277            | Value::DurationArray(_) => {
4278                Err("real: not applicable to non-numeric values".to_string())
4279            }
4280        },
4281        // imag(z) — imaginary part; returns 0.0 for real scalars and real matrices.
4282        ("imag", 1) => match &args[0] {
4283            Value::Void => Err("imag: not applicable to void".to_string()),
4284            Value::Scalar(_) => Ok(Value::Scalar(0.0)),
4285            Value::Complex(_, im) => Ok(Value::Scalar(*im)),
4286            Value::Matrix(m) => Ok(Value::Matrix(Array2::zeros(m.raw_dim()))),
4287            Value::ComplexMatrix(m) => Ok(Value::Matrix(m.mapv(|c| c.im))),
4288            Value::Str(_)
4289            | Value::StringObj(_)
4290            | Value::Lambda(_)
4291            | Value::Function { .. }
4292            | Value::Tuple(_)
4293            | Value::Cell(_)
4294            | Value::Struct(_)
4295            | Value::StructArray(_)
4296            | Value::DateTime(_)
4297            | Value::Duration(_)
4298            | Value::DateTimeArray(_)
4299            | Value::DurationArray(_) => {
4300                Err("imag: not applicable to non-numeric values".to_string())
4301            }
4302        },
4303        // abs(z) — modulus; overloads scalar abs; element-wise on matrices.
4304        ("abs", 1) => match &args[0] {
4305            Value::Void => Err("abs: not applicable to void".to_string()),
4306            Value::Scalar(n) => Ok(Value::Scalar(n.abs())),
4307            Value::Complex(re, im) => Ok(Value::Scalar((re * re + im * im).sqrt())),
4308            Value::Matrix(m) => Ok(Value::Matrix(m.mapv(|x| x.abs()))),
4309            Value::ComplexMatrix(m) => Ok(Value::Matrix(m.mapv(|c| c.norm()))),
4310            Value::Str(_)
4311            | Value::StringObj(_)
4312            | Value::Lambda(_)
4313            | Value::Function { .. }
4314            | Value::Tuple(_)
4315            | Value::Cell(_)
4316            | Value::Struct(_)
4317            | Value::StructArray(_)
4318            | Value::DateTime(_)
4319            | Value::Duration(_)
4320            | Value::DateTimeArray(_)
4321            | Value::DurationArray(_) => {
4322                Err("abs: not applicable to non-numeric values".to_string())
4323            }
4324        },
4325        // angle(z) — argument in radians; returns 0 for non-negative reals.
4326        ("angle", 1) => match &args[0] {
4327            Value::Void => Err("angle: not applicable to void".to_string()),
4328            Value::Scalar(n) => Ok(Value::Scalar(if *n >= 0.0 {
4329                0.0
4330            } else {
4331                std::f64::consts::PI
4332            })),
4333            Value::Complex(re, im) => Ok(Value::Scalar(im.atan2(*re))),
4334            Value::Matrix(m) => {
4335                Ok(Value::Matrix(m.mapv(|x| {
4336                    if x >= 0.0 { 0.0 } else { std::f64::consts::PI }
4337                })))
4338            }
4339            Value::ComplexMatrix(m) => Ok(Value::Matrix(m.mapv(|c| c.im.atan2(c.re)))),
4340            Value::Str(_)
4341            | Value::StringObj(_)
4342            | Value::Lambda(_)
4343            | Value::Function { .. }
4344            | Value::Tuple(_)
4345            | Value::Cell(_)
4346            | Value::Struct(_)
4347            | Value::StructArray(_)
4348            | Value::DateTime(_)
4349            | Value::Duration(_)
4350            | Value::DateTimeArray(_)
4351            | Value::DurationArray(_) => {
4352                Err("angle: not applicable to non-numeric values".to_string())
4353            }
4354        },
4355        // conj(z) — complex conjugate; element-wise over complex matrices.
4356        ("conj", 1) => match &args[0] {
4357            Value::Void => Err("conj: not applicable to void".to_string()),
4358            Value::Scalar(n) => Ok(Value::Scalar(*n)),
4359            Value::Complex(re, im) => Ok(make_complex(*re, -*im)),
4360            Value::Matrix(m) => Ok(Value::Matrix(m.clone())),
4361            Value::ComplexMatrix(m) => Ok(Value::ComplexMatrix(m.mapv(|c| c.conj()))),
4362            Value::Str(_)
4363            | Value::StringObj(_)
4364            | Value::Lambda(_)
4365            | Value::Function { .. }
4366            | Value::Tuple(_)
4367            | Value::Cell(_)
4368            | Value::Struct(_)
4369            | Value::StructArray(_)
4370            | Value::DateTime(_)
4371            | Value::Duration(_)
4372            | Value::DateTimeArray(_)
4373            | Value::DurationArray(_) => {
4374                Err("conj: not applicable to non-numeric values".to_string())
4375            }
4376        },
4377        // complex(re, im) — construct complex from two reals.
4378        ("complex", 2) => {
4379            let re = scalar_arg(&args[0], name, 1)?;
4380            let im = scalar_arg(&args[1], name, 2)?;
4381            Ok(make_complex(re, im))
4382        }
4383        // isreal(z) — 1.0 if imaginary part is zero, 0.0 otherwise.
4384        ("isreal", 1) => match &args[0] {
4385            Value::Void => Ok(Value::Scalar(0.0)),
4386            Value::Scalar(_) => Ok(Value::Scalar(1.0)),
4387            Value::Complex(_, im) => Ok(Value::Scalar(if *im == 0.0 { 1.0 } else { 0.0 })),
4388            Value::Matrix(_) => Ok(Value::Scalar(1.0)),
4389            Value::ComplexMatrix(_) => Ok(Value::Scalar(0.0)),
4390            // Strings are not real numbers; functions are not numbers
4391            Value::Str(_) | Value::StringObj(_) => Ok(Value::Scalar(0.0)),
4392            Value::Lambda(_)
4393            | Value::Function { .. }
4394            | Value::Tuple(_)
4395            | Value::Cell(_)
4396            | Value::Struct(_)
4397            | Value::StructArray(_)
4398            | Value::DateTime(_)
4399            | Value::Duration(_)
4400            | Value::DateTimeArray(_)
4401            | Value::DurationArray(_) => Ok(Value::Scalar(0.0)),
4402        },
4403        // --- String built-ins ---
4404        // num2str(x) — convert number to char array string
4405        ("num2str", 1) => match &args[0] {
4406            Value::Void => Err("num2str: not applicable to void".to_string()),
4407            Value::Str(s) => Ok(Value::Str(s.clone())),
4408            Value::StringObj(s) => Ok(Value::Str(s.clone())),
4409            Value::Scalar(n) => Ok(Value::Str(fmt_auto_sig(*n, 5))),
4410            Value::Complex(re, im) => Ok(Value::Str(format_complex(*re, *im, &FormatMode::Short))),
4411            Value::Matrix(m) => {
4412                let s = m
4413                    .iter()
4414                    .map(|x| fmt_auto_sig(*x, 5))
4415                    .collect::<Vec<_>>()
4416                    .join("  ");
4417                Ok(Value::Str(s))
4418            }
4419            Value::ComplexMatrix(_) => {
4420                Err("num2str: not supported for complex matrices".to_string())
4421            }
4422            Value::Lambda(_)
4423            | Value::Function { .. }
4424            | Value::Tuple(_)
4425            | Value::Cell(_)
4426            | Value::Struct(_)
4427            | Value::StructArray(_)
4428            | Value::DateTime(_)
4429            | Value::Duration(_)
4430            | Value::DateTimeArray(_)
4431            | Value::DurationArray(_) => Err("num2str: not applicable to this type".to_string()),
4432        },
4433        // num2str(x, N) — N significant digits
4434        ("num2str", 2) => {
4435            let n = scalar_arg(&args[1], name, 2)? as usize;
4436            match &args[0] {
4437                Value::Void => Err("num2str: not applicable to void".to_string()),
4438                Value::Str(s) => Ok(Value::Str(s.clone())),
4439                Value::StringObj(s) => Ok(Value::Str(s.clone())),
4440                Value::Scalar(v) => Ok(Value::Str(fmt_auto_sig(*v, n))),
4441                Value::Complex(re, im) => {
4442                    Ok(Value::Str(format_complex(*re, *im, &FormatMode::Custom(n))))
4443                }
4444                Value::Matrix(m) => {
4445                    let s = m
4446                        .iter()
4447                        .map(|x| fmt_auto_sig(*x, n))
4448                        .collect::<Vec<_>>()
4449                        .join("  ");
4450                    Ok(Value::Str(s))
4451                }
4452                Value::ComplexMatrix(_) => {
4453                    Err("num2str: not supported for complex matrices".to_string())
4454                }
4455                Value::Lambda(_)
4456                | Value::Function { .. }
4457                | Value::Tuple(_)
4458                | Value::Cell(_)
4459                | Value::Struct(_)
4460                | Value::StructArray(_)
4461                | Value::DateTime(_)
4462                | Value::Duration(_)
4463                | Value::DateTimeArray(_)
4464                | Value::DurationArray(_) => {
4465                    Err("num2str: not applicable to this type".to_string())
4466                }
4467            }
4468        }
4469        // str2double(s) — parse string as f64; return NaN on failure
4470        ("str2double", 1) => {
4471            let s = string_arg(&args[0], name, 1)?;
4472            match s.trim().parse::<f64>() {
4473                Ok(n) => Ok(Value::Scalar(n)),
4474                Err(_) => Ok(Value::Scalar(f64::NAN)),
4475            }
4476        }
4477        // str2num(s) — parse string as f64; return error on failure
4478        ("str2num", 1) => {
4479            let s = string_arg(&args[0], name, 1)?;
4480            s.trim()
4481                .parse::<f64>()
4482                .map(Value::Scalar)
4483                .map_err(|_| format!("str2num: cannot convert '{}' to number", s.trim()))
4484        }
4485        // strcat(a, b, ...) — concatenate strings
4486        ("strcat", n) if n >= 2 => {
4487            let mut result = String::new();
4488            let mut any_obj = false;
4489            for (i, arg) in args.iter().enumerate() {
4490                match arg {
4491                    Value::Str(s) => result.push_str(s.trim_end()),
4492                    Value::StringObj(s) => {
4493                        result.push_str(s);
4494                        any_obj = true;
4495                    }
4496                    _ => return Err(format!("strcat: argument {} must be a string", i + 1)),
4497                }
4498            }
4499            if any_obj {
4500                Ok(Value::StringObj(result))
4501            } else {
4502                Ok(Value::Str(result))
4503            }
4504        }
4505        // ischar(s) — 1.0 if char array, 0.0 otherwise
4506        ("ischar", 1) => Ok(Value::Scalar(if matches!(&args[0], Value::Str(_)) {
4507            1.0
4508        } else {
4509            0.0
4510        })),
4511        // isstring(s) — 1.0 if string object, 0.0 otherwise
4512        ("isstring", 1) => Ok(Value::Scalar(if matches!(&args[0], Value::StringObj(_)) {
4513            1.0
4514        } else {
4515            0.0
4516        })),
4517        // --- Struct built-ins ---
4518        // struct('k1',v1,'k2',v2,...) — construct a scalar struct from name-value pairs
4519        ("struct", _) => {
4520            if !args.len().is_multiple_of(2) {
4521                return Err(
4522                    "struct: requires an even number of arguments (name, value, ...)".to_string(),
4523                );
4524            }
4525            let mut map = IndexMap::new();
4526            for pair in args.chunks(2) {
4527                let key = match &pair[0] {
4528                    Value::Str(s) | Value::StringObj(s) => s.clone(),
4529                    _ => return Err("struct: field names must be strings".to_string()),
4530                };
4531                map.insert(key, pair[1].clone());
4532            }
4533            Ok(Value::Struct(map))
4534        }
4535        // fieldnames(s) — cell array of field names in insertion order
4536        ("fieldnames", 1) => match &args[0] {
4537            Value::Struct(map) => {
4538                let names: Vec<Value> = map.keys().map(|k| Value::Str(k.clone())).collect();
4539                Ok(Value::Cell(names))
4540            }
4541            Value::StructArray(arr) => {
4542                // Use field names from first element
4543                let names: Vec<Value> = arr
4544                    .first()
4545                    .map(|m| m.keys().map(|k| Value::Str(k.clone())).collect())
4546                    .unwrap_or_default();
4547                Ok(Value::Cell(names))
4548            }
4549            _ => Err("fieldnames: argument must be a struct".to_string()),
4550        },
4551        // isfield(s, 'name') — 1.0 if field exists, 0.0 otherwise
4552        ("isfield", 2) => {
4553            let field = match &args[1] {
4554                Value::Str(s) | Value::StringObj(s) => s.clone(),
4555                _ => return Err("isfield: second argument must be a string".to_string()),
4556            };
4557            Ok(Value::Scalar(match &args[0] {
4558                Value::Struct(map) if map.contains_key(&field) => 1.0,
4559                Value::StructArray(arr) if arr.first().is_some_and(|m| m.contains_key(&field)) => {
4560                    1.0
4561                }
4562                _ => 0.0,
4563            }))
4564        }
4565        // rmfield(s, 'name') — copy of struct with field removed
4566        ("rmfield", 2) => {
4567            let field = match &args[1] {
4568                Value::Str(s) | Value::StringObj(s) => s.clone(),
4569                _ => return Err("rmfield: second argument must be a string".to_string()),
4570            };
4571            match &args[0] {
4572                Value::Struct(map) => {
4573                    if !map.contains_key(&field) {
4574                        return Err(format!("rmfield: field '{field}' does not exist"));
4575                    }
4576                    let mut updated = map.clone();
4577                    updated.shift_remove(&field);
4578                    Ok(Value::Struct(updated))
4579                }
4580                Value::StructArray(arr) => {
4581                    let updated: Result<Vec<_>, _> = arr
4582                        .iter()
4583                        .map(|m| {
4584                            if !m.contains_key(&field) {
4585                                return Err(format!("rmfield: field '{field}' does not exist"));
4586                            }
4587                            let mut m2 = m.clone();
4588                            m2.shift_remove(&field);
4589                            Ok(m2)
4590                        })
4591                        .collect();
4592                    Ok(Value::StructArray(updated?))
4593                }
4594                _ => Err("rmfield: first argument must be a struct".to_string()),
4595            }
4596        }
4597        // isstruct(v) — 1.0 if v is a struct or struct array, 0.0 otherwise
4598        ("isstruct", 1) => Ok(Value::Scalar(
4599            if matches!(&args[0], Value::Struct(_) | Value::StructArray(_)) {
4600                1.0
4601            } else {
4602                0.0
4603            },
4604        )),
4605        // --- Cell array built-ins ---
4606        // isempty(v) — 1.0 if v has no elements, 0.0 otherwise.
4607        // Matches MATLAB: empty matrix, empty string, empty cell, or Void are empty.
4608        ("isempty", 1) => {
4609            let empty = match &args[0] {
4610                Value::Matrix(m) => m.is_empty(),
4611                Value::Str(s) | Value::StringObj(s) => s.is_empty(),
4612                Value::Cell(v) => v.is_empty(),
4613                Value::Void => true,
4614                _ => false,
4615            };
4616            Ok(Value::Scalar(if empty { 1.0 } else { 0.0 }))
4617        }
4618        // iscell(v) — 1.0 if v is a cell array, 0.0 otherwise
4619        ("iscell", 1) => Ok(Value::Scalar(if matches!(&args[0], Value::Cell(_)) {
4620            1.0
4621        } else {
4622            0.0
4623        })),
4624        // cell(n) — create 1×n cell of Scalar(0.0) slots
4625        ("cell", 1) => {
4626            let n = scalar_arg(&args[0], name, 1)? as usize;
4627            Ok(Value::Cell(vec![Value::Scalar(0.0); n]))
4628        }
4629        // cell(m, n) — create 1×(m*n) cell (2-D layout deferred; stored flat)
4630        ("cell", 2) => {
4631            let m = scalar_arg(&args[0], name, 1)? as usize;
4632            let n = scalar_arg(&args[1], name, 2)? as usize;
4633            Ok(Value::Cell(vec![Value::Scalar(0.0); m * n]))
4634        }
4635        // cellfun(f, c) — apply f to each element of cell c.
4636        // Returns Value::Matrix when all results are scalars; otherwise Value::Cell.
4637        ("cellfun", 2) => {
4638            let f = args[0].clone();
4639            match &args[1] {
4640                Value::Cell(elems) => {
4641                    let elems = elems.clone();
4642                    let mut results = Vec::with_capacity(elems.len());
4643                    for elem in &elems {
4644                        let result =
4645                            call_function_value(&f, std::slice::from_ref(elem), io.as_deref_mut())?;
4646                        results.push(result);
4647                    }
4648                    // Try uniform output (all scalars)
4649                    let all_scalar = results.iter().all(|v| matches!(v, Value::Scalar(_)));
4650                    if all_scalar {
4651                        let vals: Vec<f64> = results
4652                            .iter()
4653                            .map(|v| {
4654                                if let Value::Scalar(n) = v {
4655                                    *n
4656                                } else {
4657                                    unreachable!()
4658                                }
4659                            })
4660                            .collect();
4661                        let n = vals.len();
4662                        if n == 0 {
4663                            Ok(Value::Matrix(Array2::zeros((1, 0))))
4664                        } else {
4665                            Ok(Value::Matrix(Array2::from_shape_vec((1, n), vals).unwrap()))
4666                        }
4667                    } else {
4668                        Ok(Value::Cell(results))
4669                    }
4670                }
4671                _ => Err("cellfun: second argument must be a cell array".to_string()),
4672            }
4673        }
4674        // arrayfun(f, v) — apply f element-wise to matrix v.
4675        // Returns same-shape Value::Matrix (scalar-returning f only).
4676        ("arrayfun", 2) => {
4677            let f = args[0].clone();
4678            match &args[1] {
4679                Value::Matrix(m) => {
4680                    let m = m.clone();
4681                    let mut flat = Vec::with_capacity(m.len());
4682                    // Iterate in column-major order
4683                    for col in 0..m.ncols() {
4684                        for row in 0..m.nrows() {
4685                            let elem = Value::Scalar(m[[row, col]]);
4686                            let result = call_function_value(&f, &[elem], io.as_deref_mut())?;
4687                            match result {
4688                                Value::Scalar(n) => flat.push(n),
4689                                _ => {
4690                                    return Err(
4691                                        "arrayfun: function must return a scalar".to_string()
4692                                    );
4693                                }
4694                            }
4695                        }
4696                    }
4697                    Ok(Value::Matrix(
4698                        Array2::from_shape_vec((m.nrows(), m.ncols()), flat).unwrap(),
4699                    ))
4700                }
4701                Value::Scalar(n) => {
4702                    let elem = Value::Scalar(*n);
4703                    let result = call_function_value(&f, &[elem], io.as_deref_mut())?;
4704                    Ok(result)
4705                }
4706                _ => {
4707                    Err("arrayfun: second argument must be a numeric matrix or scalar".to_string())
4708                }
4709            }
4710        }
4711        // lower(s) — convert to lowercase
4712        ("lower", 1) => match &args[0] {
4713            Value::Str(s) => Ok(Value::Str(s.to_lowercase())),
4714            Value::StringObj(s) => Ok(Value::StringObj(s.to_lowercase())),
4715            _ => Err("lower: argument must be a string".to_string()),
4716        },
4717        // upper(s) — convert to uppercase
4718        ("upper", 1) => match &args[0] {
4719            Value::Str(s) => Ok(Value::Str(s.to_uppercase())),
4720            Value::StringObj(s) => Ok(Value::StringObj(s.to_uppercase())),
4721            _ => Err("upper: argument must be a string".to_string()),
4722        },
4723        // strtrim(s) — trim leading/trailing whitespace
4724        ("strtrim", 1) => match &args[0] {
4725            Value::Str(s) => Ok(Value::Str(s.trim().to_string())),
4726            Value::StringObj(s) => Ok(Value::StringObj(s.trim().to_string())),
4727            _ => Err("strtrim: argument must be a string".to_string()),
4728        },
4729        // strrep(s, old, new) — replace all occurrences
4730        ("strrep", 3) => {
4731            let s = string_arg(&args[0], name, 1)?.to_string();
4732            let old = string_arg(&args[1], name, 2)?;
4733            let new = string_arg(&args[2], name, 3)?;
4734            let result = s.replace(old, new);
4735            match &args[0] {
4736                Value::StringObj(_) => Ok(Value::StringObj(result)),
4737                _ => Ok(Value::Str(result)),
4738            }
4739        }
4740        // strcmp(a, b) — case-sensitive string comparison
4741        ("strcmp", 2) => {
4742            let a = string_arg(&args[0], name, 1)?;
4743            let b = string_arg(&args[1], name, 2)?;
4744            Ok(Value::Scalar(bool_to_f64(a == b)))
4745        }
4746        // strcmpi(a, b) — case-insensitive comparison
4747        ("strcmpi", 2) => {
4748            let a = string_arg(&args[0], name, 1)?.to_lowercase();
4749            let b = string_arg(&args[1], name, 2)?.to_lowercase();
4750            Ok(Value::Scalar(bool_to_f64(a == b)))
4751        }
4752        // disp(x) — display value without variable name, like MATLAB disp()
4753        ("disp", 1) => {
4754            use std::io::Write;
4755            let mode = get_display_fmt();
4756            let output = match &args[0] {
4757                Value::Str(s) | Value::StringObj(s) => format!("{s}\n"),
4758                v => match format_value_full(v, &mode) {
4759                    Some(block) => format!("{block}\n\n"),
4760                    None => format!("{}\n", format_value(v, get_display_base(), &mode)),
4761                },
4762            };
4763            match io {
4764                Some(ctx) => ctx.write_to_fd(1, &output)?,
4765                None => {
4766                    print!("{output}");
4767                    if output.contains('\n') {
4768                        std::io::stdout().flush().ok();
4769                    }
4770                }
4771            }
4772            Ok(Value::Void)
4773        }
4774        // sprintf(fmt, ...) — format and return as char array
4775        ("sprintf", n) if n >= 1 => {
4776            let fmt = string_arg(&args[0], name, 1)?.to_string();
4777            let result = format_printf(&fmt, &args[1..])?;
4778            Ok(Value::Str(result))
4779        }
4780        // fprintf([fd,] fmt, ...) — format and print; fd defaults to 1 (stdout)
4781        ("fprintf", n) if n >= 1 => {
4782            // If first arg is a numeric scalar, treat it as a file descriptor.
4783            let (fd, fmt_idx) = match &args[0] {
4784                Value::Scalar(n) => (*n as i32, 1),
4785                _ => (1, 0),
4786            };
4787            if fmt_idx >= args.len() {
4788                return Err("fprintf: missing format string".to_string());
4789            }
4790            let fmt = string_arg(&args[fmt_idx], name, fmt_idx + 1)?.to_string();
4791            let output = format_printf(&fmt, &args[fmt_idx + 1..])?;
4792            match io {
4793                Some(ctx) => ctx.write_to_fd(fd, &output)?,
4794                None => {
4795                    // No I/O context: only stdout (fd 1) is allowed
4796                    if fd == 1 {
4797                        use std::io::Write;
4798                        print!("{output}");
4799                        if output.contains('\n') {
4800                            std::io::stdout().flush().ok();
4801                        }
4802                    } else {
4803                        return Err("fprintf: file I/O not available in this context".to_string());
4804                    }
4805                }
4806            }
4807            Ok(Value::Void)
4808        }
4809        // fopen(path, mode) — open a file; returns fd or -1 on failure
4810        ("fopen", 2) => {
4811            let path = string_arg(&args[0], name, 1)?;
4812            let mode = string_arg(&args[1], name, 2)?;
4813            match io {
4814                Some(ctx) => Ok(Value::Scalar(ctx.fopen(path, mode) as f64)),
4815                None => Err("fopen: file I/O not available in this context".to_string()),
4816            }
4817        }
4818        // fclose(fd) or fclose('all')
4819        ("fclose", 1) => match &args[0] {
4820            Value::Str(s) if s == "all" => {
4821                if let Some(ctx) = io {
4822                    ctx.fclose_all();
4823                }
4824                Ok(Value::Scalar(0.0))
4825            }
4826            _ => {
4827                let fd = scalar_arg(&args[0], name, 1)? as i32;
4828                match io {
4829                    Some(ctx) => Ok(Value::Scalar(ctx.fclose(fd) as f64)),
4830                    None => Err("fclose: file I/O not available in this context".to_string()),
4831                }
4832            }
4833        },
4834        // fgetl(fd) — read line, strip newline; returns Str or Scalar(-1) at EOF
4835        ("fgetl", 1) => {
4836            let fd = scalar_arg(&args[0], name, 1)? as i32;
4837            match io {
4838                Some(ctx) => match ctx.fgetl(fd) {
4839                    Some(line) => Ok(Value::Str(line)),
4840                    None => Ok(Value::Scalar(-1.0)),
4841                },
4842                None => Err("fgetl: file I/O not available in this context".to_string()),
4843            }
4844        }
4845        // fgets(fd) — read line, keep newline; returns Str or Scalar(-1) at EOF
4846        ("fgets", 1) => {
4847            let fd = scalar_arg(&args[0], name, 1)? as i32;
4848            match io {
4849                Some(ctx) => match ctx.fgets(fd) {
4850                    Some(line) => Ok(Value::Str(line)),
4851                    None => Ok(Value::Scalar(-1.0)),
4852                },
4853                None => Err("fgets: file I/O not available in this context".to_string()),
4854            }
4855        }
4856        // isfile(path) — 1.0 if path exists and is a regular file, else 0.0
4857        ("isfile", 1) => {
4858            let path = string_arg(&args[0], name, 1)?;
4859            let is_file = std::fs::metadata(path)
4860                .map(|m| m.is_file())
4861                .unwrap_or(false);
4862            Ok(Value::Scalar(bool_to_f64(is_file)))
4863        }
4864        // isfolder(path) — 1.0 if path exists and is a directory, else 0.0
4865        ("isfolder", 1) => {
4866            let path = string_arg(&args[0], name, 1)?;
4867            let is_dir = std::fs::metadata(path).map(|m| m.is_dir()).unwrap_or(false);
4868            Ok(Value::Scalar(bool_to_f64(is_dir)))
4869        }
4870        // genpath(dir) — return dir and all subdirectories as a path separator-delimited string
4871        ("genpath", 1) => {
4872            let root = string_arg(&args[0], name, 1)?;
4873            let sep = if cfg!(windows) { ';' } else { ':' };
4874            let mut dirs: Vec<String> = Vec::new();
4875            let mut stack = vec![std::path::PathBuf::from(root)];
4876            while let Some(dir) = stack.pop() {
4877                if !dir.is_dir() {
4878                    continue;
4879                }
4880                dirs.push(dir.to_string_lossy().into_owned());
4881                if let Ok(entries) = std::fs::read_dir(&dir) {
4882                    let mut children: Vec<std::path::PathBuf> = entries
4883                        .filter_map(|e| e.ok())
4884                        .map(|e| e.path())
4885                        .filter(|p| p.is_dir())
4886                        .collect();
4887                    children.sort();
4888                    children.reverse();
4889                    stack.extend(children);
4890                }
4891            }
4892            Ok(Value::Str(dirs.join(&sep.to_string())))
4893        }
4894        // pwd() — current working directory as a char array (parser sends ans as sole arg for empty calls)
4895        ("pwd", _) => {
4896            let cwd = std::env::current_dir()
4897                .map(|p| p.to_string_lossy().into_owned())
4898                .unwrap_or_default();
4899            Ok(Value::Str(cwd))
4900        }
4901        // exist(name) — check var (1), then file (2), else 0
4902        ("exist", 1) => {
4903            let name_arg = string_arg(&args[0], name, 1)?;
4904            if env.contains_key(name_arg) {
4905                Ok(Value::Scalar(1.0))
4906            } else if std::path::Path::new(name_arg).is_file() {
4907                Ok(Value::Scalar(2.0))
4908            } else {
4909                Ok(Value::Scalar(0.0))
4910            }
4911        }
4912        // exist(name, 'var') or exist(name, 'file')
4913        ("exist", 2) => {
4914            let name_arg = string_arg(&args[0], name, 1)?;
4915            let kind = string_arg(&args[1], name, 2)?;
4916            match kind {
4917                "var" => Ok(Value::Scalar(if env.contains_key(name_arg) {
4918                    1.0
4919                } else {
4920                    0.0
4921                })),
4922                "file" => Ok(Value::Scalar(if std::path::Path::new(name_arg).is_file() {
4923                    2.0
4924                } else {
4925                    0.0
4926                })),
4927                other => Err(format!(
4928                    "exist: unknown type '{other}', expected 'var' or 'file'"
4929                )),
4930            }
4931        }
4932        // dlmread(path) / dlmread(path, delim)
4933        ("dlmread", 1) => {
4934            let path = string_arg(&args[0], name, 1)?.to_string();
4935            dlmread_impl(&path, None)
4936        }
4937        ("dlmread", 2) => {
4938            let path = string_arg(&args[0], name, 1)?.to_string();
4939            let delim = interpret_delim(string_arg(&args[1], name, 2)?);
4940            dlmread_impl(&path, Some(delim))
4941        }
4942        // dlmwrite(path, A) / dlmwrite(path, A, delim)
4943        ("dlmwrite", 2) => {
4944            let path = string_arg(&args[0], name, 1)?.to_string();
4945            dlmwrite_impl(&path, &args[1], None)
4946        }
4947        ("dlmwrite", 3) => {
4948            let path = string_arg(&args[0], name, 1)?.to_string();
4949            let delim = interpret_delim(string_arg(&args[2], name, 3)?);
4950            dlmwrite_impl(&path, &args[1], Some(delim))
4951        }
4952        // readmatrix(path) / readmatrix(path, 'Delimiter', d)
4953        ("readmatrix", n) if n == 1 || n == 3 => {
4954            let path = string_arg(&args[0], name, 1)?.to_string();
4955            let delim = parse_delimiter_opt(name, args, 1)?;
4956            readmatrix_impl(&path, delim)
4957        }
4958        // readtable(path) / readtable(path, 'Delimiter', d)
4959        ("readtable", n) if n == 1 || n == 3 => {
4960            let path = string_arg(&args[0], name, 1)?.to_string();
4961            let delim = parse_delimiter_opt(name, args, 1)?;
4962            readtable_impl(&path, delim)
4963        }
4964        // writetable(T, path) / writetable(T, path, 'Delimiter', d)
4965        ("writetable", n) if n == 2 || n == 4 => {
4966            let path = string_arg(&args[1], name, 2)?.to_string();
4967            let delim = parse_delimiter_opt(name, args, 2)?;
4968            writetable_impl(&args[0], &path, delim)
4969        }
4970        // xor(a, b) — element-wise XOR: (a != 0) XOR (b != 0)
4971        ("xor", 2) => {
4972            let a = &args[0];
4973            let b = &args[1];
4974            match (a, b) {
4975                (Value::Scalar(x), Value::Scalar(y)) => {
4976                    Ok(Value::Scalar(bool_to_f64((*x != 0.0) ^ (*y != 0.0))))
4977                }
4978                (Value::Matrix(mx), Value::Matrix(my)) => {
4979                    if mx.shape() != my.shape() {
4980                        return Err("xor: matrices must have the same dimensions".to_string());
4981                    }
4982                    Ok(Value::Matrix(ndarray::Zip::from(mx).and(my).map_collect(
4983                        |a, b| bool_to_f64((*a != 0.0) ^ (*b != 0.0)),
4984                    )))
4985                }
4986                (Value::Scalar(s), Value::Matrix(m)) => {
4987                    let sv = *s != 0.0;
4988                    Ok(Value::Matrix(m.mapv(|x| bool_to_f64(sv ^ (x != 0.0)))))
4989                }
4990                (Value::Matrix(m), Value::Scalar(s)) => {
4991                    let sv = *s != 0.0;
4992                    Ok(Value::Matrix(m.mapv(|x| bool_to_f64((x != 0.0) ^ sv))))
4993                }
4994                _ => Err("xor: arguments must be numeric".to_string()),
4995            }
4996        }
4997        // not(a) — element-wise NOT (alias for ~a)
4998        ("not", 1) => apply_elem(&args[0], |x| if x == 0.0 { 1.0 } else { 0.0 }),
4999        // int2str(x) — round to nearest integer, return as char array
5000        ("int2str", 1) => match &args[0] {
5001            Value::Scalar(n) => Ok(Value::Str(format!("{}", n.round() as i64))),
5002            Value::Matrix(m) => {
5003                let parts: Vec<String> =
5004                    m.iter().map(|x| format!("{}", x.round() as i64)).collect();
5005                Ok(Value::Str(parts.join("  ")))
5006            }
5007            _ => Err("int2str: argument must be numeric".to_string()),
5008        },
5009        // mat2str(A) — matrix to MATLAB literal syntax string
5010        ("mat2str", 1) => match &args[0] {
5011            Value::Scalar(n) => Ok(Value::Str(format!("{n}"))),
5012            Value::Matrix(m) => {
5013                if m.nrows() == 0 || m.ncols() == 0 {
5014                    return Ok(Value::Str("[]".to_string()));
5015                }
5016                let mut s = String::from("[");
5017                for (r, row) in m.rows().into_iter().enumerate() {
5018                    if r > 0 {
5019                        s.push(';');
5020                    }
5021                    for (c, val) in row.iter().enumerate() {
5022                        if c > 0 {
5023                            s.push(' ');
5024                        }
5025                        s.push_str(&format!("{val}"));
5026                    }
5027                }
5028                s.push(']');
5029                Ok(Value::Str(s))
5030            }
5031            _ => Err("mat2str: argument must be numeric".to_string()),
5032        },
5033        // strsplit(s, delim) — split string by delimiter, return cell array
5034        ("strsplit", 2) => {
5035            let s = string_arg(&args[0], name, 1)?.to_string();
5036            let delim = string_arg(&args[1], name, 2)?.to_string();
5037            let parts: Vec<Value> = s
5038                .split(delim.as_str())
5039                .map(|p| Value::Str(p.to_string()))
5040                .collect();
5041            Ok(Value::Cell(parts))
5042        }
5043        // strsplit(s) — split on whitespace
5044        ("strsplit", 1) => {
5045            let s = string_arg(&args[0], name, 1)?.to_string();
5046            let parts: Vec<Value> = s
5047                .split_whitespace()
5048                .map(|p| Value::Str(p.to_string()))
5049                .collect();
5050            Ok(Value::Cell(parts))
5051        }
5052        // strjoin(c) / strjoin(c, delim) — join a cell array of strings
5053        ("strjoin", n) if n == 1 || n == 2 => {
5054            let cells = match &args[0] {
5055                Value::Cell(v) => v,
5056                _ => {
5057                    return Err(
5058                        "strjoin: first argument must be a cell array of strings".to_string()
5059                    );
5060                }
5061            };
5062            let delim = if n == 2 {
5063                string_arg(&args[1], name, 2)?.to_string()
5064            } else {
5065                " ".to_string()
5066            };
5067            let mut parts: Vec<String> = Vec::with_capacity(cells.len());
5068            for (i, v) in cells.iter().enumerate() {
5069                match v {
5070                    Value::Str(s) | Value::StringObj(s) => parts.push(s.clone()),
5071                    _ => return Err(format!("strjoin: element {} must be a string", i + 1)),
5072                }
5073            }
5074            Ok(Value::Str(parts.join(&delim)))
5075        }
5076        // contains(s, pat) / contains(s, pat, 'IgnoreCase', tf) — substring check
5077        ("contains", 2) => {
5078            let s = string_arg(&args[0], name, 1)?;
5079            let pat = string_arg(&args[1], name, 2)?;
5080            Ok(Value::Scalar(bool_to_f64(s.contains(pat))))
5081        }
5082        ("contains", 4) => {
5083            let s = string_arg(&args[0], name, 1)?;
5084            let pat = string_arg(&args[1], name, 2)?;
5085            let key = string_arg(&args[2], name, 3)?;
5086            if key != "IgnoreCase" {
5087                return Err(format!(
5088                    "contains: unknown option '{key}'; expected 'IgnoreCase'"
5089                ));
5090            }
5091            let ignore = match &args[3] {
5092                Value::Scalar(n) => *n != 0.0,
5093                _ => return Err("contains: 'IgnoreCase' value must be a scalar".to_string()),
5094            };
5095            if ignore {
5096                Ok(Value::Scalar(bool_to_f64(
5097                    s.to_lowercase().contains(&pat.to_lowercase()),
5098                )))
5099            } else {
5100                Ok(Value::Scalar(bool_to_f64(s.contains(pat))))
5101            }
5102        }
5103        // startsWith(s, pat) — prefix check
5104        ("startsWith", 2) => {
5105            let s = string_arg(&args[0], name, 1)?;
5106            let pat = string_arg(&args[1], name, 2)?;
5107            Ok(Value::Scalar(bool_to_f64(s.starts_with(pat))))
5108        }
5109        // endsWith(s, pat) — suffix check
5110        ("endsWith", 2) => {
5111            let s = string_arg(&args[0], name, 1)?;
5112            let pat = string_arg(&args[1], name, 2)?;
5113            Ok(Value::Scalar(bool_to_f64(s.ends_with(pat))))
5114        }
5115        // regexp(s, pat) / regexp(s, pat, 'match') — regular expression search
5116        ("regexp", 2) => {
5117            let s = string_arg(&args[0], name, 1)?.to_string();
5118            let pat = string_arg(&args[1], name, 2)?.to_string();
5119            regexp_impl("regexp", &s, &pat, false, false)
5120        }
5121        ("regexp", 3) => {
5122            let s = string_arg(&args[0], name, 1)?.to_string();
5123            let pat = string_arg(&args[1], name, 2)?.to_string();
5124            let opt = string_arg(&args[2], name, 3)?;
5125            if opt != "match" {
5126                return Err(format!("regexp: unknown option '{opt}'; expected 'match'"));
5127            }
5128            regexp_impl("regexp", &s, &pat, false, true)
5129        }
5130        // regexpi(s, pat) — case-insensitive regexp
5131        ("regexpi", 2) => {
5132            let s = string_arg(&args[0], name, 1)?.to_string();
5133            let pat = string_arg(&args[1], name, 2)?.to_string();
5134            regexp_impl("regexpi", &s, &pat, true, false)
5135        }
5136        ("regexpi", 3) => {
5137            let s = string_arg(&args[0], name, 1)?.to_string();
5138            let pat = string_arg(&args[1], name, 2)?.to_string();
5139            let opt = string_arg(&args[2], name, 3)?;
5140            if opt != "match" {
5141                return Err(format!("regexpi: unknown option '{opt}'; expected 'match'"));
5142            }
5143            regexp_impl("regexpi", &s, &pat, true, true)
5144        }
5145        // regexprep(s, pat, rep) — replace all matches with literal replacement
5146        ("regexprep", 3) => {
5147            let s = string_arg(&args[0], name, 1)?.to_string();
5148            let pat = string_arg(&args[1], name, 2)?.to_string();
5149            let rep = string_arg(&args[2], name, 3)?.to_string();
5150            regexprep_impl(&s, &pat, &rep)
5151        }
5152        // error(fmt, args...) — raise a runtime error with a formatted message
5153        ("error", _) if !args.is_empty() => {
5154            let fmt_str = match &args[0] {
5155                Value::Str(s) | Value::StringObj(s) => s.clone(),
5156                _ => return Err("error: first argument must be a format string".to_string()),
5157            };
5158            let msg = format_printf(&fmt_str, &args[1..])?;
5159            Err(msg)
5160        }
5161        // warning(fmt, args...) — print a warning to stderr, continue execution
5162        ("warning", _) if !args.is_empty() => {
5163            let fmt_str = match &args[0] {
5164                Value::Str(s) | Value::StringObj(s) => s.clone(),
5165                _ => return Err("warning: first argument must be a format string".to_string()),
5166            };
5167            let msg = format_printf(&fmt_str, &args[1..])?;
5168            eprintln!("warning: {msg}");
5169            Ok(Value::Void)
5170        }
5171        // lasterr() — return last error message; lasterr(msg) — set and return previous
5172        ("lasterr", 0) => Ok(Value::Str(get_last_err())),
5173        ("lasterr", 1) => {
5174            let prev = get_last_err();
5175            let new_msg = match &args[0] {
5176                Value::Str(s) | Value::StringObj(s) => s.clone(),
5177                _ => return Err("lasterr: argument must be a string".to_string()),
5178            };
5179            set_last_err(&new_msg);
5180            Ok(Value::Str(prev))
5181        }
5182        // pcall(@func, args...) — protected call; returns [ok, result_or_msg]
5183        ("pcall", _) if !args.is_empty() => {
5184            let callable = args[0].clone();
5185            let call_args = &args[1..];
5186            let result = match &callable {
5187                Value::Lambda(f) => {
5188                    let f = f.clone();
5189                    f.0(call_args, io)
5190                }
5191                Value::Function { .. } => match io {
5192                    Some(io_ref) => FN_CALL_HOOK.with(|c| match c.get() {
5193                        Some(hook) => hook("<pcall>", &callable, call_args, env, io_ref),
5194                        None => Err("pcall: function execution not initialized".to_string()),
5195                    }),
5196                    None => {
5197                        let mut tmp_io = IoContext::new();
5198                        FN_CALL_HOOK.with(|c| match c.get() {
5199                            Some(hook) => hook("<pcall>", &callable, call_args, env, &mut tmp_io),
5200                            None => Err("pcall: function execution not initialized".to_string()),
5201                        })
5202                    }
5203                },
5204                _ => {
5205                    return Err(
5206                        "pcall: first argument must be a function handle (@func)".to_string()
5207                    );
5208                }
5209            };
5210            match result {
5211                Ok(v) => Ok(Value::Tuple(vec![Value::Scalar(1.0), v])),
5212                Err(msg) => {
5213                    set_last_err(&msg);
5214                    Ok(Value::Tuple(vec![Value::Scalar(0.0), Value::Str(msg)]))
5215                }
5216            }
5217        }
5218        // ── Phase 18 — Advanced linear algebra ──────────────────────────────
5219
5220        // eig(A): d = eig(A) → eigenvalue column vector; [V,D] = eig(A) → tuple.
5221        ("eig", 1) => match &args[0] {
5222            Value::Scalar(n) => {
5223                if get_nargout() <= 1 {
5224                    Ok(Value::Matrix(
5225                        Array2::from_shape_vec((1, 1), vec![*n]).unwrap(),
5226                    ))
5227                } else {
5228                    Ok(Value::Tuple(vec![
5229                        Value::Matrix(Array2::eye(1)),
5230                        Value::Matrix(Array2::from_elem((1, 1), *n)),
5231                    ]))
5232                }
5233            }
5234            Value::Matrix(m) => {
5235                let (evals, evecs) = eig_compute(m)?;
5236                let nn = evals.len();
5237                let has_imag = evals.iter().any(|c| c.im.abs() > 1e-14);
5238                if get_nargout() <= 1 {
5239                    if has_imag {
5240                        Ok(Value::ComplexMatrix(
5241                            Array2::from_shape_vec((nn, 1), evals).unwrap(),
5242                        ))
5243                    } else {
5244                        let reals: Vec<f64> = evals.iter().map(|c| c.re).collect();
5245                        Ok(Value::Matrix(
5246                            Array2::from_shape_vec((nn, 1), reals).unwrap(),
5247                        ))
5248                    }
5249                } else if has_imag {
5250                    Err("eig: [V,D] form not supported when eigenvalues are complex".to_string())
5251                } else {
5252                    let reals: Vec<f64> = evals.iter().map(|c| c.re).collect();
5253                    let mut d = Array2::<f64>::zeros((nn, nn));
5254                    for (i, &e) in reals.iter().enumerate() {
5255                        d[[i, i]] = e;
5256                    }
5257                    Ok(Value::Tuple(vec![Value::Matrix(evecs), Value::Matrix(d)]))
5258                }
5259            }
5260            _ => Err("eig: argument must be a real numeric matrix".to_string()),
5261        },
5262
5263        // svd(A): s = svd(A) → singular values; [U,S,V] = svd(A) → full tuple.
5264        // svd(A, 'econ') → economy tuple.
5265        ("svd", 1) => match &args[0] {
5266            Value::Scalar(n) => {
5267                let sv = n.abs();
5268                if get_nargout() <= 1 {
5269                    Ok(Value::Matrix(
5270                        Array2::from_shape_vec((1, 1), vec![sv]).unwrap(),
5271                    ))
5272                } else {
5273                    Ok(Value::Tuple(vec![
5274                        Value::Matrix(Array2::eye(1)),
5275                        Value::Matrix(Array2::from_elem((1, 1), sv)),
5276                        Value::Matrix(Array2::eye(1)),
5277                    ]))
5278                }
5279            }
5280            Value::Matrix(m) => {
5281                let mm = m.nrows();
5282                let nn = m.ncols();
5283                let (u_c, s_v, v_c) = svd_compute(m)?;
5284                let k = s_v.len();
5285                if get_nargout() <= 1 {
5286                    let col: Vec<f64> = s_v;
5287                    Ok(Value::Matrix(Array2::from_shape_vec((k, 1), col).unwrap()))
5288                } else {
5289                    // Full SVD: extend U to m×m, S to m×n.
5290                    let u_full = complete_orthonormal_basis(&u_c);
5291                    let mut s_mat = Array2::<f64>::zeros((mm, nn));
5292                    for (i, &sv) in s_v.iter().enumerate() {
5293                        s_mat[[i, i]] = sv;
5294                    }
5295                    Ok(Value::Tuple(vec![
5296                        Value::Matrix(u_full),
5297                        Value::Matrix(s_mat),
5298                        Value::Matrix(v_c),
5299                    ]))
5300                }
5301            }
5302            _ => Err("svd: argument must be a real numeric matrix".to_string()),
5303        },
5304        ("svd", 2) => match (&args[0], &args[1]) {
5305            (Value::Matrix(m), Value::Str(opt) | Value::StringObj(opt)) if opt == "econ" => {
5306                let (u_c, s_v, v_c) = svd_compute(m)?;
5307                let k = s_v.len();
5308                let mut s_mat = Array2::<f64>::zeros((k, k));
5309                for (i, &sv) in s_v.iter().enumerate() {
5310                    s_mat[[i, i]] = sv;
5311                }
5312                Ok(Value::Tuple(vec![
5313                    Value::Matrix(u_c),
5314                    Value::Matrix(s_mat),
5315                    Value::Matrix(v_c),
5316                ]))
5317            }
5318            _ => Err("svd: expected svd(A, 'econ')".to_string()),
5319        },
5320
5321        // lu(A): R = lu(A) → U factor; [L,U,P] = lu(A) → full tuple (PA=LU).
5322        ("lu", 1) => match &args[0] {
5323            Value::Scalar(n) => {
5324                if get_nargout() <= 1 {
5325                    Ok(Value::Scalar(*n))
5326                } else {
5327                    Ok(Value::Tuple(vec![
5328                        Value::Matrix(Array2::eye(1)),
5329                        Value::Matrix(Array2::from_elem((1, 1), *n)),
5330                        Value::Matrix(Array2::eye(1)),
5331                    ]))
5332                }
5333            }
5334            Value::Matrix(m) => {
5335                let (l, u, p) = lu_decompose(m)?;
5336                if get_nargout() <= 1 {
5337                    Ok(Value::Matrix(u))
5338                } else {
5339                    Ok(Value::Tuple(vec![
5340                        Value::Matrix(l),
5341                        Value::Matrix(u),
5342                        Value::Matrix(p),
5343                    ]))
5344                }
5345            }
5346            _ => Err("lu: argument must be a real numeric matrix".to_string()),
5347        },
5348
5349        // qr(A): R = qr(A) → R factor; [Q,R] = qr(A) → full tuple.
5350        ("qr", 1) => match &args[0] {
5351            Value::Scalar(n) => {
5352                if get_nargout() <= 1 {
5353                    Ok(Value::Scalar(*n))
5354                } else {
5355                    Ok(Value::Tuple(vec![
5356                        Value::Matrix(Array2::from_elem(
5357                            (1, 1),
5358                            if *n >= 0.0 { 1.0 } else { -1.0 },
5359                        )),
5360                        Value::Matrix(Array2::from_elem((1, 1), n.abs())),
5361                    ]))
5362                }
5363            }
5364            Value::Matrix(m) => {
5365                let (q, r) = qr_decompose(m)?;
5366                if get_nargout() <= 1 {
5367                    Ok(Value::Matrix(r))
5368                } else {
5369                    Ok(Value::Tuple(vec![Value::Matrix(q), Value::Matrix(r)]))
5370                }
5371            }
5372            _ => Err("qr: argument must be a real numeric matrix".to_string()),
5373        },
5374
5375        // chol(A): always returns upper triangular R such that A = R'*R.
5376        ("chol", 1) => match &args[0] {
5377            Value::Scalar(n) => {
5378                if *n < 0.0 {
5379                    Err("chol: value is not positive definite".to_string())
5380                } else {
5381                    Ok(Value::Scalar(n.sqrt()))
5382                }
5383            }
5384            Value::Matrix(m) => Ok(Value::Matrix(chol_decompose(m)?)),
5385            _ => Err("chol: argument must be a real numeric matrix".to_string()),
5386        },
5387
5388        // rank(A): numerical rank via SVD threshold.
5389        ("rank", 1) => match &args[0] {
5390            Value::Scalar(x) => Ok(Value::Scalar(if x.abs() > 1e-15 { 1.0 } else { 0.0 })),
5391            Value::Matrix(m) => {
5392                let (_, s_v, _) = svd_compute(m)?;
5393                let tol = (m.nrows().max(m.ncols())) as f64
5394                    * s_v.first().copied().unwrap_or(0.0)
5395                    * f64::EPSILON
5396                    * 2.0;
5397                let r = s_v.iter().filter(|&&s| s > tol).count();
5398                Ok(Value::Scalar(r as f64))
5399            }
5400            _ => Err("rank: argument must be a real numeric matrix".to_string()),
5401        },
5402
5403        // null(A): orthonormal basis for null space of A (columns of V for ~0 singular values).
5404        ("null", 1) => match &args[0] {
5405            Value::Scalar(_) => Ok(Value::Matrix(Array2::zeros((1, 0)))),
5406            Value::Matrix(m) => {
5407                let nn = m.ncols();
5408                let (_, s_v, v_c) = svd_compute(m)?;
5409                let tol = (m.nrows().max(nn)) as f64
5410                    * s_v.first().copied().unwrap_or(0.0)
5411                    * f64::EPSILON
5412                    * 2.0;
5413                let r = s_v.iter().filter(|&&s| s > tol).count();
5414                let null_k = nn.saturating_sub(r);
5415                if null_k == 0 {
5416                    return Ok(Value::Matrix(Array2::zeros((nn, 0))));
5417                }
5418                let mut result = Array2::<f64>::zeros((nn, null_k));
5419                for j in 0..null_k {
5420                    let col_idx = r + j;
5421                    if col_idx < v_c.ncols() {
5422                        for i in 0..nn {
5423                            result[[i, j]] = v_c[[i, col_idx]];
5424                        }
5425                    }
5426                }
5427                Ok(Value::Matrix(result))
5428            }
5429            _ => Err("null: argument must be a real numeric matrix".to_string()),
5430        },
5431
5432        // orth(A): orthonormal basis for column space of A (columns of U for nonzero singular values).
5433        ("orth", 1) => match &args[0] {
5434            Value::Scalar(x) => {
5435                if x.abs() > 1e-15 {
5436                    Ok(Value::Matrix(Array2::from_elem((1, 1), 1.0)))
5437                } else {
5438                    Ok(Value::Matrix(Array2::zeros((1, 0))))
5439                }
5440            }
5441            Value::Matrix(m) => {
5442                let mm = m.nrows();
5443                let (u_c, s_v, _) = svd_compute(m)?;
5444                let tol = (mm.max(m.ncols())) as f64
5445                    * s_v.first().copied().unwrap_or(0.0)
5446                    * f64::EPSILON
5447                    * 2.0;
5448                let r = s_v.iter().filter(|&&s| s > tol).count();
5449                if r == 0 {
5450                    return Ok(Value::Matrix(Array2::zeros((mm, 0))));
5451                }
5452                let mut result = Array2::<f64>::zeros((mm, r));
5453                for j in 0..r {
5454                    if j < u_c.ncols() {
5455                        for i in 0..mm {
5456                            result[[i, j]] = u_c[[i, j]];
5457                        }
5458                    }
5459                }
5460                Ok(Value::Matrix(result))
5461            }
5462            _ => Err("orth: argument must be a real numeric matrix".to_string()),
5463        },
5464
5465        // cond(A): condition number = σ_max / σ_min (2-norm by default).
5466        ("cond", 1) => match &args[0] {
5467            Value::Scalar(x) => {
5468                if x.abs() < 1e-15 {
5469                    Ok(Value::Scalar(f64::INFINITY))
5470                } else {
5471                    Ok(Value::Scalar(1.0))
5472                }
5473            }
5474            Value::Matrix(m) => {
5475                let (_, s_v, _) = svd_compute(m)?;
5476                if s_v.is_empty() {
5477                    return Ok(Value::Scalar(1.0));
5478                }
5479                let s_max = s_v[0];
5480                let s_min = *s_v.last().unwrap();
5481                Ok(Value::Scalar(if s_min < 1e-15 {
5482                    f64::INFINITY
5483                } else {
5484                    s_max / s_min
5485                }))
5486            }
5487            _ => Err("cond: argument must be a real numeric matrix".to_string()),
5488        },
5489
5490        // pinv(A): Moore-Penrose pseudoinverse via SVD.
5491        ("pinv", 1) => match &args[0] {
5492            Value::Scalar(x) => Ok(Value::Scalar(if x.abs() < 1e-15 { 0.0 } else { 1.0 / x })),
5493            Value::Matrix(m) => {
5494                let mm = m.nrows();
5495                let nn = m.ncols();
5496                let (u_c, s_v, v_c) = svd_compute(m)?;
5497                let k = s_v.len();
5498                let tol =
5499                    (mm.max(nn)) as f64 * s_v.first().copied().unwrap_or(0.0) * f64::EPSILON * 2.0;
5500                // pinv = V * diag(1/σ) * U^T
5501                let mut result = Array2::<f64>::zeros((nn, mm));
5502                for j in 0..k {
5503                    if s_v[j] > tol {
5504                        let inv_s = 1.0 / s_v[j];
5505                        for r in 0..nn {
5506                            for c in 0..mm {
5507                                result[[r, c]] += v_c[[r, j]] * inv_s * u_c[[c, j]];
5508                            }
5509                        }
5510                    }
5511                }
5512                Ok(Value::Matrix(result))
5513            }
5514            _ => Err("pinv: argument must be a real numeric matrix".to_string()),
5515        },
5516
5517        // ── Phase 26 — FFT ───────────────────────────────────────────────────────
5518        ("fft", 1) => fft_call(&args[0], None),
5519        ("fft", 2) => {
5520            let n = scalar_arg(&args[1], "fft", 2)?;
5521            let n = n as usize;
5522            if n == 0 {
5523                return Err("fft: length must be positive".to_string());
5524            }
5525            fft_call(&args[0], Some(n))
5526        }
5527        ("ifft", 1) => ifft_call(&args[0]),
5528
5529        // fftshift(x) — circular shift by floor(N/2), no feature flag required
5530        ("fftshift", 1) => match &args[0] {
5531            Value::Scalar(s) => Ok(Value::Scalar(*s)),
5532            Value::Matrix(m) => {
5533                let (nrows, ncols) = (m.nrows(), m.ncols());
5534                if nrows == 1 {
5535                    let n = ncols;
5536                    let shift = n / 2;
5537                    let data: Vec<f64> = m.iter().copied().collect();
5538                    let mut out = vec![0.0f64; n];
5539                    for (i, &x) in data.iter().enumerate() {
5540                        out[(i + shift) % n] = x;
5541                    }
5542                    Ok(Value::Matrix(Array2::from_shape_vec((1, n), out).unwrap()))
5543                } else if ncols == 1 {
5544                    let n = nrows;
5545                    let shift = n / 2;
5546                    let data: Vec<f64> = m.iter().copied().collect();
5547                    let mut out = vec![0.0f64; n];
5548                    for (i, &x) in data.iter().enumerate() {
5549                        out[(i + shift) % n] = x;
5550                    }
5551                    Ok(Value::Matrix(Array2::from_shape_vec((n, 1), out).unwrap()))
5552                } else {
5553                    let row_shift = nrows / 2;
5554                    let col_shift = ncols / 2;
5555                    let mut out = Array2::<f64>::zeros((nrows, ncols));
5556                    for i in 0..nrows {
5557                        for j in 0..ncols {
5558                            out[[(i + row_shift) % nrows, (j + col_shift) % ncols]] = m[[i, j]];
5559                        }
5560                    }
5561                    Ok(Value::Matrix(out))
5562                }
5563            }
5564            _ => Err("fftshift: argument must be a numeric matrix".to_string()),
5565        },
5566
5567        // ifftshift(x) — inverse circular shift by ceil(N/2), no feature flag required
5568        ("ifftshift", 1) => match &args[0] {
5569            Value::Scalar(s) => Ok(Value::Scalar(*s)),
5570            Value::Matrix(m) => {
5571                let (nrows, ncols) = (m.nrows(), m.ncols());
5572                if nrows == 1 {
5573                    let n = ncols;
5574                    let shift = n.div_ceil(2);
5575                    let data: Vec<f64> = m.iter().copied().collect();
5576                    let mut out = vec![0.0f64; n];
5577                    for (i, &x) in data.iter().enumerate() {
5578                        out[(i + shift) % n] = x;
5579                    }
5580                    Ok(Value::Matrix(Array2::from_shape_vec((1, n), out).unwrap()))
5581                } else if ncols == 1 {
5582                    let n = nrows;
5583                    let shift = n.div_ceil(2);
5584                    let data: Vec<f64> = m.iter().copied().collect();
5585                    let mut out = vec![0.0f64; n];
5586                    for (i, &x) in data.iter().enumerate() {
5587                        out[(i + shift) % n] = x;
5588                    }
5589                    Ok(Value::Matrix(Array2::from_shape_vec((n, 1), out).unwrap()))
5590                } else {
5591                    let row_shift = nrows.div_ceil(2);
5592                    let col_shift = ncols.div_ceil(2);
5593                    let mut out = Array2::<f64>::zeros((nrows, ncols));
5594                    for i in 0..nrows {
5595                        for j in 0..ncols {
5596                            out[[(i + row_shift) % nrows, (j + col_shift) % ncols]] = m[[i, j]];
5597                        }
5598                    }
5599                    Ok(Value::Matrix(out))
5600                }
5601            }
5602            _ => Err("ifftshift: argument must be a numeric matrix".to_string()),
5603        },
5604
5605        // fftfreq(n, d) — DFT sample frequencies, no feature flag required
5606        ("fftfreq", 2) => {
5607            let n = match &args[0] {
5608                Value::Scalar(s) => {
5609                    let n = *s as usize;
5610                    if *s < 1.0 || (*s - n as f64).abs() > 1e-9 {
5611                        return Err("fftfreq: n must be a positive integer".to_string());
5612                    }
5613                    n
5614                }
5615                _ => return Err("fftfreq: first argument must be a scalar integer".to_string()),
5616            };
5617            let d = scalar_arg(&args[1], "fftfreq", 2)?;
5618            if d == 0.0 {
5619                return Err("fftfreq: sample spacing d must be nonzero".to_string());
5620            }
5621            // NumPy-compatible formula: [0:pos_count-1, -neg_count:-1] / (n*d)
5622            let pos_count = (n - 1) / 2 + 1;
5623            let neg_count = n / 2;
5624            let factor = 1.0 / (n as f64 * d);
5625            let mut freqs = Vec::with_capacity(n);
5626            for k in 0..pos_count as i64 {
5627                freqs.push(k as f64 * factor);
5628            }
5629            let neg_start = -(neg_count as i64);
5630            for k in neg_start..0 {
5631                freqs.push(k as f64 * factor);
5632            }
5633            Ok(Value::Matrix(
5634                Array2::from_shape_vec((1, n), freqs).unwrap(),
5635            ))
5636        }
5637
5638        // jsondecode(str) / jsonencode(val)
5639        ("jsondecode", 1) => jsondecode_impl(&args[0]),
5640        ("jsonencode", 1) => jsonencode_impl(&args[0]),
5641
5642        // load('file.mat') — assignment form: data = load('file.mat')
5643        ("load", 1) => {
5644            let path = match &args[0] {
5645                Value::Str(s) | Value::StringObj(s) => s.clone(),
5646                _ => return Err("load: argument must be a string path".to_string()),
5647            };
5648            if !path.ends_with(".mat") {
5649                return Err("load: use bare 'load path' syntax for non-.mat files".to_string());
5650            }
5651            load_mat_file(&path)
5652        }
5653
5654        // assert(cond)
5655        ("assert", 1) => {
5656            let truthy = match &args[0] {
5657                Value::Scalar(n) => *n != 0.0 && !n.is_nan(),
5658                Value::Matrix(m) => m.iter().all(|&x| x != 0.0 && !x.is_nan()),
5659                Value::Complex(re, im) => *re != 0.0 || *im != 0.0,
5660                Value::Str(s) | Value::StringObj(s) => !s.is_empty(),
5661                _ => false,
5662            };
5663            if truthy {
5664                Ok(Value::Void)
5665            } else {
5666                Err("assert: condition is false".to_string())
5667            }
5668        }
5669
5670        // assert(expected, actual)
5671        ("assert", 2) => assert_values_equal(&args[0], &args[1], None),
5672
5673        // assert(expected, actual, tol)
5674        ("assert", 3) => {
5675            let tol = match &args[2] {
5676                Value::Scalar(t) => *t,
5677                _ => return Err("assert: tolerance must be a scalar".to_string()),
5678            };
5679            assert_values_equal(&args[0], &args[1], Some(tol))
5680        }
5681
5682        // ── datetime() constructor ────────────────────────────────────────────
5683        ("datetime", 1) => match &args[0] {
5684            Value::Str(s) | Value::StringObj(s) => {
5685                let s = s.as_str();
5686                if s == "now" {
5687                    return Ok(Value::DateTime(crate::datetime::now_timestamp()));
5688                }
5689                if s == "today" {
5690                    return Ok(Value::DateTime(crate::datetime::today_timestamp()));
5691                }
5692                crate::datetime::parse_iso8601(s).map(Value::DateTime)
5693            }
5694            _ => Err("datetime: expected a string or numeric constructor arguments".to_string()),
5695        },
5696        // datetime(ts, 'ConvertFrom', 'posixtime') — must come before the 3-scalar form
5697        ("datetime", 3) if matches!(&args[1], Value::Str(_) | Value::StringObj(_)) => {
5698            let ts = scalar_arg(&args[0], "datetime", 1)?;
5699            match (&args[1], &args[2]) {
5700                (Value::Str(k) | Value::StringObj(k), Value::Str(v) | Value::StringObj(v))
5701                    if k.eq_ignore_ascii_case("convertfrom")
5702                        && v.eq_ignore_ascii_case("posixtime") =>
5703                {
5704                    Ok(Value::DateTime(ts))
5705                }
5706                _ => Err("datetime: unsupported arguments".to_string()),
5707            }
5708        }
5709        ("datetime", 3) => {
5710            let y = scalar_arg(&args[0], "datetime", 1)? as i64;
5711            let mo = scalar_arg(&args[1], "datetime", 2)? as u32;
5712            let d = scalar_arg(&args[2], "datetime", 3)? as u32;
5713            Ok(Value::DateTime(crate::datetime::civil_to_timestamp(
5714                y, mo, d, 0, 0, 0.0,
5715            )))
5716        }
5717        ("datetime", 6) => {
5718            let y = scalar_arg(&args[0], "datetime", 1)? as i64;
5719            let mo = scalar_arg(&args[1], "datetime", 2)? as u32;
5720            let d = scalar_arg(&args[2], "datetime", 3)? as u32;
5721            let h = scalar_arg(&args[3], "datetime", 4)? as u32;
5722            let mi = scalar_arg(&args[4], "datetime", 5)? as u32;
5723            let s = scalar_arg(&args[5], "datetime", 6)?;
5724            Ok(Value::DateTime(crate::datetime::civil_to_timestamp(
5725                y, mo, d, h, mi, s,
5726            )))
5727        }
5728
5729        // ── Component extractors ──────────────────────────────────────────────
5730        ("year", 1) => match &args[0] {
5731            Value::DateTime(ts) => {
5732                let (y, ..) = crate::datetime::timestamp_to_civil(*ts);
5733                Ok(Value::Scalar(y as f64))
5734            }
5735            Value::DateTimeArray(v) => {
5736                let rows: Vec<f64> = v
5737                    .iter()
5738                    .map(|ts| {
5739                        let (y, ..) = crate::datetime::timestamp_to_civil(*ts);
5740                        y as f64
5741                    })
5742                    .collect();
5743                Ok(Value::Matrix(
5744                    ndarray::Array2::from_shape_vec((rows.len(), 1), rows)
5745                        .map_err(|e| e.to_string())?,
5746                ))
5747            }
5748            _ => Err("year: argument must be a datetime".to_string()),
5749        },
5750        ("month", 1) => match &args[0] {
5751            Value::DateTime(ts) => {
5752                let (_, mo, ..) = crate::datetime::timestamp_to_civil(*ts);
5753                Ok(Value::Scalar(mo as f64))
5754            }
5755            Value::DateTimeArray(v) => {
5756                let rows: Vec<f64> = v
5757                    .iter()
5758                    .map(|ts| {
5759                        let (_, mo, ..) = crate::datetime::timestamp_to_civil(*ts);
5760                        mo as f64
5761                    })
5762                    .collect();
5763                Ok(Value::Matrix(
5764                    ndarray::Array2::from_shape_vec((rows.len(), 1), rows)
5765                        .map_err(|e| e.to_string())?,
5766                ))
5767            }
5768            _ => Err("month: argument must be a datetime".to_string()),
5769        },
5770        ("day", 1) => match &args[0] {
5771            Value::DateTime(ts) => {
5772                let (_, _, d, ..) = crate::datetime::timestamp_to_civil(*ts);
5773                Ok(Value::Scalar(d as f64))
5774            }
5775            Value::DateTimeArray(v) => {
5776                let rows: Vec<f64> = v
5777                    .iter()
5778                    .map(|ts| {
5779                        let (_, _, d, ..) = crate::datetime::timestamp_to_civil(*ts);
5780                        d as f64
5781                    })
5782                    .collect();
5783                Ok(Value::Matrix(
5784                    ndarray::Array2::from_shape_vec((rows.len(), 1), rows)
5785                        .map_err(|e| e.to_string())?,
5786                ))
5787            }
5788            _ => Err("day: argument must be a datetime".to_string()),
5789        },
5790        ("hour", 1) => match &args[0] {
5791            Value::DateTime(ts) => {
5792                let (_, _, _, h, ..) = crate::datetime::timestamp_to_civil(*ts);
5793                Ok(Value::Scalar(h as f64))
5794            }
5795            Value::DateTimeArray(v) => {
5796                let rows: Vec<f64> = v
5797                    .iter()
5798                    .map(|ts| {
5799                        let (_, _, _, h, ..) = crate::datetime::timestamp_to_civil(*ts);
5800                        h as f64
5801                    })
5802                    .collect();
5803                Ok(Value::Matrix(
5804                    ndarray::Array2::from_shape_vec((rows.len(), 1), rows)
5805                        .map_err(|e| e.to_string())?,
5806                ))
5807            }
5808            _ => Err("hour: argument must be a datetime or duration".to_string()),
5809        },
5810        ("minute", 1) => match &args[0] {
5811            Value::DateTime(ts) => {
5812                let (_, _, _, _, mi, ..) = crate::datetime::timestamp_to_civil(*ts);
5813                Ok(Value::Scalar(mi as f64))
5814            }
5815            Value::DateTimeArray(v) => {
5816                let rows: Vec<f64> = v
5817                    .iter()
5818                    .map(|ts| {
5819                        let (_, _, _, _, mi, ..) = crate::datetime::timestamp_to_civil(*ts);
5820                        mi as f64
5821                    })
5822                    .collect();
5823                Ok(Value::Matrix(
5824                    ndarray::Array2::from_shape_vec((rows.len(), 1), rows)
5825                        .map_err(|e| e.to_string())?,
5826                ))
5827            }
5828            _ => Err("minute: argument must be a datetime or duration".to_string()),
5829        },
5830        ("second", 1) => match &args[0] {
5831            Value::DateTime(ts) => {
5832                let (_, _, _, _, _, s) = crate::datetime::timestamp_to_civil(*ts);
5833                Ok(Value::Scalar(s))
5834            }
5835            Value::DateTimeArray(v) => {
5836                let rows: Vec<f64> = v
5837                    .iter()
5838                    .map(|ts| {
5839                        let (_, _, _, _, _, s) = crate::datetime::timestamp_to_civil(*ts);
5840                        s
5841                    })
5842                    .collect();
5843                Ok(Value::Matrix(
5844                    ndarray::Array2::from_shape_vec((rows.len(), 1), rows)
5845                        .map_err(|e| e.to_string())?,
5846                ))
5847            }
5848            _ => Err("second: argument must be a datetime or duration".to_string()),
5849        },
5850
5851        // ── Predicates ────────────────────────────────────────────────────────
5852        ("isdatetime", 1) => Ok(Value::Scalar(bool_to_f64(matches!(
5853            &args[0],
5854            Value::DateTime(_) | Value::DateTimeArray(_)
5855        )))),
5856        ("isduration", 1) => Ok(Value::Scalar(bool_to_f64(matches!(
5857            &args[0],
5858            Value::Duration(_) | Value::DurationArray(_)
5859        )))),
5860        ("isnat", 1) => match &args[0] {
5861            Value::DateTime(ts) => Ok(Value::Scalar(bool_to_f64(ts.is_nan()))),
5862            Value::DateTimeArray(v) => {
5863                let rows: Vec<f64> = v
5864                    .iter()
5865                    .map(|ts| if ts.is_nan() { 1.0 } else { 0.0 })
5866                    .collect();
5867                Ok(Value::Matrix(
5868                    ndarray::Array2::from_shape_vec((rows.len(), 1), rows)
5869                        .map_err(|e| e.to_string())?,
5870                ))
5871            }
5872            _ => Ok(Value::Scalar(0.0)),
5873        },
5874
5875        // ── Duration constructors / extractors (overloaded) ───────────────────
5876        ("hours", 1) => match &args[0] {
5877            Value::Duration(s) => Ok(Value::Scalar(*s / 3600.0)),
5878            Value::DurationArray(v) => {
5879                let rows: Vec<f64> = v.iter().map(|s| s / 3600.0).collect();
5880                Ok(Value::Matrix(
5881                    ndarray::Array2::from_shape_vec((rows.len(), 1), rows)
5882                        .map_err(|e| e.to_string())?,
5883                ))
5884            }
5885            _ => {
5886                let s = scalar_arg(&args[0], "hours", 1)?;
5887                Ok(Value::Duration(s * 3600.0))
5888            }
5889        },
5890        ("minutes", 1) => match &args[0] {
5891            Value::Duration(s) => Ok(Value::Scalar(*s / 60.0)),
5892            Value::DurationArray(v) => {
5893                let rows: Vec<f64> = v.iter().map(|s| s / 60.0).collect();
5894                Ok(Value::Matrix(
5895                    ndarray::Array2::from_shape_vec((rows.len(), 1), rows)
5896                        .map_err(|e| e.to_string())?,
5897                ))
5898            }
5899            _ => {
5900                let s = scalar_arg(&args[0], "minutes", 1)?;
5901                Ok(Value::Duration(s * 60.0))
5902            }
5903        },
5904        ("seconds", 1) => match &args[0] {
5905            Value::Duration(s) => Ok(Value::Scalar(*s)),
5906            Value::DurationArray(v) => {
5907                let rows = v.to_vec();
5908                Ok(Value::Matrix(
5909                    ndarray::Array2::from_shape_vec((rows.len(), 1), rows)
5910                        .map_err(|e| e.to_string())?,
5911                ))
5912            }
5913            _ => {
5914                let s = scalar_arg(&args[0], "seconds", 1)?;
5915                Ok(Value::Duration(s))
5916            }
5917        },
5918        ("days", 1) => match &args[0] {
5919            Value::Duration(s) => Ok(Value::Scalar(*s / 86400.0)),
5920            Value::DurationArray(v) => {
5921                let rows: Vec<f64> = v.iter().map(|s| s / 86400.0).collect();
5922                Ok(Value::Matrix(
5923                    ndarray::Array2::from_shape_vec((rows.len(), 1), rows)
5924                        .map_err(|e| e.to_string())?,
5925                ))
5926            }
5927            _ => {
5928                let s = scalar_arg(&args[0], "days", 1)?;
5929                Ok(Value::Duration(s * 86400.0))
5930            }
5931        },
5932        ("milliseconds", 1) => match &args[0] {
5933            Value::Duration(s) => Ok(Value::Scalar(*s * 1000.0)),
5934            Value::DurationArray(v) => {
5935                let rows: Vec<f64> = v.iter().map(|s| s * 1000.0).collect();
5936                Ok(Value::Matrix(
5937                    ndarray::Array2::from_shape_vec((rows.len(), 1), rows)
5938                        .map_err(|e| e.to_string())?,
5939                ))
5940            }
5941            _ => {
5942                let s = scalar_arg(&args[0], "milliseconds", 1)?;
5943                Ok(Value::Duration(s / 1000.0))
5944            }
5945        },
5946        ("years", 1) => match &args[0] {
5947            Value::Duration(s) => Ok(Value::Scalar(*s / (365.2425 * 86400.0))),
5948            Value::DurationArray(v) => {
5949                let rows: Vec<f64> = v.iter().map(|s| s / (365.2425 * 86400.0)).collect();
5950                Ok(Value::Matrix(
5951                    ndarray::Array2::from_shape_vec((rows.len(), 1), rows)
5952                        .map_err(|e| e.to_string())?,
5953                ))
5954            }
5955            _ => {
5956                let s = scalar_arg(&args[0], "years", 1)?;
5957                Ok(Value::Duration(s * 365.2425 * 86400.0))
5958            }
5959        },
5960        // duration(H, M, S)
5961        ("duration", 3) => {
5962            let h = scalar_arg(&args[0], "duration", 1)?;
5963            let m = scalar_arg(&args[1], "duration", 2)?;
5964            let s = scalar_arg(&args[2], "duration", 3)?;
5965            Ok(Value::Duration(h * 3600.0 + m * 60.0 + s))
5966        }
5967
5968        // ── Formatting and conversion ─────────────────────────────────────────
5969        ("datestr", 1) => match &args[0] {
5970            Value::DateTime(ts) => {
5971                let s = crate::datetime::format_datestr(*ts, "dd-MMM-yyyy HH:mm:ss");
5972                Ok(Value::Str(s))
5973            }
5974            Value::DateTimeArray(v) => Ok(Value::Cell(
5975                v.iter()
5976                    .map(|ts| {
5977                        Value::Str(crate::datetime::format_datestr(*ts, "dd-MMM-yyyy HH:mm:ss"))
5978                    })
5979                    .collect(),
5980            )),
5981            _ => Err("datestr: argument must be a datetime".to_string()),
5982        },
5983        ("datestr", 2) => {
5984            let fmt_str = match &args[1] {
5985                Value::Str(s) | Value::StringObj(s) => s.clone(),
5986                _ => return Err("datestr: second argument must be a format string".to_string()),
5987            };
5988            match &args[0] {
5989                Value::DateTime(ts) => {
5990                    Ok(Value::Str(crate::datetime::format_datestr(*ts, &fmt_str)))
5991                }
5992                Value::DateTimeArray(v) => Ok(Value::Cell(
5993                    v.iter()
5994                        .map(|ts| Value::Str(crate::datetime::format_datestr(*ts, &fmt_str)))
5995                        .collect(),
5996                )),
5997                _ => Err("datestr: first argument must be a datetime".to_string()),
5998            }
5999        }
6000        ("datevec", 1) => match &args[0] {
6001            Value::DateTime(ts) => {
6002                let (y, mo, d, h, mi, s) = crate::datetime::timestamp_to_civil(*ts);
6003                let sec_i = s.floor() as u32;
6004                let data = vec![
6005                    y as f64,
6006                    mo as f64,
6007                    d as f64,
6008                    h as f64,
6009                    mi as f64,
6010                    sec_i as f64,
6011                ];
6012                Ok(Value::Matrix(
6013                    ndarray::Array2::from_shape_vec((1, 6), data).map_err(|e| e.to_string())?,
6014                ))
6015            }
6016            _ => Err("datevec: argument must be a datetime".to_string()),
6017        },
6018        ("datenum", 1) => match &args[0] {
6019            Value::DateTime(ts) => Ok(Value::Scalar(crate::datetime::to_datenum(*ts))),
6020            _ => Err("datenum: argument must be a datetime".to_string()),
6021        },
6022        ("datenum", 3) => {
6023            let y = scalar_arg(&args[0], "datenum", 1)? as i64;
6024            let mo = scalar_arg(&args[1], "datenum", 2)? as u32;
6025            let d = scalar_arg(&args[2], "datenum", 3)? as u32;
6026            let ts = crate::datetime::civil_to_timestamp(y, mo, d, 0, 0, 0.0);
6027            Ok(Value::Scalar(crate::datetime::to_datenum(ts)))
6028        }
6029        ("posixtime", 1) => match &args[0] {
6030            Value::DateTime(ts) => Ok(Value::Scalar(*ts)),
6031            _ => Err("posixtime: argument must be a datetime".to_string()),
6032        },
6033
6034        // ── diff for datetime/duration arrays ─────────────────────────────────
6035        ("diff", 1) => match &args[0] {
6036            Value::DateTimeArray(v) if v.len() >= 2 => {
6037                let diffs: Vec<f64> = v.windows(2).map(|w| w[1] - w[0]).collect();
6038                Ok(Value::DurationArray(diffs))
6039            }
6040            Value::DurationArray(v) if v.len() >= 2 => {
6041                let diffs: Vec<f64> = v.windows(2).map(|w| w[1] - w[0]).collect();
6042                Ok(Value::DurationArray(diffs))
6043            }
6044            Value::Matrix(m) => {
6045                // diff on numeric matrix: successive differences along first non-singleton dim
6046                let (nrows, ncols) = (m.nrows(), m.ncols());
6047                if ncols > 1 && nrows == 1 {
6048                    // Row vector → diff along columns
6049                    let data: Vec<f64> =
6050                        (0..ncols - 1).map(|j| m[[0, j + 1]] - m[[0, j]]).collect();
6051                    Ok(Value::Matrix(
6052                        ndarray::Array2::from_shape_vec((1, data.len()), data)
6053                            .map_err(|e| e.to_string())?,
6054                    ))
6055                } else if nrows > 1 {
6056                    // Column vector or matrix → diff along rows
6057                    let data: Vec<f64> = (0..nrows - 1)
6058                        .flat_map(|i| (0..ncols).map(move |j| m[[i + 1, j]] - m[[i, j]]))
6059                        .collect();
6060                    Ok(Value::Matrix(
6061                        ndarray::Array2::from_shape_vec((nrows - 1, ncols), data)
6062                            .map_err(|e| e.to_string())?,
6063                    ))
6064                } else {
6065                    Err("diff: input must have at least 2 elements".to_string())
6066                }
6067            }
6068            _ => Err("diff: unsupported argument type".to_string()),
6069        },
6070
6071        // ── Phase 23a — Matrix shape utilities ───────────────────────────────
6072        ("triu", 1) => match &args[0] {
6073            Value::Matrix(m) => {
6074                let mut r = m.clone();
6075                for i in 0..m.nrows() {
6076                    for j in 0..m.ncols() {
6077                        if (j as isize) < (i as isize) {
6078                            r[[i, j]] = 0.0;
6079                        }
6080                    }
6081                }
6082                Ok(Value::Matrix(r))
6083            }
6084            Value::Scalar(n) => Ok(Value::Scalar(*n)),
6085            _ => Err("triu: argument must be a numeric matrix".to_string()),
6086        },
6087        ("triu", 2) => match (&args[0], &args[1]) {
6088            (Value::Matrix(m), Value::Scalar(k)) => {
6089                let k = *k as isize;
6090                let mut r = m.clone();
6091                for i in 0..m.nrows() {
6092                    for j in 0..m.ncols() {
6093                        if (j as isize) - (i as isize) < k {
6094                            r[[i, j]] = 0.0;
6095                        }
6096                    }
6097                }
6098                Ok(Value::Matrix(r))
6099            }
6100            _ => Err("triu: expects (matrix, scalar)".to_string()),
6101        },
6102
6103        ("tril", 1) => match &args[0] {
6104            Value::Matrix(m) => {
6105                let mut r = m.clone();
6106                for i in 0..m.nrows() {
6107                    for j in 0..m.ncols() {
6108                        if (j as isize) > (i as isize) {
6109                            r[[i, j]] = 0.0;
6110                        }
6111                    }
6112                }
6113                Ok(Value::Matrix(r))
6114            }
6115            Value::Scalar(n) => Ok(Value::Scalar(*n)),
6116            _ => Err("tril: argument must be a numeric matrix".to_string()),
6117        },
6118        ("tril", 2) => match (&args[0], &args[1]) {
6119            (Value::Matrix(m), Value::Scalar(k)) => {
6120                let k = *k as isize;
6121                let mut r = m.clone();
6122                for i in 0..m.nrows() {
6123                    for j in 0..m.ncols() {
6124                        if (j as isize) - (i as isize) > k {
6125                            r[[i, j]] = 0.0;
6126                        }
6127                    }
6128                }
6129                Ok(Value::Matrix(r))
6130            }
6131            _ => Err("tril: expects (matrix, scalar)".to_string()),
6132        },
6133
6134        ("repmat", 3) => match (&args[0], &args[1], &args[2]) {
6135            (Value::Matrix(a), Value::Scalar(rm), Value::Scalar(cn)) => {
6136                let rm = *rm as usize;
6137                let cn = *cn as usize;
6138                if rm == 0 || cn == 0 {
6139                    return Ok(Value::Matrix(Array2::zeros((0, 0))));
6140                }
6141                let row_tile: Vec<Array2<f64>> = std::iter::repeat_n(a.view(), cn)
6142                    .map(|v| v.to_owned())
6143                    .collect();
6144                let row_block = ndarray::concatenate(
6145                    ndarray::Axis(1),
6146                    &row_tile.iter().map(|m| m.view()).collect::<Vec<_>>(),
6147                )
6148                .map_err(|e| e.to_string())?;
6149                let col_tiles: Vec<Array2<f64>> = std::iter::repeat_n(row_block.view(), rm)
6150                    .map(|v| v.to_owned())
6151                    .collect();
6152                let result = ndarray::concatenate(
6153                    ndarray::Axis(0),
6154                    &col_tiles.iter().map(|m| m.view()).collect::<Vec<_>>(),
6155                )
6156                .map_err(|e| e.to_string())?;
6157                Ok(Value::Matrix(result))
6158            }
6159            (Value::Scalar(s), Value::Scalar(rm), Value::Scalar(cn)) => {
6160                let rm = *rm as usize;
6161                let cn = *cn as usize;
6162                Ok(Value::Matrix(Array2::from_elem((rm, cn), *s)))
6163            }
6164            _ => Err("repmat: expects (matrix, m, n)".to_string()),
6165        },
6166
6167        ("kron", 2) => match (&args[0], &args[1]) {
6168            (Value::Matrix(a), Value::Matrix(b)) => {
6169                let (ra, ca) = (a.nrows(), a.ncols());
6170                let (rb, cb) = (b.nrows(), b.ncols());
6171                let mut result = Array2::<f64>::zeros((ra * rb, ca * cb));
6172                for i in 0..ra {
6173                    for j in 0..ca {
6174                        let aij = a[[i, j]];
6175                        for p in 0..rb {
6176                            for q in 0..cb {
6177                                result[[i * rb + p, j * cb + q]] = aij * b[[p, q]];
6178                            }
6179                        }
6180                    }
6181                }
6182                Ok(Value::Matrix(result))
6183            }
6184            (Value::Scalar(s), Value::Matrix(b)) => Ok(Value::Matrix(b.mapv(|x| x * s))),
6185            (Value::Matrix(a), Value::Scalar(s)) => Ok(Value::Matrix(a.mapv(|x| x * s))),
6186            (Value::Scalar(a), Value::Scalar(b)) => Ok(Value::Scalar(a * b)),
6187            _ => Err("kron: arguments must be numeric matrices".to_string()),
6188        },
6189
6190        // ── Phase 30b — meshgrid ─────────────────────────────────────────────
6191        ("meshgrid", 1) => {
6192            let xv = numeric_vec(&args[0], "meshgrid")?;
6193            let n = xv.len();
6194            let x_mat = Array2::from_shape_fn((n, n), |(_r, c)| xv[c]);
6195            let y_mat = Array2::from_shape_fn((n, n), |(r, _c)| xv[r]);
6196            if get_nargout() >= 2 {
6197                Ok(Value::Tuple(vec![
6198                    Value::Matrix(x_mat),
6199                    Value::Matrix(y_mat),
6200                ]))
6201            } else {
6202                Ok(Value::Matrix(x_mat))
6203            }
6204        }
6205        ("meshgrid", 2) => {
6206            let xv = numeric_vec(&args[0], "meshgrid")?;
6207            let yv = numeric_vec(&args[1], "meshgrid")?;
6208            let n_rows = yv.len();
6209            let n_cols = xv.len();
6210            let x_mat = Array2::from_shape_fn((n_rows, n_cols), |(_r, c)| xv[c]);
6211            let y_mat = Array2::from_shape_fn((n_rows, n_cols), |(r, _c)| yv[r]);
6212            if get_nargout() >= 2 {
6213                Ok(Value::Tuple(vec![
6214                    Value::Matrix(x_mat),
6215                    Value::Matrix(y_mat),
6216                ]))
6217            } else {
6218                Ok(Value::Matrix(x_mat))
6219            }
6220        }
6221
6222        // ── Phase 23b — Vector products ──────────────────────────────────────
6223        ("cross", 2) => {
6224            fn to_vec3(v: &Value, argn: usize) -> Result<[f64; 3], String> {
6225                match v {
6226                    Value::Matrix(m) => {
6227                        let flat: Vec<f64> = m.iter().copied().collect();
6228                        if flat.len() != 3 {
6229                            Err(format!(
6230                                "cross: argument {} must have exactly 3 elements",
6231                                argn
6232                            ))
6233                        } else {
6234                            Ok([flat[0], flat[1], flat[2]])
6235                        }
6236                    }
6237                    _ => Err(format!(
6238                        "cross: argument {} must be a 3-element vector",
6239                        argn
6240                    )),
6241                }
6242            }
6243            let a = to_vec3(&args[0], 1)?;
6244            let b = to_vec3(&args[1], 2)?;
6245            let cx = a[1] * b[2] - a[2] * b[1];
6246            let cy = a[2] * b[0] - a[0] * b[2];
6247            let cz = a[0] * b[1] - a[1] * b[0];
6248            // Result orientation follows first argument
6249            let result = match &args[0] {
6250                Value::Matrix(m) if m.nrows() == 1 => {
6251                    Array2::from_shape_vec((1, 3), vec![cx, cy, cz]).unwrap()
6252                }
6253                _ => Array2::from_shape_vec((3, 1), vec![cx, cy, cz]).unwrap(),
6254            };
6255            Ok(Value::Matrix(result))
6256        }
6257
6258        ("dot", 2) => {
6259            fn to_flat(v: &Value, argn: usize) -> Result<Vec<f64>, String> {
6260                match v {
6261                    Value::Matrix(m) => Ok(m.iter().copied().collect()),
6262                    Value::Scalar(s) => Ok(vec![*s]),
6263                    _ => Err(format!("dot: argument {} must be a numeric vector", argn)),
6264                }
6265            }
6266            let a = to_flat(&args[0], 1)?;
6267            let b = to_flat(&args[1], 2)?;
6268            if a.len() != b.len() {
6269                return Err(format!(
6270                    "dot: vectors must have the same length ({} vs {})",
6271                    a.len(),
6272                    b.len()
6273                ));
6274            }
6275            let s: f64 = a.iter().zip(b.iter()).map(|(x, y)| x * y).sum();
6276            Ok(Value::Scalar(s))
6277        }
6278
6279        // ── Phase 23c — Set operations ────────────────────────────────────────
6280        ("intersect", 2) => {
6281            fn to_sorted_vec(v: &Value, fname: &str) -> Result<Vec<f64>, String> {
6282                match v {
6283                    Value::Matrix(m) => {
6284                        let mut vals: Vec<f64> = m.iter().copied().collect();
6285                        vals.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
6286                        Ok(vals)
6287                    }
6288                    Value::Scalar(s) => Ok(vec![*s]),
6289                    _ => Err(format!("{fname}: arguments must be numeric vectors")),
6290                }
6291            }
6292            let a = to_sorted_vec(&args[0], "intersect")?;
6293            let b = to_sorted_vec(&args[1], "intersect")?;
6294            let b_set: std::collections::HashSet<u64> = b
6295                .iter()
6296                .filter(|x| !x.is_nan())
6297                .map(|x| x.to_bits())
6298                .collect();
6299            let mut result: Vec<f64> = Vec::new();
6300            for x in &a {
6301                if !x.is_nan()
6302                    && b_set.contains(&x.to_bits())
6303                    && result.last().is_none_or(|&last| last != *x)
6304                {
6305                    result.push(*x);
6306                }
6307            }
6308            let n = result.len();
6309            if n == 0 {
6310                Ok(Value::Matrix(Array2::zeros((1, 0))))
6311            } else {
6312                Ok(Value::Matrix(
6313                    Array2::from_shape_vec((1, n), result).unwrap(),
6314                ))
6315            }
6316        }
6317
6318        ("union", 2) => {
6319            fn collect_vals(v: &Value, fname: &str) -> Result<Vec<f64>, String> {
6320                match v {
6321                    Value::Matrix(m) => Ok(m.iter().copied().collect()),
6322                    Value::Scalar(s) => Ok(vec![*s]),
6323                    _ => Err(format!("{fname}: arguments must be numeric vectors")),
6324                }
6325            }
6326            let mut combined = collect_vals(&args[0], "union")?;
6327            combined.extend(collect_vals(&args[1], "union")?);
6328            combined.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
6329            let mut result: Vec<f64> = Vec::new();
6330            for x in combined {
6331                if result.last().is_none_or(|&last| last != x) {
6332                    result.push(x);
6333                }
6334            }
6335            let n = result.len();
6336            if n == 0 {
6337                Ok(Value::Matrix(Array2::zeros((1, 0))))
6338            } else {
6339                Ok(Value::Matrix(
6340                    Array2::from_shape_vec((1, n), result).unwrap(),
6341                ))
6342            }
6343        }
6344
6345        ("setdiff", 2) => {
6346            fn collect_vals2(v: &Value, fname: &str) -> Result<Vec<f64>, String> {
6347                match v {
6348                    Value::Matrix(m) => Ok(m.iter().copied().collect()),
6349                    Value::Scalar(s) => Ok(vec![*s]),
6350                    _ => Err(format!("{fname}: arguments must be numeric vectors")),
6351                }
6352            }
6353            let a = collect_vals2(&args[0], "setdiff")?;
6354            let b = collect_vals2(&args[1], "setdiff")?;
6355            let b_set: std::collections::HashSet<u64> = b
6356                .iter()
6357                .filter(|x| !x.is_nan())
6358                .map(|x| x.to_bits())
6359                .collect();
6360            let mut a_sorted = a.clone();
6361            a_sorted.sort_by(|x, y| x.partial_cmp(y).unwrap_or(std::cmp::Ordering::Equal));
6362            let mut result: Vec<f64> = Vec::new();
6363            for x in a_sorted {
6364                if !x.is_nan()
6365                    && !b_set.contains(&x.to_bits())
6366                    && result.last().is_none_or(|&last| last != x)
6367                {
6368                    result.push(x);
6369                }
6370            }
6371            let n = result.len();
6372            if n == 0 {
6373                Ok(Value::Matrix(Array2::zeros((1, 0))))
6374            } else {
6375                Ok(Value::Matrix(
6376                    Array2::from_shape_vec((1, n), result).unwrap(),
6377                ))
6378            }
6379        }
6380
6381        ("ismember", 2) => {
6382            fn collect_vals3(v: &Value, fname: &str) -> Result<Vec<f64>, String> {
6383                match v {
6384                    Value::Matrix(m) => Ok(m.iter().copied().collect()),
6385                    Value::Scalar(s) => Ok(vec![*s]),
6386                    _ => Err(format!("{fname}: arguments must be numeric")),
6387                }
6388            }
6389            let set: std::collections::HashSet<u64> = collect_vals3(&args[1], "ismember")?
6390                .into_iter()
6391                .filter(|x| !x.is_nan())
6392                .map(|x| x.to_bits())
6393                .collect();
6394            match &args[0] {
6395                Value::Scalar(s) => {
6396                    let found = !s.is_nan() && set.contains(&s.to_bits());
6397                    Ok(Value::Scalar(if found { 1.0 } else { 0.0 }))
6398                }
6399                Value::Matrix(m) => {
6400                    let result: Vec<f64> = m
6401                        .iter()
6402                        .map(|x| {
6403                            if !x.is_nan() && set.contains(&x.to_bits()) {
6404                                1.0
6405                            } else {
6406                                0.0
6407                            }
6408                        })
6409                        .collect();
6410                    let shape = m.raw_dim();
6411                    Ok(Value::Matrix(
6412                        Array2::from_shape_vec(shape, result).unwrap(),
6413                    ))
6414                }
6415                _ => Err("ismember: first argument must be numeric".to_string()),
6416            }
6417        }
6418
6419        // ── Phase 23d — Index utilities and element repetition ────────────────
6420        ("sub2ind", 3) => {
6421            let sz = match &args[0] {
6422                Value::Matrix(m) if m.len() == 2 => (m[[0, 0]] as usize, m[[0, 1]] as usize),
6423                _ => return Err("sub2ind: first argument must be [rows cols]".to_string()),
6424            };
6425            let rows = sz.0;
6426            fn idx_vals(v: &Value, argn: usize) -> Result<Vec<f64>, String> {
6427                match v {
6428                    Value::Scalar(s) => Ok(vec![*s]),
6429                    Value::Matrix(m) => Ok(m.iter().copied().collect()),
6430                    _ => Err(format!("sub2ind: argument {} must be numeric", argn)),
6431                }
6432            }
6433            let r = idx_vals(&args[1], 2)?;
6434            let c = idx_vals(&args[2], 3)?;
6435            if r.len() != c.len() {
6436                return Err(
6437                    "sub2ind: row and column index vectors must have the same length".to_string(),
6438                );
6439            }
6440            if r.len() == 1 {
6441                let idx = (c[0] as usize - 1) * rows + r[0] as usize;
6442                Ok(Value::Scalar(idx as f64))
6443            } else {
6444                let vals: Vec<f64> = r
6445                    .iter()
6446                    .zip(c.iter())
6447                    .map(|(&ri, &ci)| ((ci as usize - 1) * rows + ri as usize) as f64)
6448                    .collect();
6449                let n = vals.len();
6450                Ok(Value::Matrix(Array2::from_shape_vec((1, n), vals).unwrap()))
6451            }
6452        }
6453
6454        ("ind2sub", 2) => {
6455            let sz = match &args[0] {
6456                Value::Matrix(m) if m.len() == 2 => (m[[0, 0]] as usize, m[[0, 1]] as usize),
6457                _ => return Err("ind2sub: first argument must be [rows cols]".to_string()),
6458            };
6459            let rows = sz.0;
6460            fn idx_vals2(v: &Value, argn: usize) -> Result<Vec<f64>, String> {
6461                match v {
6462                    Value::Scalar(s) => Ok(vec![*s]),
6463                    Value::Matrix(m) => Ok(m.iter().copied().collect()),
6464                    _ => Err(format!("ind2sub: argument {} must be numeric", argn)),
6465                }
6466            }
6467            let indices = idx_vals2(&args[1], 2)?;
6468            if indices.len() == 1 {
6469                let idx = indices[0] as usize;
6470                let r = ((idx - 1) % rows + 1) as f64;
6471                let c = ((idx - 1) / rows + 1) as f64;
6472                Ok(Value::Tuple(vec![Value::Scalar(r), Value::Scalar(c)]))
6473            } else {
6474                let n = indices.len();
6475                let rs: Vec<f64> = indices
6476                    .iter()
6477                    .map(|&idx| ((idx as usize - 1) % rows + 1) as f64)
6478                    .collect();
6479                let cs: Vec<f64> = indices
6480                    .iter()
6481                    .map(|&idx| ((idx as usize - 1) / rows + 1) as f64)
6482                    .collect();
6483                let rm = Value::Matrix(Array2::from_shape_vec((1, n), rs).unwrap());
6484                let cm = Value::Matrix(Array2::from_shape_vec((1, n), cs).unwrap());
6485                Ok(Value::Tuple(vec![rm, cm]))
6486            }
6487        }
6488
6489        ("repelem", 2) => match (&args[0], &args[1]) {
6490            (Value::Matrix(a), Value::Scalar(n)) => {
6491                let n = *n as usize;
6492                let flat: Vec<f64> = a.iter().flat_map(|&x| std::iter::repeat_n(x, n)).collect();
6493                let total = flat.len();
6494                Ok(Value::Matrix(
6495                    Array2::from_shape_vec((1, total), flat).unwrap(),
6496                ))
6497            }
6498            (Value::Matrix(a), Value::Matrix(ns)) => {
6499                let av: Vec<f64> = a.iter().copied().collect();
6500                let nv: Vec<f64> = ns.iter().copied().collect();
6501                if av.len() != nv.len() {
6502                    return Err(
6503                        "repelem: element count vector must match source vector length".to_string(),
6504                    );
6505                }
6506                let flat: Vec<f64> = av
6507                    .iter()
6508                    .zip(nv.iter())
6509                    .flat_map(|(&x, &n)| std::iter::repeat_n(x, n as usize))
6510                    .collect();
6511                let total = flat.len();
6512                Ok(Value::Matrix(
6513                    Array2::from_shape_vec((1, total), flat).unwrap(),
6514                ))
6515            }
6516            (Value::Scalar(s), Value::Scalar(n)) => {
6517                let n = *n as usize;
6518                Ok(Value::Matrix(Array2::from_elem((1, n), *s)))
6519            }
6520            _ => Err("repelem: unsupported argument types".to_string()),
6521        },
6522        ("repelem", 3) => match (&args[0], &args[1], &args[2]) {
6523            (Value::Matrix(a), Value::Scalar(rm), Value::Scalar(cn)) => {
6524                let rm = *rm as usize;
6525                let cn = *cn as usize;
6526                let (nrows, ncols) = (a.nrows(), a.ncols());
6527                let mut result = Array2::<f64>::zeros((nrows * rm, ncols * cn));
6528                for i in 0..nrows {
6529                    for j in 0..ncols {
6530                        let v = a[[i, j]];
6531                        for di in 0..rm {
6532                            for dj in 0..cn {
6533                                result[[i * rm + di, j * cn + dj]] = v;
6534                            }
6535                        }
6536                    }
6537                }
6538                Ok(Value::Matrix(result))
6539            }
6540            (Value::Scalar(s), Value::Scalar(rm), Value::Scalar(cn)) => Ok(Value::Matrix(
6541                Array2::from_elem((*rm as usize, *cn as usize), *s),
6542            )),
6543            _ => Err("repelem: expects (matrix, m, n) for 2D repetition".to_string()),
6544        },
6545
6546        // ── Phase 24a — Polynomial evaluation, fitting, and roots ────────────
6547        ("polyval", 2) => {
6548            let coeffs = poly_coeffs(&args[0], "polyval")?;
6549            if coeffs.is_empty() {
6550                return Err("polyval: polynomial vector is empty".to_string());
6551            }
6552            match &args[1] {
6553                Value::Scalar(x) => Ok(Value::Scalar(horner(&coeffs, *x))),
6554                Value::Matrix(m) => Ok(Value::Matrix(m.mapv(|x| horner(&coeffs, x)))),
6555                _ => Err("polyval: second argument must be a real numeric value".to_string()),
6556            }
6557        }
6558
6559        ("polyfit", 3) => {
6560            let xv = poly_coeffs(&args[0], "polyfit")?;
6561            let yv = poly_coeffs(&args[1], "polyfit")?;
6562            let deg = match &args[2] {
6563                Value::Scalar(n) => {
6564                    let d = *n as usize;
6565                    if *n < 0.0 || (*n - d as f64).abs() > 1e-9 {
6566                        return Err("polyfit: degree must be a non-negative integer".to_string());
6567                    }
6568                    d
6569                }
6570                _ => return Err("polyfit: degree must be a scalar".to_string()),
6571            };
6572            if xv.len() != yv.len() {
6573                return Err("polyfit: x and y must have the same length".to_string());
6574            }
6575            let m = xv.len();
6576            let ncols = deg + 1;
6577            if ncols > m {
6578                return Err(format!(
6579                    "polyfit: not enough data points ({m}) for degree-{deg} fit"
6580                ));
6581            }
6582            // Build Vandermonde matrix (m × ncols), highest power first
6583            let mut vander = Array2::<f64>::zeros((m, ncols));
6584            for (i, &xi) in xv.iter().enumerate() {
6585                for j in 0..ncols {
6586                    vander[[i, j]] = xi.powi((deg - j) as i32);
6587                }
6588            }
6589            // Solve via QR: V*c = y  →  Q*R*c = y  →  R*c = Q^T*y
6590            let (q, r) = qr_decompose(&vander)?;
6591            let qty: Vec<f64> = (0..ncols)
6592                .map(|i| (0..m).map(|k| q[[k, i]] * yv[k]).sum())
6593                .collect();
6594            // Extract upper-left ncols×ncols block of R
6595            let mut r_sq = Array2::<f64>::zeros((ncols, ncols));
6596            for i in 0..ncols {
6597                for j in 0..ncols {
6598                    r_sq[[i, j]] = r[[i, j]];
6599                }
6600            }
6601            let coeffs = poly_back_sub(&r_sq, &qty)?;
6602            let result = Array2::from_shape_vec((1, ncols), coeffs)
6603                .map_err(|e| format!("polyfit: internal error: {e}"))?;
6604            Ok(Value::Matrix(result))
6605        }
6606
6607        ("roots", 1) => {
6608            let raw = poly_coeffs(&args[0], "roots")?;
6609            // Strip leading zeros
6610            let start = raw.iter().position(|&c| c != 0.0).unwrap_or(raw.len());
6611            let coeffs = &raw[start..];
6612            if coeffs.len() <= 1 {
6613                return Ok(Value::Matrix(Array2::zeros((0, 1))));
6614            }
6615            let roots = durand_kerner(coeffs)?;
6616            Ok(roots_to_value(&roots))
6617        }
6618
6619        ("poly", 1) => match &args[0] {
6620            Value::Scalar(r) => {
6621                let data = vec![1.0, -*r];
6622                Ok(Value::Matrix(Array2::from_shape_vec((1, 2), data).unwrap()))
6623            }
6624            Value::Matrix(m) => {
6625                if m.nrows() == 1 || m.ncols() == 1 {
6626                    // Root vector: expand (x − r_1)(x − r_2)…
6627                    let roots: Vec<f64> = if m.nrows() == 1 {
6628                        m.row(0).iter().copied().collect()
6629                    } else {
6630                        m.column(0).iter().copied().collect()
6631                    };
6632                    let mut p = vec![1.0_f64];
6633                    for &r in &roots {
6634                        p = poly_conv(&p, &[1.0, -r]);
6635                    }
6636                    let ncols = p.len();
6637                    Ok(Value::Matrix(
6638                        Array2::from_shape_vec((1, ncols), p).unwrap(),
6639                    ))
6640                } else {
6641                    // Square matrix: characteristic polynomial via Faddeev-LeVerrier
6642                    let coeffs = characteristic_poly(m)?;
6643                    let ncols = coeffs.len();
6644                    Ok(Value::Matrix(
6645                        Array2::from_shape_vec((1, ncols), coeffs).unwrap(),
6646                    ))
6647                }
6648            }
6649            _ => Err("poly: argument must be a numeric vector or square matrix".to_string()),
6650        },
6651
6652        // ── Phase 24b — Convolution, deconvolution, interpolation ────────────
6653        ("conv", 2) => {
6654            let a = poly_coeffs(&args[0], "conv")?;
6655            let b = poly_coeffs(&args[1], "conv")?;
6656            if a.is_empty() || b.is_empty() {
6657                return Ok(Value::Matrix(Array2::zeros((1, 0))));
6658            }
6659            let c = poly_conv(&a, &b);
6660            let len = c.len();
6661            Ok(Value::Matrix(Array2::from_shape_vec((1, len), c).unwrap()))
6662        }
6663
6664        ("deconv", 2) => {
6665            let c = poly_coeffs(&args[0], "deconv")?;
6666            let b = poly_coeffs(&args[1], "deconv")?;
6667            let (q, r) = poly_deconv(&c, &b)?;
6668            let qn = q.len();
6669            let rn = r.len();
6670            let q_val = Value::Matrix(Array2::from_shape_vec((1, qn), q).unwrap());
6671            let r_val = Value::Matrix(Array2::from_shape_vec((1, rn), r).unwrap());
6672            Ok(Value::Tuple(vec![q_val, r_val]))
6673        }
6674
6675        ("interp1", 3) => {
6676            let xv = poly_coeffs(&args[0], "interp1")?;
6677            let yv = poly_coeffs(&args[1], "interp1")?;
6678            if xv.len() != yv.len() {
6679                return Err("interp1: x and y must have the same length".to_string());
6680            }
6681            if xv.len() < 2 {
6682                return Err("interp1: requires at least two knot points".to_string());
6683            }
6684            match &args[2] {
6685                Value::Scalar(xi) => Ok(Value::Scalar(interp1_at(&xv, &yv, *xi, "linear"))),
6686                Value::Matrix(xi_m) => Ok(Value::Matrix(
6687                    xi_m.mapv(|xi| interp1_at(&xv, &yv, xi, "linear")),
6688                )),
6689                _ => Err("interp1: query points must be numeric".to_string()),
6690            }
6691        }
6692
6693        ("interp1", 4) => {
6694            let xv = poly_coeffs(&args[0], "interp1")?;
6695            let yv = poly_coeffs(&args[1], "interp1")?;
6696            let method = match &args[3] {
6697                Value::Str(s) | Value::StringObj(s) => s.clone(),
6698                _ => return Err("interp1: method argument must be a string".to_string()),
6699            };
6700            if !matches!(method.as_str(), "linear" | "nearest" | "previous" | "next") {
6701                return Err(format!(
6702                    "interp1: unknown method '{method}'; supported: linear nearest previous next"
6703                ));
6704            }
6705            if xv.len() != yv.len() {
6706                return Err("interp1: x and y must have the same length".to_string());
6707            }
6708            if xv.len() < 2 {
6709                return Err("interp1: requires at least two knot points".to_string());
6710            }
6711            match &args[2] {
6712                Value::Scalar(xi) => Ok(Value::Scalar(interp1_at(&xv, &yv, *xi, &method))),
6713                Value::Matrix(xi_m) => {
6714                    let m_str = method.as_str();
6715                    Ok(Value::Matrix(
6716                        xi_m.mapv(|xi| interp1_at(&xv, &yv, xi, m_str)),
6717                    ))
6718                }
6719                _ => Err("interp1: query points must be numeric".to_string()),
6720            }
6721        }
6722
6723        // ── 25b: tic / toc ────────────────────────────────────────────────────
6724        ("tic", 0) => {
6725            TIC_TIME.with(|t| t.set(Some(std::time::Instant::now())));
6726            Ok(Value::Void)
6727        }
6728        ("toc", 0) => {
6729            let elapsed = TIC_TIME.with(|t| t.get().map(|s| s.elapsed().as_secs_f64()));
6730            match elapsed {
6731                Some(t) => Ok(Value::Scalar(t)),
6732                None => Err("toc: tic must be called before toc".to_string()),
6733            }
6734        }
6735
6736        // ── 25a: eval (expression context — env mutations do not persist) ────
6737        ("eval", 1) => {
6738            let code = match &args[0] {
6739                Value::Str(s) | Value::StringObj(s) => s.clone(),
6740                _ => return Err("eval: argument must be a string".to_string()),
6741            };
6742            call_eval_str_hook(&code, env)
6743        }
6744        ("eval", 2) => {
6745            let code = match &args[0] {
6746                Value::Str(s) | Value::StringObj(s) => s.clone(),
6747                _ => return Err("eval: argument must be a string".to_string()),
6748            };
6749            match call_eval_str_hook(&code, env) {
6750                Err(e) => {
6751                    set_last_err(&e);
6752                    let catch = match &args[1] {
6753                        Value::Str(s) | Value::StringObj(s) => s.clone(),
6754                        _ => return Err("eval: catch argument must be a string".to_string()),
6755                    };
6756                    call_eval_str_hook(&catch, env)
6757                }
6758                ok => ok,
6759            }
6760        }
6761
6762        _ => {
6763            let hint = suggest_similar(name, env);
6764            match hint {
6765                Some(s) => Err(format!("Unknown function '{name}'; did you mean '{s}'?")),
6766                None => Err(format!("Unknown function: '{name}'")),
6767            }
6768        }
6769    }
6770}
6771
6772/// Interprets backslash escape sequences in delimiter strings.
6773/// `\t` → tab, `\n` → newline. Other strings are used as-is.
6774fn interpret_delim(s: &str) -> String {
6775    match s {
6776        r"\t" => "\t".to_string(),
6777        r"\n" => "\n".to_string(),
6778        other => other.to_string(),
6779    }
6780}
6781
6782/// Returns true if splitting every line by `delim` gives the same field count > 1.
6783fn delim_consistent(lines: &[&str], delim: char) -> bool {
6784    let counts: Vec<usize> = lines.iter().map(|l| l.split(delim).count()).collect();
6785    counts.iter().all(|&c| c > 1) && counts.windows(2).all(|w| w[0] == w[1])
6786}
6787
6788/// Reads a delimiter-separated numeric file and returns a `Value::Matrix`.
6789fn dlmread_impl(path: &str, explicit_delim: Option<String>) -> Result<Value, String> {
6790    let content =
6791        std::fs::read_to_string(path).map_err(|e| format!("dlmread: cannot read '{path}': {e}"))?;
6792
6793    let lines: Vec<&str> = content.lines().filter(|l| !l.trim().is_empty()).collect();
6794
6795    if lines.is_empty() {
6796        return Ok(Value::Matrix(Array2::zeros((0, 0))));
6797    }
6798
6799    // Determine delimiter: explicit → auto-detect (comma → tab → whitespace)
6800    let delim: Option<String> = match explicit_delim {
6801        Some(d) => Some(d),
6802        None => {
6803            if delim_consistent(&lines, ',') {
6804                Some(",".to_string())
6805            } else if delim_consistent(&lines, '\t') {
6806                Some("\t".to_string())
6807            } else {
6808                None // split by whitespace
6809            }
6810        }
6811    };
6812
6813    let mut rows: Vec<Vec<f64>> = Vec::new();
6814    for (line_num, line) in lines.iter().enumerate() {
6815        let fields: Vec<&str> = match &delim {
6816            Some(d) => line.split(d.as_str()).collect(),
6817            None => line.split_whitespace().collect(),
6818        };
6819        let mut row_vals: Vec<f64> = Vec::with_capacity(fields.len());
6820        for field in &fields {
6821            let trimmed = field.trim();
6822            if trimmed.is_empty() {
6823                row_vals.push(0.0);
6824            } else {
6825                row_vals.push(trimmed.parse::<f64>().map_err(|_| {
6826                    format!(
6827                        "dlmread: non-numeric value '{trimmed}' on line {}",
6828                        line_num + 1
6829                    )
6830                })?);
6831            }
6832        }
6833        if !row_vals.is_empty() {
6834            rows.push(row_vals);
6835        }
6836    }
6837
6838    if rows.is_empty() {
6839        return Ok(Value::Matrix(Array2::zeros((0, 0))));
6840    }
6841
6842    let ncols = rows[0].len();
6843    for (i, row) in rows.iter().enumerate() {
6844        if row.len() != ncols {
6845            return Err(format!(
6846                "dlmread: row {} has {} fields, expected {ncols}",
6847                i + 1,
6848                row.len()
6849            ));
6850        }
6851    }
6852
6853    let nrows = rows.len();
6854    let flat: Vec<f64> = rows.into_iter().flatten().collect();
6855    Array2::from_shape_vec((nrows, ncols), flat)
6856        .map_err(|e| format!("dlmread: shape error: {e}"))
6857        .map(Value::Matrix)
6858}
6859
6860/// Formats one f64 value for use in a delimited file.
6861/// Integers are written without decimal point; floats use full precision.
6862fn fmt_dlm_number(n: f64) -> String {
6863    if n.is_finite() && n == n.trunc() && n.abs() < 1e15 {
6864        format!("{}", n as i64)
6865    } else {
6866        format!("{n}")
6867    }
6868}
6869
6870/// Writes a scalar or matrix to a delimiter-separated file.
6871fn dlmwrite_impl(path: &str, val: &Value, explicit_delim: Option<String>) -> Result<Value, String> {
6872    let delim = explicit_delim.unwrap_or_else(|| ",".to_string());
6873
6874    let content = match val {
6875        Value::Scalar(n) => format!("{}\n", fmt_dlm_number(*n)),
6876        Value::Matrix(m) => {
6877            let mut out = String::new();
6878            for row in m.rows() {
6879                let parts: Vec<String> = row.iter().map(|n| fmt_dlm_number(*n)).collect();
6880                out.push_str(&parts.join(&delim));
6881                out.push('\n');
6882            }
6883            out
6884        }
6885        _ => {
6886            return Err("dlmwrite: second argument must be a numeric scalar or matrix".to_string());
6887        }
6888    };
6889
6890    std::fs::write(path, content).map_err(|e| format!("dlmwrite: cannot write '{path}': {e}"))?;
6891    Ok(Value::Void)
6892}
6893
6894// --- CSV read/write helpers (readmatrix / readtable / writetable) ---
6895
6896/// Selects the delimiter shared across all lines; falls back to `None` (whitespace splitting).
6897///
6898/// Uses CSV-aware splitting (quoting) when checking for comma consistency.
6899fn auto_detect_delim(lines: &[&str]) -> Option<String> {
6900    // Comma: use CSV-aware split so quoted fields with commas don't confuse the count.
6901    let comma_counts: Vec<usize> = lines.iter().map(|l| split_csv_row(l, ",").len()).collect();
6902    if comma_counts.iter().all(|&c| c > 1) && comma_counts.windows(2).all(|w| w[0] == w[1]) {
6903        return Some(",".to_string());
6904    }
6905    if delim_consistent(lines, '\t') {
6906        Some("\t".to_string())
6907    } else {
6908        None
6909    }
6910}
6911
6912/// Splits one CSV line by `delim`, respecting RFC 4180 double-quoted fields.
6913/// `""` inside a quoted field encodes a literal `"`.
6914/// Falls back to a plain `str::split` for multi-character delimiters.
6915fn split_csv_row(line: &str, delim: &str) -> Vec<String> {
6916    if delim.chars().count() != 1 {
6917        return line.split(delim).map(str::to_string).collect();
6918    }
6919    let delim_char = delim.chars().next().unwrap();
6920    let chars: Vec<char> = line.chars().collect();
6921    let mut fields: Vec<String> = Vec::new();
6922    let mut field = String::new();
6923    let mut i = 0;
6924    let mut in_quotes = false;
6925    while i < chars.len() {
6926        let c = chars[i];
6927        if in_quotes {
6928            if c == '"' && i + 1 < chars.len() && chars[i + 1] == '"' {
6929                field.push('"');
6930                i += 2;
6931                continue;
6932            } else if c == '"' {
6933                in_quotes = false;
6934            } else {
6935                field.push(c);
6936            }
6937        } else if c == '"' {
6938            in_quotes = true;
6939        } else if c == delim_char {
6940            fields.push(std::mem::take(&mut field));
6941        } else {
6942            field.push(c);
6943        }
6944        i += 1;
6945    }
6946    fields.push(field);
6947    fields
6948}
6949
6950/// Splits a CSV row with an optional delimiter; `None` splits by whitespace.
6951fn split_csv_row_opt(line: &str, delim: &Option<String>) -> Vec<String> {
6952    match delim {
6953        None => line.split_whitespace().map(str::to_string).collect(),
6954        Some(d) => split_csv_row(line, d),
6955    }
6956}
6957
6958/// Returns `true` if any non-empty field in `fields` cannot be parsed as `f64`.
6959fn row_is_header(fields: &[String]) -> bool {
6960    fields
6961        .iter()
6962        .any(|f| !f.trim().is_empty() && f.trim().parse::<f64>().is_err())
6963}
6964
6965/// Converts a raw header string to a valid identifier-like name.
6966/// Runs of non-alphanumeric characters collapse to `_`; a leading digit gets an `x` prefix.
6967/// Empty results fall back to `x{col}`.
6968fn sanitize_header(s: &str, col_1based: usize) -> String {
6969    let s = s.trim();
6970    if s.is_empty() {
6971        return format!("x{col_1based}");
6972    }
6973    let mut out = String::new();
6974    for c in s.chars() {
6975        if c.is_alphanumeric() || c == '_' {
6976            out.push(c);
6977        } else if !out.ends_with('_') {
6978            out.push('_');
6979        }
6980    }
6981    let out = out.trim_end_matches('_').to_string();
6982    if out.is_empty() {
6983        return format!("x{col_1based}");
6984    }
6985    if out.chars().next().unwrap().is_ascii_digit() {
6986        format!("x{out}")
6987    } else {
6988        out
6989    }
6990}
6991
6992/// Appends `_N` (1-based) suffixes to duplicate entries in a header list.
6993/// Note: collisions between deduplicated names and pre-existing `_N` names are not resolved.
6994fn deduplicate_headers(headers: Vec<String>) -> Vec<String> {
6995    let mut count: HashMap<String, usize> = HashMap::new();
6996    for h in &headers {
6997        *count.entry(h.clone()).or_insert(0) += 1;
6998    }
6999    let mut seen: HashMap<String, usize> = HashMap::new();
7000    headers
7001        .into_iter()
7002        .map(|h| {
7003            if *count.get(&h).unwrap() == 1 {
7004                h
7005            } else {
7006                let idx = seen.entry(h.clone()).or_insert(0);
7007                *idx += 1;
7008                format!("{h}_{idx}")
7009            }
7010        })
7011        .collect()
7012}
7013
7014/// Parses an optional `('Delimiter', d)` argument pair starting at `args[start]`.
7015/// Returns `Ok(None)` when no extra arguments are present.
7016fn parse_delimiter_opt(
7017    fn_name: &str,
7018    args: &[Value],
7019    start: usize,
7020) -> Result<Option<String>, String> {
7021    if args.len() <= start {
7022        return Ok(None);
7023    }
7024    let key = string_arg(&args[start], fn_name, start + 1)?;
7025    if !key.eq_ignore_ascii_case("delimiter") {
7026        return Err(format!(
7027            "{fn_name}: expected 'Delimiter' option at argument {}, got '{key}'",
7028            start + 1
7029        ));
7030    }
7031    if args.len() <= start + 1 {
7032        return Err(format!("{fn_name}: 'Delimiter' option requires a value"));
7033    }
7034    let val = interpret_delim(string_arg(&args[start + 1], fn_name, start + 2)?);
7035    Ok(Some(val))
7036}
7037
7038/// Reads a delimiter-separated file and returns a [`Value::Matrix`].
7039///
7040/// Auto-detects the delimiter (comma → tab → whitespace). When the first row contains
7041/// non-numeric text it is treated as a header and skipped. Empty cells become `NaN`
7042/// (unlike [`dlmread_impl`], which uses `0.0`).
7043fn readmatrix_impl(path: &str, explicit_delim: Option<String>) -> Result<Value, String> {
7044    let content = std::fs::read_to_string(path)
7045        .map_err(|e| format!("readmatrix: cannot read '{path}': {e}"))?;
7046
7047    let lines: Vec<&str> = content.lines().filter(|l| !l.trim().is_empty()).collect();
7048    if lines.is_empty() {
7049        return Ok(Value::Matrix(Array2::<f64>::zeros((0, 0))));
7050    }
7051
7052    let delim = match explicit_delim {
7053        Some(d) => Some(d),
7054        None => auto_detect_delim(&lines),
7055    };
7056
7057    let first_fields = split_csv_row_opt(lines[0], &delim);
7058    let skip_header = row_is_header(&first_fields);
7059    let data_lines = if skip_header { &lines[1..] } else { &lines[..] };
7060
7061    if data_lines.is_empty() {
7062        return Ok(Value::Matrix(Array2::<f64>::zeros((0, 0))));
7063    }
7064
7065    let mut rows: Vec<Vec<f64>> = Vec::new();
7066    for (i, line) in data_lines.iter().enumerate() {
7067        let fields = split_csv_row_opt(line, &delim);
7068        let mut row: Vec<f64> = Vec::with_capacity(fields.len());
7069        for f in &fields {
7070            let t = f.trim();
7071            if t.is_empty() {
7072                row.push(f64::NAN);
7073            } else {
7074                row.push(t.parse::<f64>().map_err(|_| {
7075                    format!(
7076                        "readmatrix: non-numeric value '{t}' on line {}",
7077                        i + 1 + usize::from(skip_header)
7078                    )
7079                })?);
7080            }
7081        }
7082        rows.push(row);
7083    }
7084
7085    if rows.is_empty() {
7086        return Ok(Value::Matrix(Array2::<f64>::zeros((0, 0))));
7087    }
7088
7089    let ncols = rows[0].len();
7090    for (i, row) in rows.iter().enumerate() {
7091        if row.len() != ncols {
7092            return Err(format!(
7093                "readmatrix: row {} has {} fields, expected {ncols}",
7094                i + 1,
7095                row.len()
7096            ));
7097        }
7098    }
7099
7100    let nrows = rows.len();
7101    let flat: Vec<f64> = rows.into_iter().flatten().collect();
7102    Array2::from_shape_vec((nrows, ncols), flat)
7103        .map_err(|e| format!("readmatrix: shape error: {e}"))
7104        .map(Value::Matrix)
7105}
7106
7107/// Reads a delimiter-separated file with a header row and returns a [`Value::Struct`] of columns.
7108///
7109/// The first row is always treated as column headers. Numeric columns become `Matrix` (N×1);
7110/// columns with any non-numeric cell become `Cell` of [`Value::Str`].
7111/// Whitespace is trimmed from all cell values after CSV unquoting.
7112fn readtable_impl(path: &str, explicit_delim: Option<String>) -> Result<Value, String> {
7113    let content = std::fs::read_to_string(path)
7114        .map_err(|e| format!("readtable: cannot read '{path}': {e}"))?;
7115
7116    let lines: Vec<&str> = content.lines().filter(|l| !l.trim().is_empty()).collect();
7117    if lines.is_empty() {
7118        return Ok(Value::Struct(IndexMap::new()));
7119    }
7120
7121    let delim = match explicit_delim {
7122        Some(d) => Some(d),
7123        None => auto_detect_delim(&lines),
7124    };
7125
7126    let raw_headers = split_csv_row_opt(lines[0], &delim);
7127    let ncols = raw_headers.len();
7128    let headers: Vec<String> = deduplicate_headers(
7129        raw_headers
7130            .iter()
7131            .enumerate()
7132            .map(|(i, h)| sanitize_header(h.trim(), i + 1))
7133            .collect(),
7134    );
7135
7136    let data_lines = &lines[1..];
7137    if data_lines.is_empty() {
7138        let mut s: IndexMap<String, Value> = IndexMap::new();
7139        for h in &headers {
7140            s.insert(h.clone(), Value::Matrix(Array2::<f64>::zeros((0, 1))));
7141        }
7142        return Ok(Value::Struct(s));
7143    }
7144
7145    let mut all_rows: Vec<Vec<String>> = Vec::new();
7146    for (i, line) in data_lines.iter().enumerate() {
7147        let fields = split_csv_row_opt(line, &delim);
7148        if fields.len() != ncols {
7149            return Err(format!(
7150                "readtable: row {} has {} fields, expected {ncols}",
7151                i + 2,
7152                fields.len()
7153            ));
7154        }
7155        all_rows.push(fields.into_iter().map(|f| f.trim().to_string()).collect());
7156    }
7157
7158    let nrows = all_rows.len();
7159    let mut s: IndexMap<String, Value> = IndexMap::new();
7160    for col in 0..ncols {
7161        let all_numeric = all_rows.iter().all(|row| {
7162            let t = row[col].as_str();
7163            t.is_empty() || t.parse::<f64>().is_ok()
7164        });
7165        if all_numeric {
7166            let vals: Vec<f64> = all_rows
7167                .iter()
7168                .map(|row| {
7169                    let t = row[col].as_str();
7170                    if t.is_empty() {
7171                        f64::NAN
7172                    } else {
7173                        t.parse::<f64>().unwrap()
7174                    }
7175                })
7176                .collect();
7177            let col_mat = Array2::from_shape_vec((nrows, 1), vals)
7178                .map_err(|e| format!("readtable: shape error: {e}"))?;
7179            s.insert(headers[col].clone(), Value::Matrix(col_mat));
7180        } else {
7181            let vals: Vec<Value> = all_rows
7182                .iter()
7183                .map(|row| Value::Str(row[col].clone()))
7184                .collect();
7185            s.insert(headers[col].clone(), Value::Cell(vals));
7186        }
7187    }
7188    Ok(Value::Struct(s))
7189}
7190
7191/// Quotes a CSV cell if it contains the delimiter, a double-quote, or a newline (RFC 4180).
7192fn csv_quote_cell(s: &str, delim: &str) -> String {
7193    if s.contains('"') || s.contains('\n') || s.contains(delim) {
7194        let escaped = s.replace('"', "\"\"");
7195        format!("\"{escaped}\"")
7196    } else {
7197        s.to_string()
7198    }
7199}
7200
7201/// Returns the number of rows in a struct column value.
7202///
7203/// Accepts `Matrix` (N×1), `Cell`, `Scalar`, and `Str`/`StringObj` (1 row each).
7204/// Returns `None` for unsupported types or non-column matrices.
7205fn col_nrows(v: &Value) -> Option<usize> {
7206    match v {
7207        Value::Matrix(m) if m.ncols() == 1 || m.nrows() == 0 => Some(m.nrows()),
7208        Value::Cell(c) => Some(c.len()),
7209        Value::Scalar(_) => Some(1),
7210        Value::Str(_) | Value::StringObj(_) => Some(1),
7211        _ => None,
7212    }
7213}
7214
7215/// Returns the formatted CSV cell string for row `row` of a struct column value.
7216fn col_cell_str(v: &Value, row: usize, delim: &str) -> Result<String, String> {
7217    match v {
7218        Value::Matrix(m) => Ok(csv_quote_cell(&fmt_dlm_number(m[[row, 0]]), delim)),
7219        Value::Cell(c) => match &c[row] {
7220            Value::Str(s) | Value::StringObj(s) => Ok(csv_quote_cell(s, delim)),
7221            Value::Scalar(n) => Ok(csv_quote_cell(&fmt_dlm_number(*n), delim)),
7222            _ => Err(format!(
7223                "writetable: cell element at row {} has unsupported type",
7224                row + 1
7225            )),
7226        },
7227        Value::Scalar(n) => Ok(csv_quote_cell(&fmt_dlm_number(*n), delim)),
7228        Value::Str(s) | Value::StringObj(s) => Ok(csv_quote_cell(s, delim)),
7229        _ => Err(format!(
7230            "writetable: unsupported column type at row {}",
7231            row + 1
7232        )),
7233    }
7234}
7235
7236/// Writes a struct table to a delimiter-separated file with a header row.
7237///
7238/// Each struct field is one column. All fields must have the same number of rows.
7239/// Accepts `Matrix` (N×1), `Cell`, `Scalar`, and `Str`/`StringObj` columns.
7240/// Cell values that contain the delimiter, `"`, or newlines are quoted per RFC 4180.
7241fn writetable_impl(
7242    tbl: &Value,
7243    path: &str,
7244    explicit_delim: Option<String>,
7245) -> Result<Value, String> {
7246    let delim = explicit_delim.unwrap_or_else(|| ",".to_string());
7247    let fields = match tbl {
7248        Value::Struct(m) => m,
7249        _ => return Err("writetable: first argument must be a struct".to_string()),
7250    };
7251    if fields.is_empty() {
7252        std::fs::write(path, "").map_err(|e| format!("writetable: cannot write '{path}': {e}"))?;
7253        return Ok(Value::Void);
7254    }
7255
7256    let nrows = {
7257        let (first_name, first_val) = fields.iter().next().unwrap();
7258        col_nrows(first_val).ok_or_else(|| {
7259            format!("writetable: column '{first_name}' must be a Matrix (N×1), Cell, or scalar")
7260        })?
7261    };
7262    for (cname, cval) in fields.iter() {
7263        let n = col_nrows(cval).ok_or_else(|| {
7264            format!("writetable: column '{cname}' must be a Matrix (N×1), Cell, or scalar")
7265        })?;
7266        if n != nrows {
7267            return Err(format!(
7268                "writetable: column '{cname}' has {n} rows, expected {nrows}"
7269            ));
7270        }
7271    }
7272
7273    let mut out = String::new();
7274    let header_parts: Vec<String> = fields.keys().map(|k| csv_quote_cell(k, &delim)).collect();
7275    out.push_str(&header_parts.join(&delim));
7276    out.push('\n');
7277
7278    for row in 0..nrows {
7279        let mut parts: Vec<String> = Vec::with_capacity(fields.len());
7280        for cval in fields.values() {
7281            parts.push(col_cell_str(cval, row, &delim)?);
7282        }
7283        out.push_str(&parts.join(&delim));
7284        out.push('\n');
7285    }
7286
7287    std::fs::write(path, out).map_err(|e| format!("writetable: cannot write '{path}': {e}"))?;
7288    Ok(Value::Void)
7289}
7290
7291/// Converts an f64 to u64 for bitwise operations.
7292/// Requires a non-negative integer value; returns an error otherwise.
7293fn to_bits(v: f64, fname: &str, pos: usize) -> Result<u64, String> {
7294    if v < 0.0 {
7295        return Err(format!(
7296            "{fname}: argument {pos} must be non-negative, got {v}"
7297        ));
7298    }
7299    if v.fract() != 0.0 {
7300        return Err(format!(
7301            "{fname}: argument {pos} must be an integer, got {v}"
7302        ));
7303    }
7304    if v > u64::MAX as f64 {
7305        return Err(format!(
7306            "{fname}: argument {pos} is too large for bitwise operations"
7307        ));
7308    }
7309    Ok(v as u64)
7310}
7311
7312/// Computes determinant of a square matrix via Gaussian elimination.
7313/// Computes the determinant of a square matrix via Gaussian elimination with
7314/// partial pivoting (pure Rust, no external dependencies).
7315fn det_matrix(m: &Array2<f64>) -> Result<f64, String> {
7316    let n = m.nrows();
7317    if m.ncols() != n {
7318        return Err("det: matrix must be square".to_string());
7319    }
7320    if n == 0 {
7321        return Ok(1.0);
7322    }
7323    let mut a = m.clone();
7324    let mut sign: f64 = 1.0;
7325    for col in 0..n {
7326        // Partial pivoting: swap in the row with the largest absolute value.
7327        let pivot = (col..n)
7328            .max_by(|&r1, &r2| a[[r1, col]].abs().partial_cmp(&a[[r2, col]].abs()).unwrap())
7329            .unwrap();
7330        if a[[pivot, col]].abs() < 1e-15 {
7331            return Ok(0.0); // singular
7332        }
7333        if pivot != col {
7334            for j in 0..n {
7335                let tmp = a[[pivot, j]];
7336                a[[pivot, j]] = a[[col, j]];
7337                a[[col, j]] = tmp;
7338            }
7339            sign = -sign;
7340        }
7341        let pv = a[[col, col]];
7342        for row in (col + 1)..n {
7343            let factor = a[[row, col]] / pv;
7344            for j in col..n {
7345                let val = a[[col, j]] * factor;
7346                a[[row, j]] -= val;
7347            }
7348        }
7349    }
7350    Ok(sign * (0..n).map(|i| a[[i, i]]).product::<f64>())
7351}
7352
7353/// Computes the inverse of a square matrix via Gauss-Jordan elimination with
7354/// partial pivoting (pure Rust, no external dependencies).
7355fn inv_matrix(m: &Array2<f64>) -> Result<Array2<f64>, String> {
7356    let n = m.nrows();
7357    if m.ncols() != n {
7358        return Err("inv: matrix must be square".to_string());
7359    }
7360    let cols = 2 * n;
7361    let mut aug = vec![0.0f64; n * cols];
7362    for i in 0..n {
7363        for j in 0..n {
7364            aug[i * cols + j] = m[[i, j]];
7365        }
7366        aug[i * cols + n + i] = 1.0;
7367    }
7368    for col in 0..n {
7369        // Partial pivoting: swap in the row with the largest absolute value.
7370        let pivot = (col..n)
7371            .max_by(|&r1, &r2| {
7372                aug[r1 * cols + col]
7373                    .abs()
7374                    .partial_cmp(&aug[r2 * cols + col].abs())
7375                    .unwrap()
7376            })
7377            .filter(|&r| aug[r * cols + col].abs() > 1e-12)
7378            .ok_or_else(|| "inv: matrix is singular".to_string())?;
7379        if pivot != col {
7380            for j in 0..cols {
7381                aug.swap(col * cols + j, pivot * cols + j);
7382            }
7383        }
7384        let pv = aug[col * cols + col];
7385        for j in 0..cols {
7386            aug[col * cols + j] /= pv;
7387        }
7388        for row in 0..n {
7389            if row == col {
7390                continue;
7391            }
7392            let factor = aug[row * cols + col];
7393            for j in 0..cols {
7394                let val = aug[col * cols + j] * factor;
7395                aug[row * cols + j] -= val;
7396            }
7397        }
7398    }
7399    let mut result = Array2::<f64>::zeros((n, n));
7400    for i in 0..n {
7401        for j in 0..n {
7402            result[[i, j]] = aug[i * cols + n + j];
7403        }
7404    }
7405    Ok(result)
7406}
7407
7408/// Solves the linear system `A * x = B` using Gaussian elimination with partial pivoting.
7409///
7410/// `A` must be square (n×n); `B` must have n rows. Returns x (n × k where k = B.ncols()).
7411/// This is the engine for the `\` left-division operator.
7412fn solve_linear(a: &Array2<f64>, b: &Array2<f64>) -> Result<Array2<f64>, String> {
7413    let n = a.nrows();
7414    if a.ncols() != n {
7415        return Err(format!(
7416            "\\: coefficient matrix must be square, got {}×{}",
7417            n,
7418            a.ncols()
7419        ));
7420    }
7421    let k = b.ncols();
7422    if b.nrows() != n {
7423        return Err(format!(
7424            "\\: size mismatch — A is {}×{} but b has {} rows",
7425            n,
7426            n,
7427            b.nrows()
7428        ));
7429    }
7430    if n == 0 {
7431        return Ok(Array2::zeros((0, k)));
7432    }
7433    let cols = n + k;
7434    let mut aug = vec![0.0f64; n * cols];
7435    for i in 0..n {
7436        for j in 0..n {
7437            aug[i * cols + j] = a[[i, j]];
7438        }
7439        for j in 0..k {
7440            aug[i * cols + n + j] = b[[i, j]];
7441        }
7442    }
7443    for col in 0..n {
7444        let pivot = (col..n)
7445            .max_by(|&r1, &r2| {
7446                aug[r1 * cols + col]
7447                    .abs()
7448                    .partial_cmp(&aug[r2 * cols + col].abs())
7449                    .unwrap()
7450            })
7451            .filter(|&r| aug[r * cols + col].abs() > 1e-12)
7452            .ok_or_else(|| "\\: matrix is singular or nearly singular".to_string())?;
7453        if pivot != col {
7454            for j in 0..cols {
7455                aug.swap(col * cols + j, pivot * cols + j);
7456            }
7457        }
7458        let pv = aug[col * cols + col];
7459        for j in col..cols {
7460            aug[col * cols + j] /= pv;
7461        }
7462        for row in 0..n {
7463            if row == col {
7464                continue;
7465            }
7466            let factor = aug[row * cols + col];
7467            if factor == 0.0 {
7468                continue;
7469            }
7470            for j in col..cols {
7471                let val = aug[col * cols + j] * factor;
7472                aug[row * cols + j] -= val;
7473            }
7474        }
7475    }
7476    let mut result = Array2::<f64>::zeros((n, k));
7477    for i in 0..n {
7478        for j in 0..k {
7479            result[[i, j]] = aug[i * cols + n + j];
7480        }
7481    }
7482    Ok(result)
7483}
7484
7485// ---------------------------------------------------------------------------
7486// Advanced linear algebra helpers (Phase 18)
7487// ---------------------------------------------------------------------------
7488
7489/// QR decomposition via Householder reflectors.
7490///
7491/// For an m×n matrix A returns (Q, R) where Q is m×m orthogonal and R is
7492/// m×n upper triangular such that A = Q * R.
7493fn qr_decompose(a: &Array2<f64>) -> Result<(Array2<f64>, Array2<f64>), String> {
7494    let m = a.nrows();
7495    let n = a.ncols();
7496    let k = m.min(n);
7497    let mut r = a.clone();
7498    let mut q = Array2::<f64>::eye(m);
7499
7500    for j in 0..k {
7501        let col_len = m - j;
7502        let mut v: Vec<f64> = (j..m).map(|i| r[[i, j]]).collect();
7503
7504        let norm_x = v.iter().map(|&x| x * x).sum::<f64>().sqrt();
7505        if norm_x < 1e-14 {
7506            continue;
7507        }
7508        // Householder sign convention avoids cancellation.
7509        v[0] += if v[0] >= 0.0 { norm_x } else { -norm_x };
7510        let v_sq: f64 = v.iter().map(|&x| x * x).sum();
7511        if v_sq < 1e-28 {
7512            continue;
7513        }
7514
7515        // Apply H from left to R: R[j:m, :] -= 2*v*(v^T*R[j:m,:])/v^Tv
7516        for col in j..n {
7517            let dot: f64 = (0..col_len).map(|i| v[i] * r[[j + i, col]]).sum();
7518            let fac = 2.0 * dot / v_sq;
7519            for i in 0..col_len {
7520                r[[j + i, col]] -= fac * v[i];
7521            }
7522        }
7523        // Accumulate Q from right: Q[:, j:m] -= (Q[:,j:m]*v) * 2*v^T/v^Tv
7524        for row in 0..m {
7525            let dot: f64 = (0..col_len).map(|i| q[[row, j + i]] * v[i]).sum();
7526            let fac = 2.0 * dot / v_sq;
7527            for i in 0..col_len {
7528                q[[row, j + i]] -= fac * v[i];
7529            }
7530        }
7531    }
7532
7533    Ok((q, r))
7534}
7535
7536/// LU decomposition with partial pivoting.
7537///
7538/// For an n×n square matrix A returns (L, U, P) where P*A = L*U,
7539/// L is unit lower triangular, U is upper triangular, and P is a
7540/// permutation matrix.
7541type LuResult = Result<(Array2<f64>, Array2<f64>, Array2<f64>), String>;
7542fn lu_decompose(a: &Array2<f64>) -> LuResult {
7543    let n = a.nrows();
7544    if a.ncols() != n {
7545        return Err("lu: matrix must be square".to_string());
7546    }
7547    let mut u = a.clone();
7548    let mut l = Array2::<f64>::eye(n);
7549    let mut perm: Vec<usize> = (0..n).collect();
7550
7551    for j in 0..n {
7552        let pivot = (j..n)
7553            .max_by(|&r1, &r2| {
7554                u[[r1, j]]
7555                    .abs()
7556                    .partial_cmp(&u[[r2, j]].abs())
7557                    .unwrap_or(std::cmp::Ordering::Equal)
7558            })
7559            .unwrap();
7560
7561        if pivot != j {
7562            for col in 0..n {
7563                let tmp = u[[j, col]];
7564                u[[j, col]] = u[[pivot, col]];
7565                u[[pivot, col]] = tmp;
7566            }
7567            for col in 0..j {
7568                let tmp = l[[j, col]];
7569                l[[j, col]] = l[[pivot, col]];
7570                l[[pivot, col]] = tmp;
7571            }
7572            perm.swap(j, pivot);
7573        }
7574
7575        if u[[j, j]].abs() < 1e-15 {
7576            continue;
7577        }
7578        for i in (j + 1)..n {
7579            l[[i, j]] = u[[i, j]] / u[[j, j]];
7580            for k in j..n {
7581                let val = l[[i, j]] * u[[j, k]];
7582                u[[i, k]] -= val;
7583            }
7584        }
7585    }
7586
7587    let mut p = Array2::<f64>::zeros((n, n));
7588    for (i, &j) in perm.iter().enumerate() {
7589        p[[i, j]] = 1.0;
7590    }
7591    Ok((l, u, p))
7592}
7593
7594/// Cholesky decomposition.
7595///
7596/// For a symmetric positive-definite n×n matrix A returns the upper triangular
7597/// factor R such that A = R^T * R (MATLAB convention).
7598fn chol_decompose(a: &Array2<f64>) -> Result<Array2<f64>, String> {
7599    let n = a.nrows();
7600    if a.ncols() != n {
7601        return Err("chol: matrix must be square".to_string());
7602    }
7603    let mut r = Array2::<f64>::zeros((n, n));
7604    for j in 0..n {
7605        let mut s = a[[j, j]];
7606        for k in 0..j {
7607            s -= r[[k, j]] * r[[k, j]];
7608        }
7609        if s <= 0.0 {
7610            return Err("chol: matrix is not positive definite".to_string());
7611        }
7612        r[[j, j]] = s.sqrt();
7613        for i in (j + 1)..n {
7614            let mut t = a[[j, i]];
7615            for k in 0..j {
7616                t -= r[[k, j]] * r[[k, i]];
7617            }
7618            r[[j, i]] = t / r[[j, j]];
7619        }
7620    }
7621    Ok(r)
7622}
7623
7624/// One-sided Jacobi SVD (economy form).
7625///
7626/// For an m×n matrix A returns (U, s, V) where
7627/// - U is m×k with orthonormal columns (k = min(m,n))
7628/// - s is a `Vec<f64>` of singular values in descending order (length k)
7629/// - V is n×k with orthonormal columns
7630///
7631/// For m < n the inputs are transparently transposed and outputs swapped.
7632type SvdResult = Result<(Array2<f64>, Vec<f64>, Array2<f64>), String>;
7633fn svd_compute(a: &Array2<f64>) -> SvdResult {
7634    let m = a.nrows();
7635    let n = a.ncols();
7636    if m < n {
7637        let (v, s, u) = svd_compute(&a.t().to_owned())?;
7638        return Ok((u, s, v));
7639    }
7640    // m >= n from here.
7641    let k = n;
7642    let mut b = a.clone();
7643    let mut v = Array2::<f64>::eye(k);
7644
7645    const MAX_ITER: usize = 200;
7646    const EPS: f64 = 1e-14;
7647
7648    'outer: for _ in 0..MAX_ITER {
7649        let mut changed = false;
7650        for p in 0..k {
7651            for q in (p + 1)..k {
7652                let alpha: f64 = (0..m).map(|i| b[[i, p]] * b[[i, p]]).sum();
7653                let beta: f64 = (0..m).map(|i| b[[i, q]] * b[[i, q]]).sum();
7654                let gamma: f64 = (0..m).map(|i| b[[i, p]] * b[[i, q]]).sum();
7655
7656                if gamma.abs() <= EPS * (alpha * beta).sqrt() {
7657                    continue;
7658                }
7659                changed = true;
7660
7661                let zeta = (beta - alpha) / (2.0 * gamma);
7662                let t = zeta.signum() / (zeta.abs() + (1.0 + zeta * zeta).sqrt());
7663                let c = 1.0 / (1.0 + t * t).sqrt();
7664                let s = c * t;
7665
7666                for i in 0..m {
7667                    let bp = b[[i, p]];
7668                    let bq = b[[i, q]];
7669                    b[[i, p]] = c * bp - s * bq;
7670                    b[[i, q]] = s * bp + c * bq;
7671                }
7672                for i in 0..k {
7673                    let vp = v[[i, p]];
7674                    let vq = v[[i, q]];
7675                    v[[i, p]] = c * vp - s * vq;
7676                    v[[i, q]] = s * vp + c * vq;
7677                }
7678            }
7679        }
7680        if !changed {
7681            break 'outer;
7682        }
7683    }
7684
7685    let mut sigma: Vec<f64> = (0..k)
7686        .map(|j| (0..m).map(|i| b[[i, j]] * b[[i, j]]).sum::<f64>().sqrt())
7687        .collect();
7688    let mut u_mat = Array2::<f64>::zeros((m, k));
7689    for j in 0..k {
7690        if sigma[j] > EPS {
7691            for i in 0..m {
7692                u_mat[[i, j]] = b[[i, j]] / sigma[j];
7693            }
7694        }
7695    }
7696
7697    // Sort descending by singular value.
7698    let mut order: Vec<usize> = (0..k).collect();
7699    order.sort_by(|&a, &b| {
7700        sigma[b]
7701            .partial_cmp(&sigma[a])
7702            .unwrap_or(std::cmp::Ordering::Equal)
7703    });
7704    let sigma_s: Vec<f64> = order.iter().map(|&i| sigma[i]).collect();
7705    let mut u_s = Array2::<f64>::zeros((m, k));
7706    let mut v_s = Array2::<f64>::zeros((n, k));
7707    for (ni, &oi) in order.iter().enumerate() {
7708        for r in 0..m {
7709            u_s[[r, ni]] = u_mat[[r, oi]];
7710        }
7711        for r in 0..k {
7712            v_s[[r, ni]] = v[[r, oi]];
7713        }
7714    }
7715    sigma = sigma_s;
7716
7717    Ok((u_s, sigma, v_s))
7718}
7719
7720/// Extends an m×k matrix with orthonormal columns to a full m×m orthogonal matrix.
7721///
7722/// Tries each standard basis vector e_0..e_{m-1} in order; keeps those that
7723/// have non-negligible component orthogonal to the existing basis.
7724fn complete_orthonormal_basis(u: &Array2<f64>) -> Array2<f64> {
7725    let m = u.nrows();
7726    let k = u.ncols();
7727    let mut basis: Vec<Vec<f64>> = (0..k).map(|j| u.column(j).to_vec()).collect();
7728
7729    let mut ei = 0usize;
7730    while basis.len() < m && ei < m {
7731        let mut v: Vec<f64> = vec![0.0; m];
7732        v[ei] = 1.0;
7733        ei += 1;
7734        for b in &basis {
7735            let dot: f64 = v.iter().zip(b.iter()).map(|(&a, &b)| a * b).sum();
7736            for (vi, &bi) in v.iter_mut().zip(b.iter()) {
7737                *vi -= dot * bi;
7738            }
7739        }
7740        let norm = v.iter().map(|&x| x * x).sum::<f64>().sqrt();
7741        if norm > 1e-10 {
7742            for vi in &mut v {
7743                *vi /= norm;
7744            }
7745            basis.push(v);
7746        }
7747    }
7748
7749    let mut result = Array2::<f64>::zeros((m, m));
7750    for (j, b) in basis.iter().enumerate() {
7751        for (i, &val) in b.iter().enumerate() {
7752            result[[i, j]] = val;
7753        }
7754    }
7755    result
7756}
7757
7758/// QR-iteration eigendecomposition for a real square matrix.
7759///
7760/// Returns `(eigenvalues, eigenvectors)` where eigenvalues is a `Vec<f64>` of
7761/// length n and eigenvectors is an n×n matrix whose columns are the eigenvectors.
7762/// Uses the basic QR iteration with a simple diagonal shift (Wilkinson-style).
7763/// Convergence is reliable for symmetric matrices; general matrices converge for
7764/// most well-conditioned inputs within `MAX_ITER` steps.
7765fn eig_compute(a: &Array2<f64>) -> Result<(Vec<Complex<f64>>, Array2<f64>), String> {
7766    let n = a.nrows();
7767    if a.ncols() != n {
7768        return Err("eig: matrix must be square".to_string());
7769    }
7770    if n == 0 {
7771        return Ok((vec![], Array2::zeros((0, 0))));
7772    }
7773    if n == 1 {
7774        return Ok((vec![Complex::new(a[[0, 0]], 0.0)], Array2::eye(1)));
7775    }
7776
7777    let mut ak = a.clone();
7778    let mut evecs = Array2::<f64>::eye(n);
7779
7780    const MAX_ITER: usize = 2000;
7781    const EPS: f64 = 1e-12;
7782
7783    for _ in 0..MAX_ITER {
7784        // Wilkinson shift: uses the trailing 2×2 submatrix for cubic convergence.
7785        let mu = {
7786            let d = ak[[n - 1, n - 1]];
7787            if n >= 2 {
7788                let a = ak[[n - 2, n - 2]];
7789                let b = ak[[n - 2, n - 1]];
7790                let delta = (a - d) / 2.0;
7791                if delta.abs() < 1e-30 {
7792                    d - b.abs()
7793                } else {
7794                    d - b * b / (delta + delta.signum() * (delta * delta + b * b).sqrt())
7795                }
7796            } else {
7797                d
7798            }
7799        };
7800
7801        for i in 0..n {
7802            ak[[i, i]] -= mu;
7803        }
7804        let (q, r) = qr_decompose(&ak)?;
7805        ak = r.dot(&q);
7806        for i in 0..n {
7807            ak[[i, i]] += mu;
7808        }
7809        evecs = evecs.dot(&q);
7810
7811        // Convergence check: all sub-diagonals small (real eigenvalues only).
7812        let max_sub = (0..(n - 1))
7813            .map(|i| ak[[i + 1, i]].abs())
7814            .fold(0.0_f64, f64::max);
7815        if max_sub < EPS {
7816            break;
7817        }
7818    }
7819
7820    // Post-convergence scan: extract complex conjugate pairs from 2×2 quasi-triangular blocks.
7821    // A sub-diagonal entry larger than EPS_BLOCK indicates a complex eigenvalue pair.
7822    const EPS_BLOCK: f64 = 1e-8;
7823    let mut evals: Vec<Complex<f64>> = Vec::with_capacity(n);
7824    let mut i = 0;
7825    while i < n {
7826        if i + 1 < n && ak[[i + 1, i]].abs() > EPS_BLOCK {
7827            let (a_ii, b, c, d_ii) = (
7828                ak[[i, i]],
7829                ak[[i, i + 1]],
7830                ak[[i + 1, i]],
7831                ak[[i + 1, i + 1]],
7832            );
7833            let p = (a_ii + d_ii) / 2.0;
7834            let disc = ((a_ii - d_ii) / 2.0).powi(2) + b * c;
7835            if disc < 0.0 {
7836                let q = (-disc).sqrt();
7837                evals.push(Complex::new(p, q));
7838                evals.push(Complex::new(p, -q));
7839            } else {
7840                let q = disc.sqrt();
7841                evals.push(Complex::new(p + q, 0.0));
7842                evals.push(Complex::new(p - q, 0.0));
7843            }
7844            i += 2;
7845        } else {
7846            evals.push(Complex::new(ak[[i, i]], 0.0));
7847            i += 1;
7848        }
7849    }
7850
7851    Ok((evals, evecs))
7852}
7853
7854// ---------------------------------------------------------------------------
7855// Indexing
7856// ---------------------------------------------------------------------------
7857
7858/// Creates a copy of `env` with `end` set to `dim_size`.
7859/// Used by `eval_index` so that `end` in index expressions resolves to the correct dimension size.
7860fn env_with_end(env: &Env, dim_size: usize) -> Env {
7861    let mut e = env.clone();
7862    e.insert("end".to_string(), Value::Scalar(dim_size as f64));
7863    e
7864}
7865
7866/// Returns `true` if `expr` (or any sub-expression) references the identifier `end`.
7867///
7868/// Used to skip the full [`Env`] clone inside [`eval_index`] when `end` is absent.
7869pub(crate) fn contains_end(expr: &Expr) -> bool {
7870    match expr {
7871        Expr::Var(s) => s == "end",
7872        Expr::Number(_)
7873        | Expr::Colon
7874        | Expr::StrLiteral(_)
7875        | Expr::StringObjLiteral(_)
7876        | Expr::NaT
7877        | Expr::FuncHandle(_) => false,
7878        Expr::UnaryMinus(e)
7879        | Expr::UnaryNot(e)
7880        | Expr::Transpose(e)
7881        | Expr::PlainTranspose(e)
7882        | Expr::FieldGet(e, _) => contains_end(e),
7883        Expr::BinOp(l, _, r) => contains_end(l) || contains_end(r),
7884        Expr::Call(_, args) | Expr::DotCall(_, args) => args.iter().any(contains_end),
7885        Expr::Matrix(rows) => rows.iter().flat_map(|r| r.iter()).any(contains_end),
7886        Expr::Range(a, step, b) => {
7887            contains_end(a) || step.as_deref().is_some_and(contains_end) || contains_end(b)
7888        }
7889        Expr::Lambda { body, .. } => contains_end(body),
7890        Expr::CellLiteral(elems) => elems.iter().any(contains_end),
7891        Expr::CellIndex(a, b) => contains_end(a) || contains_end(b),
7892    }
7893}
7894
7895/// Evaluates `val(args...)` — indexing a variable with one or two index arguments.
7896///
7897/// Disambiguation rule (Octave semantics): a name that exists in `Env` is always
7898/// treated as a variable to be indexed, never as a function call.
7899fn eval_index(val: &Value, args: &[Expr], env: &Env) -> Result<Value, String> {
7900    match args.len() {
7901        0 => Err("Indexing requires at least one index".to_string()),
7902        1 => {
7903            // v(i), v(1:3), v(:), v(end), v(end-1:end)
7904            match val {
7905                Value::Void => Err("Cannot index into void".to_string()),
7906                Value::Lambda(_) | Value::Function { .. } | Value::Tuple(_) => {
7907                    Err("Cannot index into a function value".to_string())
7908                }
7909                Value::Cell(_) => Err("Use c{i} to index into a cell array, not c(i)".to_string()),
7910                Value::Struct(_) => {
7911                    Err("Use s.field to access struct fields, not s(i)".to_string())
7912                }
7913                Value::StructArray(arr) => {
7914                    let total = arr.len();
7915                    let _owned_env;
7916                    let env1: &Env = if contains_end(&args[0]) {
7917                        _owned_env = env_with_end(env, total);
7918                        &_owned_env
7919                    } else {
7920                        env
7921                    };
7922                    match resolve_dim(&args[0], total, env1)? {
7923                        DimIdx::All => {
7924                            // s(:) — return all elements as a new struct array
7925                            Ok(Value::StructArray(arr.clone()))
7926                        }
7927                        DimIdx::Indices(idxs) => {
7928                            if idxs.len() == 1 {
7929                                let i = idxs[0];
7930                                if i >= total {
7931                                    return Err(format!(
7932                                        "Index {} out of range (1..{})",
7933                                        i + 1,
7934                                        total
7935                                    ));
7936                                }
7937                                Ok(Value::Struct(arr[i].clone()))
7938                            } else {
7939                                let mut selected = Vec::with_capacity(idxs.len());
7940                                for &i in &idxs {
7941                                    if i >= total {
7942                                        return Err(format!(
7943                                            "Index {} out of range (1..{})",
7944                                            i + 1,
7945                                            total
7946                                        ));
7947                                    }
7948                                    selected.push(arr[i].clone());
7949                                }
7950                                Ok(Value::StructArray(selected))
7951                            }
7952                        }
7953                    }
7954                }
7955                Value::Scalar(n) => {
7956                    let _owned_env;
7957                    let env1: &Env = if contains_end(&args[0]) {
7958                        _owned_env = env_with_end(env, 1);
7959                        &_owned_env
7960                    } else {
7961                        env
7962                    };
7963                    match resolve_dim(&args[0], 1, env1)? {
7964                        DimIdx::All | DimIdx::Indices(_) => Ok(Value::Scalar(*n)),
7965                    }
7966                }
7967                Value::Complex(re, im) => {
7968                    let _owned_env;
7969                    let env1: &Env = if contains_end(&args[0]) {
7970                        _owned_env = env_with_end(env, 1);
7971                        &_owned_env
7972                    } else {
7973                        env
7974                    };
7975                    match resolve_dim(&args[0], 1, env1)? {
7976                        DimIdx::All | DimIdx::Indices(_) => Ok(Value::Complex(*re, *im)),
7977                    }
7978                }
7979                Value::ComplexMatrix(m) => {
7980                    let total = m.nrows() * m.ncols();
7981                    let _owned_env;
7982                    let env1: &Env = if contains_end(&args[0]) {
7983                        _owned_env = env_with_end(env, total);
7984                        &_owned_env
7985                    } else {
7986                        env
7987                    };
7988                    match resolve_dim(&args[0], total, env1)? {
7989                        DimIdx::All => {
7990                            // A(:) → column vector (column-major), as ComplexMatrix
7991                            let mut flat: Vec<Complex<f64>> = Vec::with_capacity(total);
7992                            for col in 0..m.ncols() {
7993                                for row in 0..m.nrows() {
7994                                    flat.push(m[[row, col]]);
7995                                }
7996                            }
7997                            Ok(Value::ComplexMatrix(
7998                                Array2::from_shape_vec((total, 1), flat).unwrap(),
7999                            ))
8000                        }
8001                        DimIdx::Indices(idxs) => {
8002                            let nrows = m.nrows();
8003                            let ncols_m = m.ncols();
8004                            let vals: Result<Vec<Complex<f64>>, String> = idxs
8005                                .iter()
8006                                .map(|&i| {
8007                                    let row = i % nrows;
8008                                    let col = i / nrows;
8009                                    if col >= ncols_m {
8010                                        Err(format!("Index {} out of range (1..{})", i + 1, total))
8011                                    } else {
8012                                        Ok(m[[row, col]])
8013                                    }
8014                                })
8015                                .collect();
8016                            let vals = vals?;
8017                            if vals.len() == 1 {
8018                                let c = vals[0];
8019                                Ok(make_complex(c.re, c.im))
8020                            } else {
8021                                let n = vals.len();
8022                                Ok(Value::ComplexMatrix(
8023                                    Array2::from_shape_vec((1, n), vals).unwrap(),
8024                                ))
8025                            }
8026                        }
8027                    }
8028                }
8029                Value::Matrix(m) => {
8030                    let total = m.nrows() * m.ncols();
8031                    let _owned_env;
8032                    let env1: &Env = if contains_end(&args[0]) {
8033                        _owned_env = env_with_end(env, total);
8034                        &_owned_env
8035                    } else {
8036                        env
8037                    };
8038                    match resolve_dim(&args[0], total, env1)? {
8039                        DimIdx::All => {
8040                            // A(:) → column vector, column-major order
8041                            let mut flat = Vec::with_capacity(total);
8042                            for col in 0..m.ncols() {
8043                                for row in 0..m.nrows() {
8044                                    flat.push(m[[row, col]]);
8045                                }
8046                            }
8047                            Ok(Value::Matrix(
8048                                Array2::from_shape_vec((total, 1), flat).unwrap(),
8049                            ))
8050                        }
8051                        DimIdx::Indices(idxs) => {
8052                            // Column-major linear indexing
8053                            let nrows = m.nrows();
8054                            let ncols_m = m.ncols();
8055                            let vals: Result<Vec<f64>, String> = idxs
8056                                .iter()
8057                                .map(|&i| {
8058                                    // i is 0-based, column-major
8059                                    let row = i % nrows;
8060                                    let col = i / nrows;
8061                                    if col >= ncols_m {
8062                                        Err(format!("Index {} out of range (1..{})", i + 1, total))
8063                                    } else {
8064                                        Ok(m[[row, col]])
8065                                    }
8066                                })
8067                                .collect();
8068                            let vals = vals?;
8069                            if vals.len() == 1 {
8070                                Ok(Value::Scalar(vals[0]))
8071                            } else {
8072                                let n = vals.len();
8073                                Ok(Value::Matrix(Array2::from_shape_vec((1, n), vals).unwrap()))
8074                            }
8075                        }
8076                    }
8077                }
8078                Value::Str(s) => {
8079                    // Index into a char array — returns char code(s)
8080                    let chars: Vec<char> = s.chars().collect();
8081                    let total = chars.len();
8082                    let _owned_env;
8083                    let env1: &Env = if contains_end(&args[0]) {
8084                        _owned_env = env_with_end(env, total);
8085                        &_owned_env
8086                    } else {
8087                        env
8088                    };
8089                    match resolve_dim(&args[0], total, env1)? {
8090                        DimIdx::All => {
8091                            let codes: Vec<f64> = chars.iter().map(|&c| c as u32 as f64).collect();
8092                            if codes.len() == 1 {
8093                                Ok(Value::Scalar(codes[0]))
8094                            } else {
8095                                let n = codes.len();
8096                                Ok(Value::Matrix(
8097                                    Array2::from_shape_vec((1, n), codes).unwrap(),
8098                                ))
8099                            }
8100                        }
8101                        DimIdx::Indices(idxs) => {
8102                            let mut selected = String::new();
8103                            for &i in &idxs {
8104                                if i >= chars.len() {
8105                                    return Err(format!("Index {} out of range", i + 1));
8106                                }
8107                                selected.push(chars[i]);
8108                            }
8109                            if selected.chars().count() == 1 {
8110                                Ok(Value::Scalar(selected.chars().next().unwrap() as u32 as f64))
8111                            } else {
8112                                Ok(Value::Str(selected))
8113                            }
8114                        }
8115                    }
8116                }
8117                Value::StringObj(s) => {
8118                    // String object indexing — treat as single element
8119                    let _owned_env;
8120                    let env1: &Env = if contains_end(&args[0]) {
8121                        _owned_env = env_with_end(env, 1);
8122                        &_owned_env
8123                    } else {
8124                        env
8125                    };
8126                    match resolve_dim(&args[0], 1, env1)? {
8127                        DimIdx::All | DimIdx::Indices(_) => Ok(Value::StringObj(s.clone())),
8128                    }
8129                }
8130                Value::DateTimeArray(v) => {
8131                    let total = v.len();
8132                    let _owned_env;
8133                    let env1: &Env = if contains_end(&args[0]) {
8134                        _owned_env = env_with_end(env, total);
8135                        &_owned_env
8136                    } else {
8137                        env
8138                    };
8139                    match resolve_dim(&args[0], total, env1)? {
8140                        DimIdx::All => Ok(Value::DateTimeArray(v.clone())),
8141                        DimIdx::Indices(idxs) => {
8142                            if idxs.len() == 1 {
8143                                let i = idxs[0];
8144                                if i >= total {
8145                                    return Err(format!(
8146                                        "Index {} out of range (1..{})",
8147                                        i + 1,
8148                                        total
8149                                    ));
8150                                }
8151                                Ok(Value::DateTime(v[i]))
8152                            } else {
8153                                let mut sel = Vec::with_capacity(idxs.len());
8154                                for &i in &idxs {
8155                                    if i >= total {
8156                                        return Err(format!(
8157                                            "Index {} out of range (1..{})",
8158                                            i + 1,
8159                                            total
8160                                        ));
8161                                    }
8162                                    sel.push(v[i]);
8163                                }
8164                                Ok(Value::DateTimeArray(sel))
8165                            }
8166                        }
8167                    }
8168                }
8169                Value::DurationArray(v) => {
8170                    let total = v.len();
8171                    let _owned_env;
8172                    let env1: &Env = if contains_end(&args[0]) {
8173                        _owned_env = env_with_end(env, total);
8174                        &_owned_env
8175                    } else {
8176                        env
8177                    };
8178                    match resolve_dim(&args[0], total, env1)? {
8179                        DimIdx::All => Ok(Value::DurationArray(v.clone())),
8180                        DimIdx::Indices(idxs) => {
8181                            if idxs.len() == 1 {
8182                                let i = idxs[0];
8183                                if i >= total {
8184                                    return Err(format!(
8185                                        "Index {} out of range (1..{})",
8186                                        i + 1,
8187                                        total
8188                                    ));
8189                                }
8190                                Ok(Value::Duration(v[i]))
8191                            } else {
8192                                let mut sel = Vec::with_capacity(idxs.len());
8193                                for &i in &idxs {
8194                                    if i >= total {
8195                                        return Err(format!(
8196                                            "Index {} out of range (1..{})",
8197                                            i + 1,
8198                                            total
8199                                        ));
8200                                    }
8201                                    sel.push(v[i]);
8202                                }
8203                                Ok(Value::DurationArray(sel))
8204                            }
8205                        }
8206                    }
8207                }
8208                Value::DateTime(_) | Value::Duration(_) => {
8209                    // Scalar datetime/duration: indexing with (1) is valid, returns self.
8210                    let _owned_env;
8211                    let env1: &Env = if contains_end(&args[0]) {
8212                        _owned_env = env_with_end(env, 1);
8213                        &_owned_env
8214                    } else {
8215                        env
8216                    };
8217                    match resolve_dim(&args[0], 1, env1)? {
8218                        DimIdx::All | DimIdx::Indices(_) => Ok(val.clone()),
8219                    }
8220                }
8221            }
8222        }
8223        2 => {
8224            // A(i, j), A(:, j), A(i, :), A(:, :), A(end, :), A(1:end, 2)
8225            if matches!(
8226                val,
8227                Value::Void
8228                    | Value::Str(_)
8229                    | Value::StringObj(_)
8230                    | Value::Lambda(_)
8231                    | Value::Function { .. }
8232                    | Value::Tuple(_)
8233                    | Value::Cell(_)
8234                    | Value::Struct(_)
8235                    | Value::StructArray(_)
8236                    | Value::DateTime(_)
8237                    | Value::Duration(_)
8238                    | Value::DateTimeArray(_)
8239                    | Value::DurationArray(_)
8240            ) {
8241                return Err("2D indexing not supported for this type".to_string());
8242            }
8243            // ComplexMatrix is explicitly supported below (not in the guard above)
8244            let (nrows, ncols) = match val {
8245                Value::Scalar(_) | Value::Complex(_, _) => (1, 1),
8246                Value::Matrix(m) => (m.nrows(), m.ncols()),
8247                Value::ComplexMatrix(m) => (m.nrows(), m.ncols()),
8248                _ => unreachable!(),
8249            };
8250            let _owned_r;
8251            let env_r: &Env = if contains_end(&args[0]) {
8252                _owned_r = env_with_end(env, nrows);
8253                &_owned_r
8254            } else {
8255                env
8256            };
8257            let _owned_c;
8258            let env_c: &Env = if contains_end(&args[1]) {
8259                _owned_c = env_with_end(env, ncols);
8260                &_owned_c
8261            } else {
8262                env
8263            };
8264            let row_idx = resolve_dim(&args[0], nrows, env_r)?;
8265            let col_idx = resolve_dim(&args[1], ncols, env_c)?;
8266
8267            let rows: Vec<usize> = match row_idx {
8268                DimIdx::All => (0..nrows).collect(),
8269                DimIdx::Indices(v) => v,
8270            };
8271            let cols: Vec<usize> = match col_idx {
8272                DimIdx::All => (0..ncols).collect(),
8273                DimIdx::Indices(v) => v,
8274            };
8275
8276            if rows.len() == 1 && cols.len() == 1 {
8277                match val {
8278                    Value::Scalar(n) => Ok(Value::Scalar(*n)),
8279                    Value::Complex(re, im) => Ok(Value::Complex(*re, *im)),
8280                    Value::Matrix(m) => Ok(Value::Scalar(m[[rows[0], cols[0]]])),
8281                    Value::ComplexMatrix(m) => {
8282                        let c = m[[rows[0], cols[0]]];
8283                        Ok(make_complex(c.re, c.im))
8284                    }
8285                    _ => unreachable!(),
8286                }
8287            } else {
8288                let out_r = rows.len();
8289                let out_c = cols.len();
8290                match val {
8291                    Value::ComplexMatrix(m) => {
8292                        let flat: Vec<Complex<f64>> = rows
8293                            .iter()
8294                            .flat_map(|&r| cols.iter().map(move |&c| m[[r, c]]))
8295                            .collect();
8296                        Ok(Value::ComplexMatrix(
8297                            Array2::from_shape_vec((out_r, out_c), flat).unwrap(),
8298                        ))
8299                    }
8300                    _ => {
8301                        let flat: Vec<f64> = rows
8302                            .iter()
8303                            .flat_map(|&r| {
8304                                cols.iter().map(move |&c| match val {
8305                                    Value::Scalar(n) => *n,
8306                                    Value::Complex(re, _) => *re,
8307                                    Value::Matrix(m) => m[[r, c]],
8308                                    _ => unreachable!(),
8309                                })
8310                            })
8311                            .collect();
8312                        Ok(Value::Matrix(
8313                            Array2::from_shape_vec((out_r, out_c), flat).unwrap(),
8314                        ))
8315                    }
8316                }
8317            }
8318        }
8319        n => Err(format!(
8320            "Indexing with {n} indices is not supported (max 2)"
8321        )),
8322    }
8323}
8324
8325/// Resolved index along one dimension. Indices are 0-based.
8326enum DimIdx {
8327    All,
8328    Indices(Vec<usize>),
8329}
8330
8331/// Resolves one index argument for a dimension of size `dim_size`.
8332/// `Expr::Colon` → `DimIdx::All`.
8333/// Scalar → single 0-based index (validates 1-based bounds).
8334/// Row/column vector → multiple 0-based indices.
8335/// Logical mask: a 0/1 vector whose length equals `dim_size` selects positions where value is 1.
8336fn resolve_dim(expr: &Expr, dim_size: usize, env: &Env) -> Result<DimIdx, String> {
8337    if matches!(expr, Expr::Colon) {
8338        return Ok(DimIdx::All);
8339    }
8340    let val = eval(expr, env)?;
8341    let floats: Vec<f64> = match val {
8342        Value::Void => {
8343            return Err("Index must be numeric, not void".to_string());
8344        }
8345        Value::Scalar(n) => vec![n],
8346        Value::Complex(re, im) => {
8347            if im != 0.0 {
8348                return Err("Index must be real, not complex".to_string());
8349            }
8350            vec![re]
8351        }
8352        Value::Matrix(m) => {
8353            // Allow 2-D matrices only when they qualify as a logical mask (same numel as dim_size).
8354            let total = m.nrows() * m.ncols();
8355            if m.nrows() > 1 && m.ncols() > 1 && total != dim_size {
8356                return Err("Index must be a scalar or vector, not a matrix".to_string());
8357            }
8358            // Collect in column-major order so mask positions align with linear indexing.
8359            if m.nrows() > 1 && m.ncols() > 1 {
8360                let mut v = Vec::with_capacity(total);
8361                for col in 0..m.ncols() {
8362                    for row in 0..m.nrows() {
8363                        v.push(m[[row, col]]);
8364                    }
8365                }
8366                v
8367            } else {
8368                m.iter().copied().collect()
8369            }
8370        }
8371        Value::Str(_) | Value::StringObj(_) => {
8372            return Err("Index must be numeric, not a string".to_string());
8373        }
8374        Value::ComplexMatrix(_) => {
8375            return Err("Index must be real, not a complex matrix".to_string());
8376        }
8377        Value::Lambda(_)
8378        | Value::Function { .. }
8379        | Value::Tuple(_)
8380        | Value::Cell(_)
8381        | Value::Struct(_)
8382        | Value::StructArray(_)
8383        | Value::DateTime(_)
8384        | Value::Duration(_)
8385        | Value::DateTimeArray(_)
8386        | Value::DurationArray(_) => {
8387            return Err("Index must be numeric, not a function or datetime".to_string());
8388        }
8389    };
8390    // Logical mask: a 0/1 array whose element count matches dim_size selects by boolean mask.
8391    if dim_size > 0 && floats.len() == dim_size && floats.iter().all(|&f| f == 0.0 || f == 1.0) {
8392        let idxs: Vec<usize> = floats
8393            .iter()
8394            .enumerate()
8395            .filter(|&(_, &f)| f == 1.0)
8396            .map(|(i, _)| i)
8397            .collect();
8398        return Ok(DimIdx::Indices(idxs));
8399    }
8400    let mut idxs = Vec::with_capacity(floats.len());
8401    for n in floats {
8402        let i = n.round() as i64;
8403        if i < 1 || i as usize > dim_size {
8404            return Err(format!("Index {i} out of range (1..{dim_size})"));
8405        }
8406        idxs.push(i as usize - 1);
8407    }
8408    Ok(DimIdx::Indices(idxs))
8409}
8410
8411/// Formats a number for display: integers without decimal point,
8412/// floats with up to 10 significant fractional digits, trailing zeros trimmed.
8413/// Always decimal — used for expression re-display, not user-facing output.
8414pub fn format_number(n: f64) -> String {
8415    if n.fract() == 0.0 && n.abs() < 1e15 {
8416        format!("{}", n as i64)
8417    } else if n != 0.0 && (n.abs() >= 1e15 || n.abs() < 1e-9) {
8418        trim_sci(&format!("{:.15e}", n))
8419    } else {
8420        let s = format!("{:.10}", n);
8421        s.trim_end_matches('0').trim_end_matches('.').to_string()
8422    }
8423}
8424
8425/// Formats a scalar `f64` for user-facing output using the given base and format mode.
8426pub fn format_scalar(n: f64, base: Base, mode: &FormatMode) -> String {
8427    // FormatMode::Hex always shows IEEE 754 bits regardless of base.
8428    if matches!(mode, FormatMode::Hex) {
8429        return format_decimal(n, mode);
8430    }
8431    match base {
8432        Base::Dec => format_decimal(n, mode),
8433        _ => format_non_dec(n, base),
8434    }
8435}
8436
8437/// Formats a complex number `re + im*i` for display.
8438///
8439/// - `a + 0i` → `a`  (pure real)
8440/// - `0 + bi` → `bi`
8441/// - `im == ±1` suppresses the coefficient: `i`, `-i`, `a + i`, `a - i`
8442pub fn format_complex(re: f64, im: f64, mode: &FormatMode) -> String {
8443    if im == 0.0 {
8444        return format_decimal(re, mode);
8445    }
8446    let im_abs = im.abs();
8447    let im_str = if im_abs == 1.0 {
8448        String::new()
8449    } else {
8450        format_decimal(im_abs, mode)
8451    };
8452    if re == 0.0 {
8453        if im < 0.0 {
8454            format!("-{}i", im_str)
8455        } else {
8456            format!("{}i", im_str)
8457        }
8458    } else {
8459        let re_str = format_decimal(re, mode);
8460        if im < 0.0 {
8461            format!("{} - {}i", re_str, im_str)
8462        } else {
8463            format!("{} + {}i", re_str, im_str)
8464        }
8465    }
8466}
8467
8468/// Reconstructs a source-like string from an `Expr`.
8469///
8470/// Used to populate the display string of lambda values so that
8471/// `f = @(x) x.^2` shows `f = @(x) x .^ 2` in the REPL.
8472pub fn expr_to_string(e: &Expr) -> String {
8473    match e {
8474        Expr::Number(n) => {
8475            if n.is_nan() {
8476                "nan".to_string()
8477            } else if n.is_infinite() {
8478                if *n > 0.0 {
8479                    "inf".to_string()
8480                } else {
8481                    "-inf".to_string()
8482                }
8483            } else {
8484                format!("{n}")
8485            }
8486        }
8487        Expr::Var(name) => name.clone(),
8488        Expr::UnaryMinus(e) => format!("-{}", expr_to_string(e)),
8489        Expr::UnaryNot(e) => format!("~{}", expr_to_string(e)),
8490        Expr::BinOp(l, op, r) => {
8491            let op_str = match op {
8492                Op::Add => "+",
8493                Op::Sub => "-",
8494                Op::Mul => "*",
8495                Op::Div => "/",
8496                Op::Pow => "^",
8497                Op::ElemMul => ".*",
8498                Op::ElemDiv => "./",
8499                Op::ElemPow => ".^",
8500                Op::Eq => "==",
8501                Op::NotEq => "~=",
8502                Op::Lt => "<",
8503                Op::Gt => ">",
8504                Op::LtEq => "<=",
8505                Op::GtEq => ">=",
8506                Op::And => "&&",
8507                Op::Or => "||",
8508                Op::ElemAnd => "&",
8509                Op::ElemOr => "|",
8510                Op::LDiv => "\\",
8511            };
8512            format!("{} {op_str} {}", expr_to_string(l), expr_to_string(r))
8513        }
8514        Expr::Call(name, args) => {
8515            let args_str = args
8516                .iter()
8517                .map(expr_to_string)
8518                .collect::<Vec<_>>()
8519                .join(", ");
8520            format!("{name}({args_str})")
8521        }
8522        Expr::Transpose(e) => format!("{}'", expr_to_string(e)),
8523        Expr::PlainTranspose(e) => format!("{}.'", expr_to_string(e)),
8524        Expr::Range(start, step, stop) => {
8525            if let Some(step) = step {
8526                format!(
8527                    "{}:{}:{}",
8528                    expr_to_string(start),
8529                    expr_to_string(step),
8530                    expr_to_string(stop)
8531                )
8532            } else {
8533                format!("{}:{}", expr_to_string(start), expr_to_string(stop))
8534            }
8535        }
8536        Expr::StrLiteral(s) => format!("'{s}'"),
8537        Expr::StringObjLiteral(s) => format!("\"{s}\""),
8538        Expr::Lambda { params, body, .. } => {
8539            format!("@({}) {}", params.join(", "), expr_to_string(body))
8540        }
8541        Expr::FuncHandle(name) => format!("@{name}"),
8542        Expr::Matrix(_) => "[...]".to_string(),
8543        Expr::CellLiteral(_) => "{...}".to_string(),
8544        Expr::CellIndex(e, i) => format!("{}{{{}}}", expr_to_string(e), expr_to_string(i)),
8545        Expr::Colon => ":".to_string(),
8546        Expr::NaT => "NaT".to_string(),
8547        Expr::FieldGet(base, field) => format!("{}.{field}", expr_to_string(base)),
8548        Expr::DotCall(segs, args) => {
8549            let args_str = args
8550                .iter()
8551                .map(expr_to_string)
8552                .collect::<Vec<_>>()
8553                .join(", ");
8554            format!("{}({args_str})", segs.join("."))
8555        }
8556    }
8557}
8558
8559/// Formats a `Value` compactly: scalars as a number string, matrices as `[NxM double]`.
8560pub fn format_value(v: &Value, base: Base, mode: &FormatMode) -> String {
8561    match v {
8562        Value::Void => String::new(),
8563        Value::Scalar(n) => format_scalar(*n, base, mode),
8564        Value::Matrix(m) => format!("[{}x{} double]", m.nrows(), m.ncols()),
8565        Value::ComplexMatrix(m) => format!("[{}×{} complex]", m.nrows(), m.ncols()),
8566        Value::Complex(re, im) => format_complex(*re, *im, mode),
8567        Value::Str(s) => s.clone(),
8568        Value::StringObj(s) => s.clone(),
8569        Value::Lambda(lf) => lf.1.clone(),
8570        Value::Function {
8571            params, outputs, ..
8572        } => {
8573            let params_str = params.join(", ");
8574            let out_str = match outputs.len() {
8575                0 => String::new(),
8576                1 => format!("{} = ", outputs[0]),
8577                _ => format!("[{}] = ", outputs.join(", ")),
8578            };
8579            format!("@function {out_str}f({params_str})")
8580        }
8581        Value::Tuple(vals) => {
8582            let parts: Vec<String> = vals.iter().map(|v| format_value(v, base, mode)).collect();
8583            format!("({})", parts.join(", "))
8584        }
8585        Value::Cell(v) => format!("{{1×{} cell}}", v.len()),
8586        Value::Struct(_) => "[1×1 struct]".to_string(),
8587        Value::StructArray(arr) => format!("[1×{} struct]", arr.len()),
8588        Value::DateTime(ts) => crate::datetime::format_datetime(*ts),
8589        Value::Duration(s) => crate::datetime::format_duration(*s),
8590        Value::DateTimeArray(v) => format!("[{}×1 datetime]", v.len()),
8591        Value::DurationArray(v) => format!("[{}×1 duration]", v.len()),
8592    }
8593}
8594
8595/// Returns `None` for scalars, complex numbers, strings, and void (displayed inline or suppressed);
8596/// `Some(full_string)` for matrices (MATLAB-style column-aligned display).
8597pub fn format_value_full(v: &Value, mode: &FormatMode) -> Option<String> {
8598    match v {
8599        Value::Void
8600        | Value::Scalar(_)
8601        | Value::Complex(_, _)
8602        | Value::Str(_)
8603        | Value::StringObj(_)
8604        | Value::Lambda(_)
8605        | Value::Function { .. }
8606        | Value::Tuple(_)
8607        | Value::DateTime(_)
8608        | Value::Duration(_) => None,
8609        Value::Matrix(m) => Some(format_matrix(m, mode)),
8610        Value::ComplexMatrix(m) => Some(format_complex_matrix(m, mode)),
8611        Value::Cell(elems) => Some(format_cell(elems, mode)),
8612        Value::Struct(map) => Some(format_struct(map, mode)),
8613        Value::StructArray(arr) => Some(format_struct_array(arr, mode)),
8614        Value::DateTimeArray(v) => Some(format_datetime_array(v)),
8615        Value::DurationArray(v) => Some(format_duration_array(v)),
8616    }
8617}
8618
8619/// Formats a cell array in MATLAB-style multi-line display.
8620fn format_cell(elems: &[Value], mode: &FormatMode) -> String {
8621    if elems.is_empty() {
8622        return "  {}".to_string();
8623    }
8624    let mut lines = vec!["  {".to_string()];
8625    for (i, val) in elems.iter().enumerate() {
8626        let label = format!("    [1,{}]", i + 1);
8627        match val {
8628            Value::Matrix(_) => {
8629                lines.push(format!("{label}:"));
8630                if let Some(full) = format_value_full(val, mode) {
8631                    for line in full.lines() {
8632                        lines.push(format!("   {line}"));
8633                    }
8634                }
8635            }
8636            Value::Cell(_) => {
8637                lines.push(format!("{label}: {}", format_value(val, Base::Dec, mode)));
8638            }
8639            _ => {
8640                lines.push(format!("{label}: {}", format_value(val, Base::Dec, mode)));
8641            }
8642        }
8643    }
8644    lines.push("  }".to_string());
8645    lines.join("\n")
8646}
8647
8648/// Formats a struct in MATLAB 2014b+ multi-line style.
8649fn format_struct(map: &IndexMap<String, Value>, mode: &FormatMode) -> String {
8650    let mut lines = vec![
8651        String::new(),
8652        "  scalar structure containing the fields:".to_string(),
8653        String::new(),
8654    ];
8655    for (key, val) in map {
8656        let val_str = match val {
8657            Value::Struct(_) => "[1×1 struct]".to_string(),
8658            Value::StructArray(arr) => format!("[1×{} struct]", arr.len()),
8659            Value::Matrix(m) => format!("[{}×{} double]", m.nrows(), m.ncols()),
8660            Value::Cell(v) => format!("{{1×{} cell}}", v.len()),
8661            _ => format_value(val, Base::Dec, mode),
8662        };
8663        lines.push(format!("    {key}: {val_str}"));
8664    }
8665    lines.join("\n")
8666}
8667
8668/// Formats a 1×N struct array (shows each element's fields).
8669fn format_struct_array(arr: &[IndexMap<String, Value>], mode: &FormatMode) -> String {
8670    let n = arr.len();
8671    let mut lines = vec![
8672        String::new(),
8673        format!("  1×{n} struct array with fields:"),
8674        String::new(),
8675    ];
8676    // Collect field names from the first element
8677    if let Some(first) = arr.first() {
8678        for key in first.keys() {
8679            lines.push(format!("    {key}"));
8680        }
8681    }
8682    // Show first element's values if array has exactly 1 element
8683    if n == 1
8684        && let Some(first) = arr.first()
8685    {
8686        lines.clear();
8687        lines.push(String::new());
8688        lines.push("  scalar structure containing the fields:".to_string());
8689        lines.push(String::new());
8690        for (key, val) in first {
8691            let val_str = match val {
8692                Value::Struct(_) => "[1×1 struct]".to_string(),
8693                Value::StructArray(a) => format!("[1×{} struct]", a.len()),
8694                Value::Matrix(m) => format!("[{}×{} double]", m.nrows(), m.ncols()),
8695                Value::Cell(v) => format!("{{1×{} cell}}", v.len()),
8696                _ => format_value(val, Base::Dec, mode),
8697            };
8698            lines.push(format!("    {key}: {val_str}"));
8699        }
8700    }
8701    lines.join("\n")
8702}
8703
8704fn format_datetime_array(v: &[f64]) -> String {
8705    let mut lines = Vec::with_capacity(v.len());
8706    for ts in v {
8707        lines.push(format!("  {}", crate::datetime::format_datetime(*ts)));
8708    }
8709    lines.join("\n")
8710}
8711
8712fn format_duration_array(v: &[f64]) -> String {
8713    let mut lines = Vec::with_capacity(v.len());
8714    for secs in v {
8715        lines.push(format!("  {}", crate::datetime::format_duration(*secs)));
8716    }
8717    lines.join("\n")
8718}
8719
8720/// Formats a complex matrix with right-aligned columns, 3-space indent, 3 spaces between columns.
8721/// Each element is formatted using [`format_complex`]; columns are aligned to the widest entry.
8722fn format_complex_matrix(m: &Array2<Complex<f64>>, mode: &FormatMode) -> String {
8723    if m.nrows() == 0 || m.ncols() == 0 {
8724        return "   []".to_string();
8725    }
8726    let ncols = m.ncols();
8727
8728    // Split each cell into (re_str, sign " + "/" - ", im_abs_str) so that the
8729    // real and imaginary parts can be right-aligned independently.  This keeps
8730    // the leading indent uniform regardless of how many digits each part needs.
8731    let parts: Vec<Vec<(String, &'static str, String)>> = m
8732        .rows()
8733        .into_iter()
8734        .map(|row| {
8735            row.iter()
8736                .map(|c| {
8737                    let re_str = format_decimal(c.re, mode);
8738                    let im_abs = format_decimal(c.im.abs(), mode);
8739                    let sign = if c.im < 0.0 { " - " } else { " + " };
8740                    (re_str, sign, im_abs)
8741                })
8742                .collect()
8743        })
8744        .collect();
8745
8746    // Per-column max widths for re and im parts independently.
8747    let re_widths: Vec<usize> = (0..ncols)
8748        .map(|c| parts.iter().map(|row| row[c].0.len()).max().unwrap_or(0))
8749        .collect();
8750    let im_widths: Vec<usize> = (0..ncols)
8751        .map(|c| parts.iter().map(|row| row[c].2.len()).max().unwrap_or(0))
8752        .collect();
8753
8754    let mut lines = Vec::new();
8755    for row in &parts {
8756        let mut line = String::from("   ");
8757        for (c, (re_str, sign, im_str)) in row.iter().enumerate() {
8758            if c > 0 {
8759                // Pad the previous column's im part so column boundaries stay fixed.
8760                // Inter-column gap is 3 spaces past the widest im in this column.
8761                let prev_im_pad = im_widths[c - 1].saturating_sub(row[c - 1].2.len());
8762                for _ in 0..prev_im_pad {
8763                    line.push(' ');
8764                }
8765                line.push_str("   ");
8766            }
8767            let re_pad = re_widths[c].saturating_sub(re_str.len());
8768            for _ in 0..re_pad {
8769                line.push(' ');
8770            }
8771            line.push_str(re_str);
8772            line.push_str(sign);
8773            line.push_str(im_str);
8774            line.push('i');
8775        }
8776        lines.push(line);
8777    }
8778    lines.join("\n")
8779}
8780
8781/// Formats a matrix with right-aligned columns, 3-space indent, 3 spaces between columns.
8782/// `FormatMode::Plus` renders a sign grid (`+`, `-`, `0`).
8783fn format_matrix(m: &Array2<f64>, mode: &FormatMode) -> String {
8784    if m.nrows() == 0 || m.ncols() == 0 {
8785        return "   []".to_string();
8786    }
8787    // Special rendering for format +
8788    if matches!(mode, FormatMode::Plus) {
8789        let lines: Vec<String> = m
8790            .rows()
8791            .into_iter()
8792            .map(|row| {
8793                let chars: String = row
8794                    .iter()
8795                    .map(|&x| {
8796                        if x > 0.0 {
8797                            '+'
8798                        } else if x < 0.0 {
8799                            '-'
8800                        } else {
8801                            '0'
8802                        }
8803                    })
8804                    .collect();
8805                format!("   {}", chars)
8806            })
8807            .collect();
8808        return lines.join("\n");
8809    }
8810    let ncols = m.ncols();
8811    let cells: Vec<Vec<String>> = m
8812        .rows()
8813        .into_iter()
8814        .map(|row| row.iter().map(|&x| format_decimal(x, mode)).collect())
8815        .collect();
8816    let col_widths: Vec<usize> = (0..ncols)
8817        .map(|c| cells.iter().map(|row| row[c].len()).max().unwrap_or(0))
8818        .collect();
8819    let mut lines = Vec::new();
8820    for row in &cells {
8821        let mut line = String::from("   ");
8822        for (c, cell) in row.iter().enumerate() {
8823            if c > 0 {
8824                line.push_str("   ");
8825            }
8826            let pad = col_widths[c].saturating_sub(cell.len());
8827            for _ in 0..pad {
8828                line.push(' ');
8829            }
8830            line.push_str(cell);
8831        }
8832        lines.push(line);
8833    }
8834    lines.join("\n")
8835}
8836
8837/// Formats a number in a non-decimal integer base (hex/bin/oct).
8838/// Rounds to the nearest integer before formatting.
8839pub fn format_non_dec(n: f64, base: Base) -> String {
8840    let i = n.round() as i64;
8841    let u = i.unsigned_abs();
8842    let sign = if i < 0 { "-" } else { "" };
8843    match base {
8844        Base::Hex => format!("{}0x{:X}", sign, u),
8845        Base::Bin => format!("{}0b{:b}", sign, u),
8846        Base::Oct => format!("{}0o{:o}", sign, u),
8847        Base::Dec => format_decimal(n, &FormatMode::default()),
8848    }
8849}
8850
8851// ---------------------------------------------------------------------------
8852// Internal decimal formatters
8853// ---------------------------------------------------------------------------
8854
8855fn format_decimal(n: f64, mode: &FormatMode) -> String {
8856    if n.is_nan() {
8857        return "NaN".to_string();
8858    }
8859    if n.is_infinite() {
8860        return if n > 0.0 { "Inf" } else { "-Inf" }.to_string();
8861    }
8862    match mode {
8863        FormatMode::Short | FormatMode::ShortG => fmt_auto_sig(n, 5),
8864        FormatMode::Long | FormatMode::LongG => fmt_auto_sig(n, 15),
8865        FormatMode::ShortE => fmt_sci_dp(n, 4),
8866        FormatMode::LongE => fmt_sci_dp(n, 14),
8867        FormatMode::Bank => format!("{:.2}", n),
8868        FormatMode::Rat => fmt_rat(n),
8869        FormatMode::Hex => fmt_hex_ieee754(n),
8870        FormatMode::Plus => fmt_plus_sign(n),
8871        FormatMode::Custom(prec) => fmt_custom_prec(n, *prec),
8872    }
8873}
8874
8875/// Integer shortcut: fits in i64 without fractional part.
8876#[inline]
8877fn is_exact_int(n: f64) -> bool {
8878    n.fract() == 0.0 && n.abs() < 1e15
8879}
8880
8881/// Auto fixed/scientific with `sig` significant digits (MATLAB-compatible).
8882/// Uses fixed notation for exponents in [-3, sig), scientific otherwise.
8883/// Integers are shown without a decimal point.
8884fn fmt_auto_sig(n: f64, sig: usize) -> String {
8885    if is_exact_int(n) {
8886        return format!("{}", n as i64);
8887    }
8888    let abs_n = n.abs();
8889    let exp = if abs_n == 0.0 {
8890        0i32
8891    } else {
8892        abs_n.log10().floor() as i32
8893    };
8894    if exp >= -3 && exp < sig as i32 {
8895        let dp = (sig as i32 - 1 - exp) as usize;
8896        let s = format!("{:.prec$}", n, prec = dp);
8897        // Only strip trailing zeros when there is a decimal point.
8898        if s.contains('.') {
8899            s.trim_end_matches('0').trim_end_matches('.').to_string()
8900        } else {
8901            s
8902        }
8903    } else {
8904        let s = format!("{:.prec$e}", n, prec = sig - 1);
8905        trim_sci(&s)
8906    }
8907}
8908
8909/// Always scientific notation with `dp` decimal places.
8910fn fmt_sci_dp(n: f64, dp: usize) -> String {
8911    let s = format!("{:.prec$e}", n, prec = dp);
8912    trim_sci(&s)
8913}
8914
8915/// Legacy custom-precision: N decimal places, auto fixed/scientific.
8916fn fmt_custom_prec(n: f64, prec: usize) -> String {
8917    if is_exact_int(n) {
8918        return format!("{}", n as i64);
8919    }
8920    if n.abs() >= 1e15 || (n != 0.0 && n.abs() < 1e-9) {
8921        let s = format!("{:.prec$e}", n, prec = prec);
8922        trim_sci(&s)
8923    } else {
8924        let s = format!("{:.prec$}", n, prec = prec);
8925        s.trim_end_matches('0').trim_end_matches('.').to_string()
8926    }
8927}
8928
8929/// Rational approximation via continued fractions. Returns `"p/q"` or `"p"` if denominator is 1.
8930fn fmt_rat(n: f64) -> String {
8931    if is_exact_int(n) {
8932        return format!("{}", n as i64);
8933    }
8934    let sign = if n < 0.0 { -1i64 } else { 1i64 };
8935    let x = n.abs();
8936    let (mut h1, mut h2): (i64, i64) = (1, 0);
8937    let (mut k1, mut k2): (i64, i64) = (0, 1);
8938    let mut b = x;
8939    for _ in 0..64 {
8940        let a = b.floor() as i64;
8941        let (nh, nk) = (a * h1 + h2, a * k1 + k2);
8942        if nk > 10_000 {
8943            break;
8944        }
8945        h2 = h1;
8946        h1 = nh;
8947        k2 = k1;
8948        k1 = nk;
8949        let frac = b - a as f64;
8950        if frac < 1e-12 || (h1 as f64 / k1 as f64 - x).abs() < 1e-6 {
8951            break;
8952        }
8953        b = 1.0 / frac;
8954    }
8955    let p = sign * h1;
8956    if k1 == 1 {
8957        format!("{}", p)
8958    } else {
8959        format!("{}/{}", p, k1)
8960    }
8961}
8962
8963/// IEEE 754 double-precision bit pattern as 16 uppercase hex digits.
8964fn fmt_hex_ieee754(n: f64) -> String {
8965    format!("{:016X}", n.to_bits())
8966}
8967
8968/// Sign indicator: `+`, `-`, or ` ` for zero.
8969fn fmt_plus_sign(n: f64) -> String {
8970    if n > 0.0 {
8971        "+".to_string()
8972    } else if n < 0.0 {
8973        "-".to_string()
8974    } else {
8975        " ".to_string()
8976    }
8977}
8978
8979fn trim_sci(s: &str) -> String {
8980    if let Some(e_pos) = s.find('e') {
8981        let mantissa = s[..e_pos].trim_end_matches('0').trim_end_matches('.');
8982        let exp_str = &s[e_pos + 1..];
8983        let (sign, digits) = if let Some(d) = exp_str.strip_prefix('-') {
8984            ("-", d)
8985        } else if let Some(d) = exp_str.strip_prefix('+') {
8986            ("+", d)
8987        } else {
8988            ("+", exp_str)
8989        };
8990        let exp_num: i32 = digits.parse().unwrap_or(0);
8991        format!("{}e{}{:02}", mantissa, sign, exp_num)
8992    } else {
8993        s.to_string()
8994    }
8995}
8996
8997// --- MAT built-in helpers ---
8998
8999/// Loads a MATLAB Level 5/7 MAT file and returns a [`Value::Struct`].
9000///
9001/// Requires the `mat` Cargo feature; without it, always returns an error.
9002pub fn load_mat_file(path: &str) -> Result<Value, String> {
9003    load_mat_file_impl(path)
9004}
9005
9006#[cfg(feature = "mat")]
9007fn load_mat_file_impl(path: &str) -> Result<Value, String> {
9008    crate::mat::mat_load(path)
9009}
9010
9011#[cfg(not(feature = "mat"))]
9012fn load_mat_file_impl(_path: &str) -> Result<Value, String> {
9013    Err("load: .mat support not available — rebuild with --features mat".to_string())
9014}
9015
9016// --- Regex built-in helpers ---
9017
9018#[cfg(feature = "regex")]
9019fn regexp_impl(
9020    fname: &str,
9021    s: &str,
9022    pat: &str,
9023    ignore_case: bool,
9024    return_match: bool,
9025) -> Result<Value, String> {
9026    use ndarray::Array2;
9027    let full_pat = if ignore_case {
9028        format!("(?i){pat}")
9029    } else {
9030        pat.to_string()
9031    };
9032    let re = regex::Regex::new(&full_pat).map_err(|e| format!("{fname}: invalid pattern: {e}"))?;
9033    if return_match {
9034        let matches: Vec<Value> = re
9035            .find_iter(s)
9036            .map(|m| Value::Str(m.as_str().to_string()))
9037            .collect();
9038        Ok(Value::Cell(matches))
9039    } else {
9040        match re.find(s) {
9041            Some(m) => Ok(Value::Scalar((s[..m.start()].chars().count() + 1) as f64)),
9042            None => Ok(Value::Matrix(Array2::zeros((0, 0)))),
9043        }
9044    }
9045}
9046
9047#[cfg(not(feature = "regex"))]
9048fn regexp_impl(
9049    fname: &str,
9050    _s: &str,
9051    _pat: &str,
9052    _ignore_case: bool,
9053    _return_match: bool,
9054) -> Result<Value, String> {
9055    Err(format!(
9056        "{fname}: not available — rebuild with --features regex"
9057    ))
9058}
9059
9060#[cfg(feature = "regex")]
9061fn regexprep_impl(s: &str, pat: &str, rep: &str) -> Result<Value, String> {
9062    let re = regex::Regex::new(pat).map_err(|e| format!("regexprep: invalid pattern: {e}"))?;
9063    let result = re.replace_all(s, regex::NoExpand(rep));
9064    Ok(Value::Str(result.into_owned()))
9065}
9066
9067#[cfg(not(feature = "regex"))]
9068fn regexprep_impl(_s: &str, _pat: &str, _rep: &str) -> Result<Value, String> {
9069    Err("regexprep: not available — rebuild with --features regex".to_string())
9070}
9071
9072// ── Phase 26 — FFT built-in helpers ─────────────────────────────────────────
9073
9074/// Extracts a flat real vector from a Scalar or 1-D Matrix (row or column).
9075#[cfg(feature = "fft")]
9076fn extract_real_vec(v: &Value, name: &str) -> Result<Vec<f64>, String> {
9077    match v {
9078        Value::Scalar(s) => Ok(vec![*s]),
9079        Value::Matrix(m) if m.nrows() == 1 || m.ncols() == 1 => Ok(m.iter().copied().collect()),
9080        Value::Matrix(m) => Err(format!(
9081            "{name}: input must be a vector (got {}×{} matrix)",
9082            m.nrows(),
9083            m.ncols()
9084        )),
9085        _ => Err(format!("{name}: input must be a real numeric vector")),
9086    }
9087}
9088
9089/// Wraps a `Vec<(f64,f64)>` of FFT output into a 1×N `Value::ComplexMatrix`.
9090#[cfg(feature = "fft")]
9091fn complex_pairs_to_complex_matrix(data: Vec<(f64, f64)>) -> Value {
9092    let n = data.len();
9093    if n == 0 {
9094        return Value::ComplexMatrix(Array2::zeros((1, 0)));
9095    }
9096    let elems: Vec<Complex<f64>> = data
9097        .into_iter()
9098        .map(|(re, im)| Complex::new(re, im))
9099        .collect();
9100    Value::ComplexMatrix(Array2::from_shape_vec((1, n), elems).unwrap())
9101}
9102
9103/// Extracts a flat complex vector from a [`Value::ComplexMatrix`], `Cell`, or real matrix.
9104#[cfg(feature = "fft")]
9105fn extract_complex_vec(v: &Value, name: &str) -> Result<Vec<(f64, f64)>, String> {
9106    match v {
9107        Value::Scalar(s) => Ok(vec![(*s, 0.0)]),
9108        Value::Matrix(m) => Ok(m.iter().copied().map(|x| (x, 0.0)).collect()),
9109        Value::ComplexMatrix(m) => Ok(m.iter().map(|c| (c.re, c.im)).collect()),
9110        Value::Cell(elems) => elems
9111            .iter()
9112            .enumerate()
9113            .map(|(i, e)| match e {
9114                Value::Complex(re, im) => Ok((*re, *im)),
9115                Value::Scalar(s) => Ok((*s, 0.0)),
9116                _ => Err(format!(
9117                    "{name}: cell element {} must be a complex or real number",
9118                    i + 1
9119                )),
9120            })
9121            .collect(),
9122        _ => Err(format!(
9123            "{name}: input must be a complex matrix, cell array, or numeric vector"
9124        )),
9125    }
9126}
9127
9128#[cfg(feature = "fft")]
9129fn fft_call(v: &Value, n_opt: Option<usize>) -> Result<Value, String> {
9130    let real = extract_real_vec(v, "fft")?;
9131    let n = n_opt.unwrap_or(real.len());
9132    if n == 0 {
9133        return Err("fft: length must be positive".to_string());
9134    }
9135    let out = crate::fft::fft_forward(&real, n);
9136    Ok(complex_pairs_to_complex_matrix(out))
9137}
9138
9139#[cfg(not(feature = "fft"))]
9140fn fft_call(_v: &Value, _n_opt: Option<usize>) -> Result<Value, String> {
9141    Err("fft: not available — rebuild with --features fft".to_string())
9142}
9143
9144#[cfg(feature = "fft")]
9145fn ifft_call(v: &Value) -> Result<Value, String> {
9146    let complex = extract_complex_vec(v, "ifft")?;
9147    if complex.is_empty() {
9148        return Ok(Value::Matrix(ndarray::Array2::zeros((1, 0))));
9149    }
9150    let out = crate::fft::fft_inverse(&complex);
9151    if out.iter().all(|(_, im)| im.abs() < 1e-12) {
9152        let real: Vec<f64> = out.iter().map(|(re, _)| *re).collect();
9153        let n = real.len();
9154        Ok(Value::Matrix(
9155            ndarray::Array2::from_shape_vec((1, n), real).unwrap(),
9156        ))
9157    } else {
9158        Ok(complex_pairs_to_complex_matrix(out))
9159    }
9160}
9161
9162#[cfg(not(feature = "fft"))]
9163fn ifft_call(_v: &Value) -> Result<Value, String> {
9164    Err("ifft: not available — rebuild with --features fft".to_string())
9165}
9166
9167// --- JSON built-in helpers ---
9168
9169#[cfg(feature = "json")]
9170fn jsondecode_impl(arg: &Value) -> Result<Value, String> {
9171    let s = match arg {
9172        Value::Str(s) | Value::StringObj(s) => s.as_str(),
9173        _ => return Err("jsondecode: argument must be a string".to_string()),
9174    };
9175    let jval: serde_json::Value =
9176        serde_json::from_str(s).map_err(|e| format!("jsondecode: invalid JSON: {e}"))?;
9177    Ok(crate::json::json_to_value(&jval))
9178}
9179
9180#[cfg(not(feature = "json"))]
9181fn jsondecode_impl(_arg: &Value) -> Result<Value, String> {
9182    Err("jsondecode: not available — rebuild with --features json".to_string())
9183}
9184
9185#[cfg(feature = "json")]
9186fn jsonencode_impl(arg: &Value) -> Result<Value, String> {
9187    let jval = crate::json::value_to_json(arg)?;
9188    let s = serde_json::to_string(&jval)
9189        .map_err(|e| format!("jsonencode: serialization error: {e}"))?;
9190    Ok(Value::Str(s))
9191}
9192
9193#[cfg(not(feature = "json"))]
9194fn jsonencode_impl(_arg: &Value) -> Result<Value, String> {
9195    Err("jsonencode: not available — rebuild with --features json".to_string())
9196}
9197
9198// ---------------------------------------------------------------------------
9199// Phase 24 — Polynomial helpers
9200// ---------------------------------------------------------------------------
9201
9202/// Evaluates a polynomial with real coefficients at a complex point using Horner's method.
9203fn cpoly_eval(coeffs: &[f64], z: (f64, f64)) -> (f64, f64) {
9204    let mut acc = (0.0_f64, 0.0_f64);
9205    for &c in coeffs {
9206        // acc = acc * z + c
9207        acc = (acc.0 * z.0 - acc.1 * z.1 + c, acc.0 * z.1 + acc.1 * z.0);
9208    }
9209    acc
9210}
9211
9212/// Evaluates a polynomial at a real point using Horner's method.
9213fn horner(coeffs: &[f64], x: f64) -> f64 {
9214    coeffs.iter().fold(0.0, |acc, &c| acc * x + c)
9215}
9216
9217/// Extracts polynomial (or 1-D knot) coefficients from a scalar or row/column vector `Value`.
9218fn poly_coeffs(v: &Value, fname: &str) -> Result<Vec<f64>, String> {
9219    match v {
9220        Value::Scalar(s) => Ok(vec![*s]),
9221        Value::Matrix(m) => {
9222            if m.nrows() == 1 {
9223                Ok(m.row(0).iter().copied().collect())
9224            } else if m.ncols() == 1 {
9225                Ok(m.column(0).iter().copied().collect())
9226            } else {
9227                Err(format!(
9228                    "{fname}: argument must be a vector, got {}×{}",
9229                    m.nrows(),
9230                    m.ncols()
9231                ))
9232            }
9233        }
9234        _ => Err(format!("{fname}: argument must be a real numeric vector")),
9235    }
9236}
9237
9238/// Discrete linear convolution of two sequences. Result length = `a.len() + b.len() − 1`.
9239fn poly_conv(a: &[f64], b: &[f64]) -> Vec<f64> {
9240    if a.is_empty() || b.is_empty() {
9241        return vec![];
9242    }
9243    let mut result = vec![0.0_f64; a.len() + b.len() - 1];
9244    for (i, &ai) in a.iter().enumerate() {
9245        for (j, &bj) in b.iter().enumerate() {
9246            result[i + j] += ai * bj;
9247        }
9248    }
9249    result
9250}
9251
9252/// Polynomial long division `c / b` → `(quotient, remainder)`.
9253///
9254/// The remainder has the same length as `c` (MATLAB convention), satisfying
9255/// `conv(b, q) + r == c` element-wise.
9256fn poly_deconv(c: &[f64], b: &[f64]) -> Result<(Vec<f64>, Vec<f64>), String> {
9257    if b.is_empty() || b.iter().all(|&x| x == 0.0) {
9258        return Err("deconv: divisor polynomial must not be zero".to_string());
9259    }
9260    let mc = c.len();
9261    let mb = b.len();
9262    if mb > mc {
9263        return Ok((vec![0.0], c.to_vec()));
9264    }
9265    let q_len = mc - mb + 1;
9266    let mut remainder = c.to_vec();
9267    let mut q = vec![0.0_f64; q_len];
9268    for i in 0..q_len {
9269        let coeff = remainder[i] / b[0];
9270        q[i] = coeff;
9271        for j in 0..mb {
9272            remainder[i + j] -= coeff * b[j];
9273        }
9274    }
9275    // Zero out rounding residuals relative to the input scale
9276    let scale = c.iter().map(|v| v.abs()).fold(0.0_f64, f64::max).max(1.0);
9277    for x in &mut remainder {
9278        if x.abs() < 1e-10 * scale {
9279            *x = 0.0;
9280        }
9281    }
9282    Ok((q, remainder))
9283}
9284
9285/// Finds all roots of `coeffs` (degree = `coeffs.len() − 1`) using the
9286/// Durand–Kerner (Weierstrass) iteration.
9287///
9288/// Returns roots as `(re, im)` pairs sorted by descending real part, then
9289/// descending imaginary part.
9290fn durand_kerner(coeffs: &[f64]) -> Result<Vec<(f64, f64)>, String> {
9291    let n = coeffs.len() - 1; // degree
9292    if n == 0 {
9293        return Ok(vec![]);
9294    }
9295    let lc = coeffs[0];
9296    if lc == 0.0 {
9297        return Err("roots: leading coefficient must not be zero".to_string());
9298    }
9299    // Normalize to monic polynomial
9300    let monic: Vec<f64> = coeffs.iter().map(|&c| c / lc).collect();
9301
9302    // Cauchy root bound: all roots have |z| ≤ r
9303    let r = 1.0 + monic[1..].iter().map(|c| c.abs()).fold(0.0_f64, f64::max);
9304
9305    // Initial guesses on a circle, rotated by 0.25/n turns to avoid the real axis
9306    // (a purely real start can stall for polynomials with purely imaginary roots).
9307    let mut z: Vec<(f64, f64)> = (0..n)
9308        .map(|k| {
9309            let angle = 2.0 * std::f64::consts::PI * (k as f64 + 0.25) / n as f64;
9310            (r * angle.cos(), r * angle.sin())
9311        })
9312        .collect();
9313
9314    const MAX_ITER: usize = 2000;
9315    const EPS: f64 = 1e-12;
9316
9317    for _ in 0..MAX_ITER {
9318        let z_old = z.clone();
9319        let mut max_corr = 0.0_f64;
9320        for i in 0..n {
9321            let (pre, pim) = cpoly_eval(&monic, z_old[i]);
9322            // denominator = Π_{j≠i}(z_i − z_j)
9323            let mut dre = 1.0_f64;
9324            let mut dim = 0.0_f64;
9325            for j in 0..n {
9326                if j == i {
9327                    continue;
9328                }
9329                let (dr, di) = (z_old[i].0 - z_old[j].0, z_old[i].1 - z_old[j].1);
9330                (dre, dim) = (dre * dr - dim * di, dre * di + dim * dr);
9331            }
9332            // correction = p(z_i) / denom
9333            let d2 = dre * dre + dim * dim;
9334            let (cre, cim) = if d2 > 0.0 {
9335                ((pre * dre + pim * dim) / d2, (pim * dre - pre * dim) / d2)
9336            } else {
9337                (pre, pim)
9338            };
9339            let corr_abs = (cre * cre + cim * cim).sqrt();
9340            max_corr = max_corr.max(corr_abs);
9341            z[i] = (z_old[i].0 - cre, z_old[i].1 - cim);
9342        }
9343        if max_corr < EPS {
9344            break;
9345        }
9346    }
9347
9348    // Sort by descending real part, then descending imaginary part
9349    z.sort_by(|a, b| {
9350        b.0.partial_cmp(&a.0)
9351            .unwrap_or(std::cmp::Ordering::Equal)
9352            .then(b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal))
9353    });
9354
9355    Ok(z)
9356}
9357
9358/// Converts a list of complex roots into a `Value`.
9359///
9360/// Returns a real `Matrix` (column vector) when all imaginary parts are below
9361/// `1e-9`; otherwise returns a `Cell` of `Scalar`/`Complex` elements.
9362fn roots_to_value(roots: &[(f64, f64)]) -> Value {
9363    const IMAG_TOL: f64 = 1e-9;
9364    let all_real = roots.iter().all(|(_, im)| im.abs() < IMAG_TOL);
9365    if all_real {
9366        let data: Vec<f64> = roots.iter().map(|(re, _)| *re).collect();
9367        let n = data.len();
9368        Value::Matrix(Array2::from_shape_vec((n, 1), data).unwrap())
9369    } else {
9370        let vals: Vec<Value> = roots
9371            .iter()
9372            .map(|&(re, im)| {
9373                if im.abs() < IMAG_TOL {
9374                    Value::Scalar(re)
9375                } else {
9376                    Value::Complex(re, im)
9377                }
9378            })
9379            .collect();
9380        Value::Cell(vals)
9381    }
9382}
9383
9384/// Computes the characteristic polynomial of a square matrix using the
9385/// Faddeev-LeVerrier algorithm.
9386///
9387/// Returns coefficients `[1, c_{n-1}, …, c_0]` in descending degree order.
9388fn characteristic_poly(a: &Array2<f64>) -> Result<Vec<f64>, String> {
9389    let n = a.nrows();
9390    if a.ncols() != n {
9391        return Err("poly: matrix must be square".to_string());
9392    }
9393    if n == 0 {
9394        return Ok(vec![1.0]);
9395    }
9396    let mut coeffs = vec![0.0_f64; n + 1];
9397    coeffs[0] = 1.0;
9398    let mut nk = Array2::<f64>::eye(n); // N_0 = I
9399    for (k, coeff) in coeffs.iter_mut().enumerate().skip(1) {
9400        let ank = a.dot(&nk); // A * N_{k-1}
9401        let tr: f64 = (0..n).map(|i| ank[[i, i]]).sum();
9402        let ak = -tr / k as f64;
9403        *coeff = ak;
9404        nk = ank; // N_k = A*N_{k-1} + a_k*I
9405        for i in 0..n {
9406            nk[[i, i]] += ak;
9407        }
9408    }
9409    Ok(coeffs)
9410}
9411
9412/// Back-substitution solver for upper-triangular system `R * x = b`.
9413fn poly_back_sub(r: &Array2<f64>, b: &[f64]) -> Result<Vec<f64>, String> {
9414    let n = r.nrows();
9415    let mut x = vec![0.0_f64; n];
9416    for i in (0..n).rev() {
9417        let mut s = b[i];
9418        for j in (i + 1)..n {
9419            s -= r[[i, j]] * x[j];
9420        }
9421        if r[[i, i]].abs() < 1e-14 {
9422            return Err(
9423                "polyfit: Vandermonde matrix is rank-deficient; reduce polynomial degree"
9424                    .to_string(),
9425            );
9426        }
9427        x[i] = s / r[[i, i]];
9428    }
9429    Ok(x)
9430}
9431
9432/// Evaluates `interp1` at a single query point `xi` using the given `method`.
9433///
9434/// Returns `NaN` for queries outside `[x[0], x[n-1]]`.
9435fn interp1_at(x: &[f64], y: &[f64], xi: f64, method: &str) -> f64 {
9436    let n = x.len();
9437    if xi < x[0] || xi > x[n - 1] {
9438        return f64::NAN;
9439    }
9440    // Index of the leftmost knot ≤ xi (in [0, n-1])
9441    let lo = x.partition_point(|&xk| xk <= xi).saturating_sub(1);
9442    // For methods that need a right neighbour, clamp to n-2
9443    let lo2 = lo.min(n - 2);
9444    match method {
9445        "nearest" => {
9446            if lo == n - 1 {
9447                return y[n - 1];
9448            }
9449            if (xi - x[lo2]) <= (x[lo2 + 1] - xi) {
9450                y[lo2]
9451            } else {
9452                y[lo2 + 1]
9453            }
9454        }
9455        "previous" => y[lo],
9456        "next" => {
9457            if lo == n - 1 || xi == x[lo] {
9458                y[lo]
9459            } else {
9460                y[lo2 + 1]
9461            }
9462        }
9463        _ => {
9464            // "linear" (default)
9465            if lo == n - 1 {
9466                return y[n - 1];
9467            }
9468            let t = (xi - x[lo2]) / (x[lo2 + 1] - x[lo2]);
9469            y[lo2] + t * (y[lo2 + 1] - y[lo2])
9470        }
9471    }
9472}
9473
9474#[cfg(test)]
9475#[path = "eval_tests.rs"]
9476mod tests;